8

I'm using links like #!/OrderList?id=123, which shows the list of all orders and more details of the order 123. With reloadOnSearch=false and watching $routeUpdate it works fine, except for one thing: All such links get put into the browsers history, while I'd prefer to have only one such link there. For example, instead of

#!/OrderList?id=123
#!/OrderList?id=124
#!/OrderList?id=125
#!/AnotherList?id=678
#!/AnotherList?id=679

just the last member of each group, i.e.,

#!/OrderList?id=125
#!/AnotherList?id=679

I'm aware of $location.replace(), but I can't see where to place it when the change happens via following a link. I tried to place it in $scope.$on("$routeUpdate", ...), but it did nothing, probably because it's too late when the route has already changed.

I'm not using neither router-ui nor the HTML5 mode (just plain angular-route).


I'm afraid, I wasn't clear about me insisting on using href rather than a custom handler. I want the links to work with middle mouse click and bookmarks and everything. A combination of ng-href and ng-click might do what I want, but I've found a simple solution working with plain links.

Community
  • 1
  • 1
maaartinus
  • 44,714
  • 32
  • 161
  • 320
  • 1
    Might be a duplicate of http://stackoverflow.com/questions/19012915/angularjs-redirect-without-pushing-a-history-state – Ven Sep 05 '16 at 06:54
  • @Ven The big difference is that in the linked question, the *application calls* `$location.path('/someNewPath').replace();` where in my case, the *user* is e.g. on `/someNewPath?id=1` and *clicks* on `'/someNewPath?id=2`. – maaartinus Sep 05 '16 at 08:30
  • there's another answer that talks about this case I think? – Ven Sep 05 '16 at 08:43
  • What about [`$routeChangeStart`](https://docs.angularjs.org/api/ngRoute/service/$route#$routeChangeStart)? – adamdport Sep 09 '16 at 19:17
  • @Ven Sorry, I don't really understand the other answer. – maaartinus Sep 12 '16 at 14:04

4 Answers4

3

Looks like you may want to update the URL query parameter using an ng-click function instead of relying on a link, then call a function like the one below to update the parameter... With replace state, the history should only track the current value. I haven't tested this case so if you try it, let me know if it works.

        function changeUrlParam (param, value) {
            var currentURL = window.location.href;
            var urlObject = currentURL.split('?');
            var newQueryString = '?';

            value = encodeURIComponent(value);

            if(urlObject.length > 1){
                var queries = urlObject[1].split('&');

                var updatedExistingParam = false;
                for (i = 0; i < queries.length; i++){
                    var queryItem = queries[i].split('=');

                    if(queryItem.length > 1){
                         if(queryItem[0] == param){
                            newQueryString += queryItem[0] + '=' + value + '&';
                            updatedExistingParam = true;
                         }else{
                            newQueryString += queryItem[0] + '=' + queryItem[1] + '&';
                         }
                    }
                }
                if(!updatedExistingParam){
                    newQueryString += param + '=' + value + '&';
                }
            }else{
                newQueryString += param + '=' + value + '&';
            }
            window.history.replaceState('', '', urlObject[0] + newQueryString.slice(0, -1));
        }
2

Maybe what you can do is, istead of a regular <a ng-href="#!/OrderList?id={{your.id}}">Link to your ID</a> you can create a link with an ng-clickdirective bound to a function which retrieves the data and passes it to the view.

Your HTML

`<span ng-click="loadListItem(your.id)">Link to your ID</span>`

<div id="your-item-data">
    {{item.id}} - {{item.name}}
</div>

Your controller

myApp.controller('someController', function($scope) {
    $scope.loadListItem(itemId) = function (
        var myItem;

        // Get item by 'itemId' and assign it to 'myItem' var

        $scope.item = myItem;
    );
});

This way instead of changing your URL, you can retrieve the item data in your controller and pass it to your view.

You don't give much detail of your controller/service implementation, but I hope this helps.

Alvaro Vazquez
  • 372
  • 3
  • 9
  • I'm afraid, I wasn't clear about me insisting on using `href` rather than a custom handler. I want the links to work with middle mouse click and bookmarks and everything. – maaartinus Sep 13 '16 at 09:02
2

I think you were on the right track with the $scope.$on("$routeUpdate", ...) thing. Rather than $routeUpdate, however, try binding on $routeChangeStart:

$scope.$on("$routeChangeStart", function(event, nextRoute, currentRoute){
  if (nextRoute.yourCriteria === currentRoute.yourCriteria){
    //do your location replacement magic
  }
});

If you wanted, you could even define a dontUpdateHistory boolean property in your route definitions, and then check for that property in your run block:

myApp.config(function($routeProvider){
  $routeProvider.when('/whatever' {
    templateUrl: 'whatever',
    dontUpdateHistory: true  //something like this
  });
}).run(function($rootScope){
  $rootScope.on('$routeChangeStart', function(event, nextRoute, currentRoute){
  if (nextRoute.dontUpdateHistory){
    //do your location replacement magic
  }
});

I haven't tested any of this, but hopefully it gets the idea across.

adamdport
  • 11,687
  • 14
  • 69
  • 91
  • It sounds good, but doesn't work: There's no `$routeChangeStart` when changing the `search` only and using `reloadOnSearch=false`. +++ Concerning the second part, that's not what I wanted: Independently of where I am, there should be a new history entry iff I go "far", i.e., the `path` changes. – maaartinus Sep 12 '16 at 04:44
  • I tried calling `$location.replace()` on `$locationChangeStart`, and it gets called and does nothing at all. – maaartinus Sep 12 '16 at 05:11
  • @maaartinus yes you would have to set `reloadOnSearch=true` and then call `event.preventDefault()` within `$routeChangeStart` whenn you want to prevent reloading. Regarding the `path` changes, you can get the path from the route objects `nextRoute` and `currentRoute` and check for whatever criteria you want. – adamdport Sep 12 '16 at 13:03
  • Sorry if it's not helpful! – adamdport Sep 12 '16 at 13:52
  • It was helpful; I started with your code. But to be really helpful, you'd have to try it out and find out why it doesn't work. The angularjs global handler stores the current value of `$location.$$replace` set by `$location.replace()`; that's why setting it in `$locationChangeStart` is too late. So I [cancel the change and redo it with replace](http://stackoverflow.com/a/39452369/581205). – maaartinus Sep 13 '16 at 08:59
0

I wasn't satisfied with any answer and after quite some debugging I found this solution:

.run(function($rootScope, $location) {
    var replacing;

    $rootScope.$on("$locationChangeStart", function(event, newUrl, oldUrl) {
        if (oldUrl === newUrl) return; // Nobody cares.

        // Make urls relative.
        var baseLength = $location.absUrl().length - $location.url().length;
        newUrl = newUrl.substring(baseLength);
        oldUrl = oldUrl.substring(baseLength);

        // Strip search, leave path only.
        var newPath = newUrl.replace(/\?.*/, "");
        var oldPath = oldUrl.replace(/\?.*/, "");

        // Substantial change, history should be written normally.
        if (oldPath !== newPath) return;

        // We're replacing, just let it happen.
        if (replacing) {
            replacing = false;
            return;
        }

        // We're NOT replacing, scratch it ...          
        event.preventDefault();

        // ... and do the same transition with replace later.
        $rootScope.$evalAsync(function() {
            $location.url(newUrl).replace();
            replacing = true;
        });
    });
})
maaartinus
  • 44,714
  • 32
  • 161
  • 320