Angular.js: Option on $location to allow hash/path change w/o reloading the route

Created on 12 Dec 2012  ·  194Comments  ·  Source: angular/angular.js

We have a paradigm in our app that user's create new things on the same page that they view things. So our route is like /thing/:id. When creating a new thing they go to /thing/new. Once the thing has been successfully saved we want to change the route to /thing/1234 (or whatever its new id is). Now the partial doesnt need to be reloaded because the data is all the same. We just want the path to be updated so a user can now bookmark the correct link, etc.

Having an option on $location (not on the route definition) to enable/disable route loading would work but I'm sure there are other ways to implement the feature.

Lots of comments $location ngRoute high confusing feature

Most helpful comment

The solution is essentially to add an extra parameter to $location.path().

app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) {
    var original = $location.path;
    $location.path = function (path, reload) {
        if (reload === false) {
            var lastRoute = $route.current;
            var un = $rootScope.$on('$locationChangeSuccess', function () {
                $route.current = lastRoute;
                un();
            });
        }
        return original.apply($location, [path]);
    };
}]);

And then just use like this

$location.path('/path/that/you/need', false);

All 194 comments

+1, having the same issue.

@cgross Since the $location service uses JQuery-style method chaining, and all commands are deferred until $digest anyway, I suggest adding a new chaining method, e.g. reload(boolean):

$location.path("/thing/"+thing.id).replace().reload(false)

P.S. Is this the Chris Gross that started the Eclipse Nebula project?

Yup :)

+1 :+1:

+1

Right now, we're using a dirty hack to get around this problem.

    $scope.$on('$locationChangeSuccess', function(event) {

        // Want to prevent re-loading when going from /dataEntry/1 to some other dataEntry path
        if ($route && $route.current && $route.current.$route.templateUrl.indexOf('dataEntry') > 0) {
            $route.current = lastRoute; //Does the actual prevention of routing
        }
});

+1

+1

+1

+1

+1

Here is my attempt at fixing this by supporting preventDefault() for $routeChangeStart. What do you think about that?
Commit : https://github.com/redhotsly/angular.js/commit/f8ac46e6ac9d76cf855077d21b68b4c2c8043db1

I've been working into @qualidafial approach for a few days and now the PR is ready. I've created a new notify method that allows skipping the location change notification for one time. This should avoid triggering any route change or reload.

This doesn't touch the routing system, so you don't have to change your route definitions. It will still work even if you use ui-router instead of the standard AngularJS routing system.

Example: $location.path('/client/2').replace().notify(false);

I've also worked some tests and documentation bits.

+1

another +1
Using @lrlopez' pull request already. Works like a charm.

+1

+1

Unfortunately PR https://github.com/angular/angular.js/pull/2398 has been rejected by the Angular team as skipping notifications could lead to inconsistencies between the actual URL and the current route. There is a long explanation at the end of the discussion.

I don't need this feature in any of my projects, but I'll keep it in my repository so it can be merged if you wish. Just let me know if it gets out of sync with the master branch so I can rebase the changes. Thanks!

+1

+1, need this :(

+1

Edit: better approach here: https://github.com/angular/angular.js/issues/1699#issuecomment-22511464

I've created a reusable factory to bypass this (based on the idea from @shahmirn):

/**
 * HACK Do not reload the current template if it is not needed.
 *
 * See AngularJS: Change hash and route without completely reloading controller http://stackoverflow.com/questions/12115259/angularjs-change-hash-and-route-without-completely-reloading-controller
 */
app.factory('DoNotReloadCurrentTemplate', ['$route', function($route) {
  return function(scope) {
    var lastRoute = $route.current;
    scope.$on('$locationChangeSuccess', function() {
      if (lastRoute.$$route.templateUrl === $route.current.$$route.templateUrl) {
        console.log('DoNotReloadCurrentTemplate not reloading template: ' + $route.current.$$route.templateUrl);
        $route.current = lastRoute;
      }
    });
  };
}]);

How to use:

app.controller('MyCtrl',
  ['$scope', 'DoNotReloadCurrentTemplate',
  function($scope, DoNotReloadCurrentTemplate) {

  DoNotReloadCurrentTemplate($scope);
}]);

Source: http://stackoverflow.com/a/16496112/990356

If you set "reloadOnSearch" to false on the route then it seems to fix the hash changes reloading the whole page.

+1

This would be great for mobile apps, where the ng-view create/destroy lifecycle is not appropriate (kills performance and usability).

@tkrotoff Thanks for the example. Since your workaround is cancelling event notifications I've added a broadcast on the rootscope so other controllers can still be notified of the route change:

/**
 * HACK Do not reload the current template if it is not needed.
 *
 * See AngularJS: Change hash and route without completely reloading controller http://stackoverflow.com/questions/12115259/angularjs-change-hash-and-route-without-completely-reloading-controller
 */
app.factory('DoNotReloadCurrentTemplate', ['$route', '$rootScope', function($route, $rootScope) {
  return function(scope) {
    var lastRoute = $route.current;
    scope.$on('$locationChangeSuccess', function() {
      if (lastRoute.$$route.templateUrl === $route.current.$$route.templateUrl) {
        console.log('DoNotReloadCurrentTemplate not reloading template: ' + $route.current.$$route.templateUrl);
        $rootScope.$broadcast('locationChangedWithoutReload', $route.current);
        $route.current = lastRoute;        
      }
    });
  };
}]);

Then:

app.controller('MyCtrl',
  ['$scope', 'DoNotReloadCurrentTemplate',
  function($scope, DoNotReloadCurrentTemplate) {

  DoNotReloadCurrentTemplate($scope);

  $rootScope.$on('locationChangedWithoutReload', function(event, route) {
    // set location specific breadcrumbs
    $scope.breadcrumbs = assembleBreadCrumbs();

    // do something for specific route
    if (route.$$route.action == 'item') {
      $scope.item = $scope.items[params.itemId];
      $rootScope.title = $scope.item.name;
    }
  });
}]);

I have the same issue as the topic author has and created modified version of solution described above.

app.factory('skipReload', [
    '$route',
    '$rootScope',
    function ($route, $rootScope) {
        return function () {
            var lastRoute = $route.current;
            var un = $rootScope.$on('$locationChangeSuccess', function () {
                $route.current = lastRoute;
                un();
            });
        };
    }
]);

Usage:

app.controller('ThingCtrl', ['$scope', '$http', '$location', 'skipReload', function ($scope, $http, $location, skipReload) {
    ...
    $scope.submit = function () {
        ...
        $http.post('thing', thing).success(function (id) {
            skipReload();
            $location.path('/thing/' + id).replace();
        });
    };
}]);

Using permanent event listener that tests templateUrl to decide whether to allow route reload, could break routing to other views that uses same templateUrl. For example, I routed to /thing/new and the view shows link to last added thing /thing/5, I'll be unable to navigate to thing 5 and to any other thing (even changing url in browser explicitly). Another problem with templateUrl testing is that templates could be specified by using template property (as string or function).

This hack allows to skip exactly one route reload and doesn't break routing.

Also I've tried less verbose (for usage in controller) solution:

app.factory('location', [
    '$location',
    '$route',
    '$rootScope',
    function ($location, $route, $rootScope) {
        $location.skipReload = function () {
            var lastRoute = $route.current;
            var un = $rootScope.$on('$locationChangeSuccess', function () {
                $route.current = lastRoute;
                un();
            });
            return $location;
        };
        return $location;
    }
]);
app.controller('ThingCtrl', ['$scope', '$http', 'location', function ($scope, $http, location) {
    ...
    $scope.submit = function () {
        ...
        $http.post('thing', thing).success(function (id) {
            location.skipReload().path('/thing/' + id).replace();
        });
    };
}]);

Seems working fine.

+1

@sergey-buturlakin your $location.skipReload is just wonderful! I use it instead of DoNotReloadCurrentTemplate

@sergey-buturlakin Your solution looks to be working for me. Thanks!

after running skipReload() from https://github.com/angular/angular.js/issues/1699#issuecomment-22511464 it keeps reloading disabled
it looks like un() should be called later

It prevents any next reload, and should be called right before path change.
When path change finished - "un" will be called.

yes, but I wanted to skip reload only once, however proposed approach prevents any further reloading

+1

+1

+1

PR https://github.com/angular/angular.js/pull/2398 only skips reload once. I don't know if it has merge conflicts with current master. If that's the case, just drop me a line and I'll fix it.

I use the sergey's solution, while droping the useless (according to me) "un" variable and call.

This leads to:

  angular.module('services.locationChanger', [])
    .service('locationChanger', ['$location', '$route', '$rootScope', function ($location, $route, $rootScope) {

        this.skipReload = function () {
            var lastRoute = $route.current;
            $rootScope.$on('$locationChangeSuccess', function () {
                $route.current = lastRoute;
            });
            return this;
        };

        this.withoutRefresh = function (url, doesReplace) {
            if(doesReplace){
                $location.path(url).replace();
            }
            else {
                $location.path(url || '/');
            }
        };
    }]);

and any caller:

 locationChanger.skipReload().withoutRefresh("/", true);

+1

+1

+1

+1

@mica16 Isn't the un variable used to unbind the event?

I created two plunkrs for testing, with and without the un variable to unbind the events and it seems that without it the event keeps firing over and over again

Yes indeed, the un variable sounds weird.
According to the original author, it would be considered as useful..but I'm not convainced that is the perfect solution.

I preferred to rewrite it, but I trashed the feature since I don't expect it any more.

So go with it if your unit test can ensure it works as you expect :)

You are right guys
My solution was

app.factory 'location', ['$location', '$route', '$rootScope', '$timeout', ($location, $route, $rootScope, $timeout) ->
  $location.skipReload = ->
    lastRoute = $route.current
    un = $rootScope.$on('$locationChangeSuccess', ->
      $route.current = lastRoute
    )
    $timeout((->
      un()
    ), 1000)
    $location

  $location
]

then in controller

location.skipReload().path('/new/path').replace()

+1

+1

+1

+1

another way to do it is to put it out of the current javascript "step" so that route is not aware of the change made to the window.location:

setTimeout(function() {
    window.location.hash = "";
    if (typeof (history.pushState) !== "undefined") {
        history.pushState("", document.title, location.pathname);
    }
}, 0);

+1

+1

+1

+1

What about using $anchorScroll? I was poking around the net and found out that there are some tools to do this. It works for me at least.

You should check it out: http://docs.angularjs.org/api/ng.$anchorScroll

+1

Is this a bad idea? I'll admit I haven't tested it extensively. I didn't like the idea of calling a separate method prior to setting the path. From a usage point of view at least this seems cleaner.

I extended the in built $location.path function, using sergey's technique.

App.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) {
        var original = $location.path;
        $location.path = function (path, reload) {
            if (reload === false) {
                var lastRoute = $route.current;
                var un = $rootScope.$on('$locationChangeSuccess', function () {
                    $route.current = lastRoute;
                    un();
                });
            }

            return original.apply($location, [path]);
        };
    }])

Usage:

$location.path('/my/path', false);

+1

@EvanWinstanley solutions works for me, thanks!

+1

@EvanWinstanley/sergey's fix looks nice and it doesn't reload the controller, but when changing the route, resolves are requested again.

My main use case for this would be manually changing the $location.path without reloading the resolves/controller so I'm able to do caching.

For example: I have a select that allows a user to choose between multiple applications, which also changes the $location.path for nicer urls. When a user selects an application, the results from the API call are cached. Even with the fix, it always does the resolve again, which is unnecessary since the results are already cached.

+1

@sergey-buturlakin solution works fine for me , it doesn't reload the controller but still reload reslove function and send request to server .

+1

@EvanWinstanley 's solution is not working for me. On the other hand @mica16's solution worked too well... (the back button would not refresh the page). I tried to write my own version without success :(.

+1

+1

+1

+1
Please support: $location.reload();

@jvmvik have you tried $route.current.reload() ?

7.1.1

no news about this after more than a year?

+1

+1

I've made a bit of improvement on Sergey's solution.
Now it supports, e.g., custom urls for tabs (navigating tabs) without reloading the whole page.

// Original: https://github.com/angular/angular.js/issues/1699#issuecomment-22511464
//
// Usage:
//
// (interception is needed for Back/Forward buttons to work)
//
// location.intercept($scope._url_pattern, function(matched) {
//   // can return false to abort interception
//   var type = matched[1]
//   if (!type) {
//     return;
//   }
//   $scope.safeApply(function() {
//     $scope.data_type = type; 
//     $scope.params.page = 1; 
//     $scope.get_data(); 
//   });
// });
// 
// anywhere in your controller: 
// location.skipReload().path(url);
//
// to replace in history stack:
// location.skipReload().path(url).replace();

app.factory('location', [
    '$location',
    '$route',
    '$rootScope',
    function ($location, $route, $rootScope) {
        var page_route = $route.current;

        $location.skipReload = function () {
            //var lastRoute = $route.current;
            var unbind = $rootScope.$on('$locationChangeSuccess', function () {
                $route.current = page_route;
                unbind();
            });
            return $location;
        };

        if ($location.intercept) {
            throw '$location.intercept is already defined';
        }

        $location.intercept = function(url_pattern, load_url) {

            function parse_path() {
                var match = $location.path().match(url_pattern)
                if (match) {
                    match.shift();
                    return match;
                }
            }

            var unbind = $rootScope.$on("$locationChangeSuccess", function() {
                var matched = parse_path();
                if (!matched || load_url(matched) === false) {
                  return unbind();
                }
                $route.current = page_route;
            });
        };

        return $location;
    }
]);

+1

+1

Another workaround to prevent route reloading when changing $location path, is to set the current route pathParams to match the new location path.

In my $routeProvider config, I have a route to /tasks as follows

when('/tasks/:section/:taskId?', {
      templateUrl: 'partials/tasks.html',
      controller: 'TaskCtrl',
      reloadOnSearch: false
      })

The above route has two pathParams section and taskId (taskId is optional) and reloadOnSearch is set to false.

To navigate from say /tasks/inbox to /tasks/inbox/122 without reloading the route, set the taskId pathParam before changing the $location path as follows

$scope.showTask = function(task) {
    // set taskId to prevent route reloading
    $route.current.pathParams ['taskId'] = task.id;
    $location.path('/tasks/'+$route.current.pathParams ['section']+'/'+task.id);
};

Note that the section pathParam does not need to be set since it was already set by the route.

When the route finds out that the pathParams extracted from the new $location path matches the current route pathParams (and reloadOnSearch is set to false) it will not reload the route instead a $routeUpdate event will be fired.

To navigate back from /tasks/inbox/122 to /tasks/inbox without reloading the route, delete the taskId pathParam before changing the $location path as follows

$scope.back = function () {
    // delete taskId to prevent route reloading
    delete $route.current.pathParams ['taskId'];
    $location.path('/tasks/'+$route.current.pathParams ['section']);
};

+1

+:100:

Did you read Derick Bailey article about the Backbone router http://lostechies.com/derickbailey/2011/08/28/dont-execute-a-backbone-js-route-handler-from-your-code/ ? Backbone provide this option and according to Derick you should always avoid triggering a reload (that seems the opposite of what angular's router seems to do here).
There are 48 "+1" on this post ... I do not think that all these people are doing something wrong. Having to store var lastRoute = $route.current; and restore it later on $locationChangeSuccess with $route.current = lastRoute; seems pretty hacky.
@IgorMinar do you have any thoughts?

OK I'm owning this topic for the next week so we can finally close this.

First things first, I don't think the route should reload when the #hash value changes.

website.com/#/my-route#top

website.com/#/my-route#bottom

So when top changes to bottom it should not reload, but if the my-route value changes then it should. We can all agree on this right?

Second, which is more important, we need an easy way of pointing out that the route path is not going to reload when changed, but this gets to be problematic if an earlier fragment of the route changes. So why don't we have a different notation for a route param by using two :: colons to point out that it acts like a wildcard value.

// this will reload the page when the id changes like normal
$routeProvider.register('/users/:id')

// this will reload the page when the id changes, but not the section (since two colons are used)
$routeProvider.register('/users/:id/::section')

What do you guys think?

This doesn't really help with the situation where you want to move from:

/users/new

to

/users/123

once the new user has been created and assigned an id, without reloading the user, since it is already there.

Because somehow one has to map the new user object to the current user object with id 123. This is rather application specific no?

For what it is worth Ember Router doesn't support changing URL without transitioning either:

http://stackoverflow.com/questions/19076834/change-url-without-transition

@matsko Can we just empower the engineer and let him or her decide if the route should trigger a change or not?
I am ok with good defaults.
Create a way so the engineer can changes the url without running the whole chain of reload events. Personally I am ok with adding an optional extra parameter with options (ex: {reload: false}) on ".path()", ".search()", etc.

@ChrisCinelli yes, but what happens when there are two params in a route definition? How can you prevent one from reloading while allowing the other to? The flag would be an all-or-nothing feature.

@matsko So on the implementation end, is it being considered that we would be able to cherry-pick which parameters cause reloads and which do not? And if so, do you already have an approach in mind or is that where we are hitting a snag?

@cgross cherry-pick yes (using the double colon). This is just an idea for now so nothing concrete. If this is inline with the plans for angular 2.0 then we may be able to use it or try to use something that 2.0 is planning on having or something that Dart already has (to keep the API consistent).

I think that'd be perfect, and would look good (at least IMO) as part of the route.

$routeProvider,when("/:categoryName/:id[reload=no]", { ... })

And then, in the controller

$scope.$on("$routeChangeSuccess", function () {
  if ($routeParams.id !== $scope.activeId) {
    // React to ID update without reload
  }
});

Yes something like that, but the double colon '::id' would be used instead of :id[reload=no].

Keep in mind that this is just an idea, but a lot of people here do want this feature.

I was thinking in terms of maybe eventually adding other options, but I guess there aren't really any other options that there could be. It's good too see this request is being taken seriously. Pretty URLs are kind of a big deal to a lot of us.

Matsko +1 for double colon, but I think it would also be useful to be able to pass options to path() as well for some edge cases mentioned above

+1 for double colon

@matsko asked me to weigh in on how we are handling this in Angular 2.0's router. I know that it doesn't help those of you who need this for ng1.x but I thought it might be interesting to you.

The new router has a navigate method which you can pass a _url_ and an optional _options_ object to. This is based on the history capabilities of the Backbone router, so it supports two options replace and trigger If you want to go from /users/new to /users/123 without causing a screen navigation, you simply specify trigger:false. If you also want it to replace the /users/new history entry with the new id-based url, you would also specify replace:true.

Updating routes from a _new_ route to an _id-based_ route without causing DOM transforms, etc. is a key use case for this API. Another common example is pagination. For example, if you've got a grid and you want the user to page through the data, you can use this to update the fragment with the current page info without causing navigation. I'm sure there are other uses, but these are the ones I've seen repeatedly in my consulting over the years.

Hope this info helps. Perhaps the 1.x router can adopt this technique?

@EisenbergEffect so to confirm. For 1.x, the method to do this without changing the path would look like so:

$location.path(value, { trigger : false })

And this would effectively replace the path without causing the new route + controller to become aware. This could work for now, but how do you feel about the double :: colon idea?

For everyone else here, would you rather have the URL changing (without reloading the route) be facilitated through the route or by the developer when the URL is changed?

There should be a central source of truth for location, like a service that
routes and browsers can both refer to and influence but remains the single
truth
On Jul 1, 2014 11:28 PM, "Matias Niemelä" [email protected] wrote:

@EisenbergEffect https://github.com/EisenbergEffect so to confirm. For
1.x, the method to do this without changing the path would look like so:

$location.path(value, { trigger : false })

And this would effectively replace the path without causing the new route

  • controller to become aware. This could work for now, but how do you feel
    about the double :: colon idea?

For everyone else here, would you rather have the URL changing (without
reloading the route) be facilitated through the route or by the developer
when the URL is changed?


Reply to this email directly or view it on GitHub
https://github.com/angular/angular.js/issues/1699#issuecomment-47741350.

Here's another idea... Allow two new methods on each route definition, say entering(transitionInfo) and leaving(transitionInfo) or something. If they exist then the leaving method would be called on the old route before leaving and then the entering() method would be called on the new route. The transitionInfo would be an object that looks like the following:

{
  newUrl: ...
  newRoute: ...
  oldUrl: ...
  oldRoute: ...
  cancelRouteChange: false,
  ignoreRouteChange: false
}

In this method we can check the old and new routes/urls and then if you want the URL to change but not rerun the route transition you set ignoreRouteChange to true. If you want to cancel the route change and roll back to the previous route you set cancelRouteChange to true.

This then gives the developer complete control over when route transitions occur, will work if the URL is modified directly in the address bar and also locates the functionality for deciding about a particular route change close to the definition of the individual routes that care about the change.

I believe there is something similar in AngularV2 router called activate(). @EisenbergEffect can you confirm?

What do you think?

@matsko Yes, that api could look like that. You might also want to consider implementing the replace option as well, since that is frequently needed too. In this particular case of /users/new to /users/123 I would think you would leverage both options since you don't actually want /users/new to remain in the back stack after the new user is created.

Regarding the ::, I'm not sure that's entirely useful. The reason is that, in the users example above, it's not the url pattern that matters, but the specific application context. For example, if you transition from customers to users/123 then you want to notify. Also, if you abandon users/new and go to users/456 then you also want to notify. It's only when you are at users/new and have finalized the creation of the new user that you want to simply update the fragment to represent the newly created user's id without causing any transition. This is highly dependent on what the user is actually doing within the context of that user screen at that point in time.

@petebacondarwin One issue with that idea is that the knowledge of allowance of the route notification isn't something that is globally available along with the route definitions. Rather, it is embedded within the particular user controller since that controller is the piece of code that knows whether or not a new user is actually being created at this moment vs. simply navigating away from the new user screen to a pre-existing user.

To me the way to go is giving control to the caller that want to change a route. So I am definitely in favor with two options replace and trigger.

@EisenbergEffect - you are right. In general, this information is not immediately available, although I would imagine implementing this in the following way:

Have a service called currentUser, which contains the user we are currently working with. I like to do this generally as I am not comfortable with overloading my controllers with code. If this feels too specific then you could have currentEntity, which is the current "thing" that you are editing/viewing.

Then the route enter() method would be able to have this injected and decide whether navigating to this route would actually change the current entity. If not then we don't bother to run the transition, just update the URL instead.

I appreciate that this could become a bit unwieldy in a big app but I thought I would throw it out there as an option.

What are the arguments against giving the caller (i.e. the one requesting the route/url change) the control over whether the route transition actually occurs? I remember that @IgorMinar was concerned that this broke the constraint that one state of the application directly mapped to one URL and that allowing the URL to change without re-running the route would potentially put the application in a state that was different depending upon the way in which you arrived there.

@petebacondarwin That could work for this specific scenario, but it gets a bit trickier to handle the pagination example I mentioned above, which would want to not trigger, and not replace, while the new user scenario wants to not trigger but replace.

To address the state issue is a bit more complicated. The reason is that we have two URLs that represent the same controller state....but the difference is in the model. So, we actually have two different states in reality....but from a UX perspective, we don't want to jolt the user in making the transition. From a perf perspective, we don't want to needlessly throw away DOM and recreate everything. So, in the case of the new user => saved user transition we really do want to avoid triggering and also replace the fragment so that the back button does _not_ take you back to the new user screen. Internally, the router or location mechanism would just need to track what the current fragment is. (I'm making a bit of an assumption there since I don't know how ng1.x works in this regard. With 2.x we make sure to carefully track the current/previous fragment regardless of how it was changed.) In the pagination scenario, we don't want to trigger because we want a nice UX for paging through the data and don't want needless DOM disposal. But we DO want to have the previous fragment kept so that user can use the back button to move backwards through the pages of data.

Notice that in both of these cases, it's not just about the url and the state of the app but also about how we transition from the current state into that new state. None of this matters if you deep link directly to the new view. It does matter when you are moving from one to another within the context of these very specific cases.

+1

+1

+1

+1
And also feature to change URL without changing rout

+1 - nice to see I'm not alone wanting this!

+1, have not yet added(

+1

+1

+1

+1

+1

+1

+1

:+1:

You too @basarat? :smile:

@johnnyreilly just have a service where you inject $route and $location and you can have this function:

/** navigate */
    public navigate(url: string, reload = true) {
        this.$location.url(url);

        // Don't do a controller reload on this navigation. Note: *you* need to be careful not to actually *need* a controller reload
        if (!reload) {
            var lastRoute = this.$route.current;
            var unsub = this.$rootScope.$on('$locationChangeSuccess', () => {
                this.$route.current = lastRoute; // fake this to make the route logic think nothing changed
                unsub();
            });
        }
    }

Nice @basarat!

+1

+1

One thing that I'm noticing with the current solutions is that my router resolve block is still getting executed. Anyone else notice this? It was easy enough to pass something in the $route.current object, but that seems hacky. Also, it seems like this issue will never be fixed? :cry:

+1

+1

For those of you who don't want to have to scroll back... as far as I can tell, this is what we have so far (sorry coffeescript). Unfortunately, this isn't working 100% for me. I'm still getting my resolve executed and the back/forward buttons seem broken now.

#
# Special $location.path function that allows you to change the url, but not
# send things through the router.
# @see https://github.com/angular/angular.js/issues/1699
#
@app.run(['$rootScope', '$location', '$route', '$timeout', ($rootScope, $location, $route, $timeout) ->

    originalPath = $location.path

    $location.path = (url, reload=true) ->
        if ! reload
            lastRoute = $route.current
            if lastRoute
                unsub = $rootScope.$on('$locationChangeSuccess', ->
                    $route.current = lastRoute # fake this to make the route logic think nothing changed
                    $route.current.ignore = true
                    unsub()
                )
                # make sure to clean up and unregister the unsub function
                $timeout( ->
                    unsub()
                , 500)

        originalPath.apply($location, [url])

])

@lookfirst you are calling unsub twice. Prefer : https://github.com/angular/angular.js/issues/1699#issuecomment-54931571

@basarat shrug... I added that because I was finding that unsub() wasn't always being called in your preferred example. Maybe the best would be to add a guard in the unsub function.

unsubbed = false
unsub = ... ->
    if ! unsubbed
        ...
        unsubbed = true
        unsub()

$timeout(unsub, 500)

Regardless, this solution is a total mess. It breaks all sorts of things. We really need an angular approved fix.

because I was finding that unsub() wasn't always being called in your preferred example

@lookfirst you are using location.path I am using location.url. That might be impacting it.

Just nitpicking (I like to be helpful) : instead of originalPath.apply($location, [url]) you can do originalPath.call($location, url) (you might already know this)

Interesting... location.url works a lot better (the back button works again!), but seems to be causing another issue on my end. I'm going to have to dig into this more when I haven't been coding for 10 hours straight. Thanks for the pointers and discussion, really!

+1

+1

+1

@kuchumovn Your solution is great. One problem I got is that $routeParams does not change after the path changed. I solved it by replacing the value of $routeParams inside the event listeners. I'm not sure if this is the correct way, but it works for me.

app.factory('Location', [
    '$location',
    '$route',
    '$rootScope',
    '$routeParams',
    function ($location, $route, $rootScope, $routeParams) {
        var page_route = $route.current;

        $location.skipReload = function () {
            //var lastRoute = $route.current;
            var unbind = $rootScope.$on('$locationChangeSuccess', function () {
                angular.copy($route.current.params, $routeParams);
                $route.current = page_route;
                unbind();
            });
            return $location;
        };

        if ($location.intercept) {
            throw '$location.intercept is already defined';
        }

        $location.intercept = function(url_pattern, load_url) {

            function parse_path() {
                var match = $location.path().match(url_pattern);
                if (match) {
                    match.shift();
                    return match;
                }
            }

            var unbind = $rootScope.$on("$locationChangeSuccess", function() {
                var matched = parse_path();
                if (!matched || load_url(matched) === false) {
                  return unbind();
                }
                angular.copy($route.current.params, $routeParams);
                $route.current = page_route;
            });
        };

        return $location;
    }
]);

+1

+1

I talked to @IgorMinar in person yesterday... he says that the future is the router in Angular 2.0, which will get back ported to 1.3 soon. Something to consider...

why want you just merge ui-router in angular?
uui router seems to be pretty solving anything can be needed.

agree with @elennaro. although ui-router takes some creativity and study to make it work, it does the trick in every scenario i've come across. curious what are the downsides of adopting ui-router and throwing google resources at it?

It seems that none of the solution will not execute the resolve, any ideas?

+1

+1

+1 for $location.path(value, { trigger : false })
Any progress on this?

+1

+1

+1 what about a completely seperate method? leaving path() as is, and just adding a $location.hash method that simply changes the browser's url without controller reload. this allows us to control back-navigation of the browser and is a valid use-case scenario in rich applications.

+1

Currently using the snippet here in this page: http://joelsaupe.com/programming/angularjs-change-path-without-reloading/

+1

+1

+1

A workaround I found was to add a child state with an absolute url.

$stateProvider.state('state.child', {
    url: '^/some-other-url'
});

Then simply do $state.go('.child') and that should change the url to the one you want without reloading and keep everyone happy. Note that caret, which indicates that the url won't inherit from the parent.. took me awhile to figure this out.

+1

+1

+1

+1

+1

+1

+1

@btford how is this dealt with (if at all) in the componentRouter?

Thanks @balsarori for a simple, working solution.

Elaborated on top of it to allow updating $route path parameters, without reloading the controller.

It uses console.log instead of $log as we can simply strip all logs for production builds with uglifyjs.

Edit: Fix when using in a project:

/**
 * parameter handling
 */

'use strict';

angular.module('route-params', [
  'ng',
  'ngRoute',
])
.service('routeParams', function(
  $route,
  $location
) {
  var R = {};

  /**
   * @ngdoc method
   * @name routeParams#replace
   *
   * @description
   * Replace route params, including path params, without reloading controller.
   *
   * Causes `$route` service to update the current URL, replacing
   * current route parameters with those specified in `params`.
   *
   * Provided property names that match the route's path segment
   * definitions will be interpolated into the location's path, while
   * remaining properties will be treated as query params.
   *
   * If `options.reload` is truthy, the current controller is reloaded.
   *
   * If `options.history` is truthy, the browser history is updated.
   * If `options.history` is undefined, the browser history is only updated
   * when changing value of any existing `$route` parameter,
   * ie. not when adding a previously undefined `$route` parameter.
   *
   * @param {!Object<string, string>} params mapping of URL parameter names to values
   * @param {!Object<string, boolean>} options `history` and `reload` options.
   *
   * @returns {Object} self
   */
  R.replace = function(params, options) {
    var key, value, updateRequired;
    options = options || {};

    // Convert params to strings, and check if they differ from current route params.
    // If `options.history` is undefined, and passed params update any current route params, then set it to true.
    var currentParams = $route.current.params;

    console.log('route: params: replace', {last: angular.copy(currentParams), next: params,
      route: $route.current, options: options});

    if (!$route.current.$$route) {
      // Can't change location.
      return;
    }
    var currentPath = $route.current.$$route.originalPath;
    var history = options.history;

    for (key in params) {
      // jshint eqnull: true
      value = params[key] = (params[key] == null ? undefined : params[key] + '');

      if (value !== currentParams[key]) {
        updateRequired = true;

        console.log('route: params: replace: ' + (currentPath.search(':\\b' + key + '\\b') ? 'path.' : 'search.') +
          key + ' = ' + (key in $route.current.params ? $route.current.params[key] + ' -> ' : '') + params[key]);

        if (history === undefined && key in currentParams) {
          console.log('route: params: replace: update history: ' + key + ' = ' + currentParams[key] + ' -> ' + value);
          history = true;
        }
      }
    }

    if (updateRequired) {
      // If `options.reload` is falsey, then set current route `reloadOnSearch` to false,
      // and make passed path params equal with current route path params,
      // so that `$route` treats the change as update only, and does not broadcast `$routeChangeStart` event.
      //
      // See https://github.com/angular/angular.js/issues/1699#issuecomment-45048054
      // and https://github.com/angular/angular.js/tree/v1.3.x/src/ngRoute/route.js#L484
      // and https://github.com/angular/angular.js/tree/v1.3.x/src/ngRoute/route.js#L539
      if (!options.reload) {
        // Set current route `reloadOnSearch` to false.
        $route.current.$$route.reloadOnSearch = false;

        // Add any passed path params to current route path params, and convert them to strings.
        // Path params are detected by searching for respective name group `:key[*?]` in current route path.
        for (key in params) {
          if (currentPath.search(':\\b' + key + '\\b') !== -1) {
            $route.current.pathParams[key] = params[key];
          }
        }

        // Add any current route path params to passed params, if not set there already.
        for (key in $route.current.pathParams) {
          if (!(key in params)) {
            params[key] = $route.current.pathParams[key];
          }
        }

        // TODO: push state if `options.history` is truthy.
      }
      // If `options.history` is falsey, and controller is reloaded,
      // then make `$location` replace current history state, instead of pushing a new one.
      else if (!history) {
        $location.replace();
      }

      $route.updateParams(params);

      // Update current route params, so the change is reflected immediatelly,
      // and nearby replace() call work correctly.
      for (key in params) {
        $route.current.params[key] = params[key];
      }
    }

    return R;
  };

  return R;
});

What's the current status of this issue? It will be solved in the new router? On-time for 1.4 release? Soonish?

I'm having the same problem but with query string parameters. I don't want a reload when changing them. But I can't disable reloadOnSearch because I need to we aware if the user changes the query string manually! :)

Thanks!

@marcalj fwiw, we hacked this solution with a service that kept the url in sync with the address bar so that we could disable reloadOnSearch and grab query parameters. also, users can navigate the url directly via bookmark or link.

With ng-router or ui-router? Can you provide me a link with the code or something?

Maybe this could be solved using new router, I can wait for it since my project it's not finished yet.

Thanks!

@marcalj our example is a bit convoluted and possibly outdated but look at https://github.com/irthos/mngr/blob/master/app.js and check line 10. it uses our api.loadState method which identifies params in the url to construct the proper state.

NgSilent was the solution for me.
Look at https://github.com/garakh/ngSilent
Tested with ngRoute 1.3.15

I used both @sergey-buturlakin and @kuchumovn solution, and I continually run into the same issue.

On the page in question (pagex) I use location.skipReload and it works great - I can update the path over and over without a route change. If I then use an actual $location.path to change the route away from pagex to a new controller, it works, but if I hit the back button to return to pagex, and then try to navigate to another page, I can no longer change my route anymore without refreshing the browser. Is there a way around this?

+1

+1. I like double colon approach that Matias suggested

+1

It is common use case for single page app to not reload page on path change,
very strange why it is not yet embedded to angular.
made plugin based on @EvanWinstanley solution:
https://github.com/anglibs/angular-location-update

Let's revisit this in 1.5. There is a related PR at #5860

I need the same function for query string update without triggering route controller! :)

Something like: $location.searchSilent('sf', SearchFilter.getForRouteParams());

$location.path(href);
scope.$apply();
is the solution!!
we need inform to angular the change with $apply()

This thread has not been open for 2 years because angular hadn't been notified -- we want to change a part of the route without invoking everything.

@petebacondarwin it would be good to have not only location path update without reload, but also path params update without reload ... like implemented above https://github.com/angular/angular.js/issues/1699#issuecomment-96126887

The solution of @jicasada worked perfectly fine for me! Thx!

@jicasada does this work with ui-router? I have raised a bug with ui-router where reloadOnSearch:false does not work if parent and child state share the same URL. If I transition to child state with updated request param it will call the parent controller twice.

+1

+1

I confirm that ngSilent is doing perfectly the job. Even so an official solution would be highly appreciated.

The solution is essentially to add an extra parameter to $location.path().

app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) {
    var original = $location.path;
    $location.path = function (path, reload) {
        if (reload === false) {
            var lastRoute = $route.current;
            var un = $rootScope.$on('$locationChangeSuccess', function () {
                $route.current = lastRoute;
                un();
            });
        }
        return original.apply($location, [path]);
    };
}]);

And then just use like this

$location.path('/path/that/you/need', false);

I should just be able to do $location.replace() just like the window.location.replace function. This is just silly because every Create in a CRUD operation will always have this issue.

(y) +1

+1

I advise you use "search param" like this:
/thing?id=1234
Do not config the param in the router provider,it won't reload view and controller.
If you want to get id
$location.search().id //1234
@cgross

Demo http://codepen.io/dolymood/details/jrEQBx/
Use decorator to decorator ngViewDirective

+1

+1

+1

+1

+1

+1

+!

Enough of the +1s already :-)
We get the message. We will look at this before Xmas!

Closed by #15002.

Was this page helpful?
0 / 5 - 0 ratings