39

My single page application loads a home page and I want to display a series of ideas. Each of the ideas is displayed in an animated flash container, with animations displayed to cycle between the ideas.

Ideas are loaded using $http:

$scope.flash = new FlashInterface scope:$scope,location:$location

$http.get("/competition.json")
  .success (data) ->
    $scope.flash._init data

However, to benefit from history navigation and UX I wish to update the address bar to display the correct url for each idea using $location:

$location.path "/i/#{idea.code}"
$scope.$apply()

I am calling $apply here because this event comes from outwith the AngularJS context ie Flash. I would like for the current controller/view to remain and for the view to not reload. This is very bad because reloading the view results in the whole flash object being thrown away and the preloader cycle beginning again.

I've tried listening for $routeChangeStart to do a preventDefault:

$scope.$on "$routeChangeStart", (ev,next,current) ->
  ev.preventDefault()
$scope.$on "$routeChangeSuccess", (ev,current) ->
  ev.preventDefault()

but to no avail. The whole thing would be hunky dory if I could figure out a way of overriding the view reload when I change the $location.path.

I'm still very much feeling my way around AngularJS so I'd be glad of any pointers on how to structure the app to achieve my goal!

georgeawg
  • 48,608
  • 13
  • 72
  • 95
chrisbateskeegan
  • 1,013
  • 1
  • 11
  • 14
  • I'm late but I just encountered a similar issue. Is there a reason you couldn't put the `ideas` outside the `ng-view` directive and give them their own controller, which interacts with the controllers inside `ng-view` via scope inheritance? I'm mostly asking because this is how I solved my own problem and am wondering if it has any shortcomings, or if I should switch to the accepted answer. – jclancy Jul 11 '13 at 02:42
  • 1
    If I were solving this problem again today, I'd be very tempted to use ui-router with its hierarchical view capability. I'm not sure that would solve the specific URL problem I had here though, would need more research been a while...! – chrisbateskeegan Jul 12 '13 at 06:14
  • OK, thanks for pointing me in that direction, and thanks for responding to such a late comment! – jclancy Jul 12 '13 at 06:16

7 Answers7

94

Instead of updating the path, just update query param with a page number.

set your route to ignore query param changes:

....
$routeProvider.when('/foo', {..., reloadOnSearch: false})
....

and in your app update $location with:

...
$location.search('page', pageNumber);
...
T J
  • 42,762
  • 13
  • 83
  • 138
Igor Minar
  • 9,574
  • 3
  • 22
  • 13
  • 10
    Thanks for the tip, Igor. I'd spotted and discounted this approached previously because I'd prefer URL's like '/idea/Acas' rather than '/foo?idea=Acas'. It does, however, have the virtue of working and in the world of pragmatists working trumps perfect! – chrisbateskeegan Sep 19 '12 at 08:46
  • 5
    Perfect, thanks a lot of this tip. Also I want to notice that if you want to detect the parameter changing you can do it with : $scope.$on("$routeUpdate", function(){ // The search param has changed }) – Unitech Dec 26 '12 at 08:06
  • This is the answer! All these other answer I find deal with messing around with $route and $state, this is straight forward and gets the job done! – Leon Gaban Feb 23 '16 at 15:53
31

From this blog post:

by default all location changes go through the routing process, which updates the angular view.

There’s a simple way to short-circuit this, however. Angular watches for a location change (whether it’s accomplished through typing in the location bar, clicking a link or setting the location through $location.path()). When it senses this change, it broadcasts an event, $locationChangeSuccess, and begins the routing process. What we do is capture the event and reset the route to what it was previously.

function MyCtrl($route, $scope) {
    var lastRoute = $route.current;
    $scope.$on('$locationChangeSuccess', function(event) {
        $route.current = lastRoute;
    });
}
Mark Bell
  • 28,985
  • 26
  • 118
  • 145
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • Thanks for this, it sounds exactly like the solution I was looking for. I've gone through the blog article and had a good play. Unfortunately it doesn't seem to work. I tried placing the code in `$routeChangeStart` to see if that would make a difference but to no avail. Is there perhaps some witchcraft that I'm missing? – chrisbateskeegan Sep 16 '12 at 16:23
  • Another idea: try setting reloadOnSearch=false, http://docs.angularjs.org/api/ng.$routeProvider See also https://github.com/angular/angular.js/pull/1151 – Mark Rajcok Sep 17 '12 at 13:53
  • 2
    This is a terrible, terrible hack... but it works. And until Angular provides some way to decouple the location update event from a reload (unfortunately preventing default prevents the url change outright) it's what we're using. And yes, fwiw, reloadOnSearch=false works great if what you need to change is limited to the search params. Our issue was with the hash for bookmarkable tabs. – zapnap Apr 17 '13 at 19:13
  • @mark Thanks for this, I was working on an irrelevant project and this made my program work. Thanks! – alex wilhelm Aug 21 '13 at 03:18
  • 4
    It looks like this applies to all location changes. So if there are specific links that you want to go through angular routing, they will be broken. Also, the back button will no longer work. – Ryan Shea Sep 16 '13 at 17:43
  • If you only want to do this as a one off call the remove handler in the event eg var removeNoReloadFn = $scope.$on('$locationChangeSuccess', function (e) { ... removeNoReloadFn(); }); – Chris Herring Nov 18 '13 at 02:54
  • Just implemented this and works smoothly. As hacky and terrible as it might sound, the implementation was short and didn't break anything (as I feared). – Apoorv Parijat Nov 28 '13 at 00:13
  • I was stuck in this problem for hours! Thank you so much! – Rahul Desai Jan 13 '16 at 06:06
3

My solution was to use the $routeChangeStart because that gives you the "next" and "last" routes, you can compare them without the need of an extra variable like on $locationChangeSuccess.

The benefit is being able to access the "params" property on both "next" and "last" routes like next.params.yourproperty when you are using the "/property/value" URL style and of course use $location.url or $location.path to change the URL instead of $location.search() that depends on "?property=value" URL style.

In my case I used it not only for that but also to prevent the route to change is the controller did not change:

$scope.$on('$routeChangeStart',function(e,next,last){
  if(next.$$route.controller === last.$$route.controller){
    e.preventDefault();
    $route.current = last.$$route;
    //do whatever you want in here!
  }
});

Personally I feel like AngularJS should provide a way to control it, right now they assume that whenever you change the browser's location you want to change the route.

preams
  • 81
  • 3
  • 1
    This does not work for me when trying to use this within a controller in AngularJS 1.2.6. I think it is related to $routeChangeStart being a broadcast which is not preventable: http://stackoverflow.com/a/14895801/959140 – Robin Andersson Feb 20 '14 at 17:43
1

You should be loading $location via Dependency Injection and using the following:

$scope.apply(function () {
    $location.path("yourPath");
}

Keep in mind that you should not use hashtags(#) while using $location.path. This is for compability for HTML5 mode.

Umur Kontacı
  • 35,403
  • 8
  • 73
  • 96
  • 1
    Thanks for the tip. I _think_ I'm loading $location correctly `IndexController = ($scope,$http,$location,$route) ->` and I've updated the apply code to be as you suggest. I'm not exactly sure of what difference that makes though, could you elaborate a little? – chrisbateskeegan Sep 16 '12 at 16:14
0

The $locationChangeSuccess event is a bit of a brute force approach, but I found that checking the path allows us to avoid page reloads when the route path template is unchanged, but reloads the page when switching to a different route template:

var lastRoute = $route.current;
$scope.$on('$locationChangeSuccess', function (event) {
    if (lastRoute.$$route.originalPath === $route.current.$$route.originalPath) {
        $route.current = lastRoute;
    }
});

Adding that code to a particular controller makes the reloading more intelligent.

Edit: While this makes it a bit easier, I ultimately didn't like the complexity of the code I was writing to keep friendly looking URL's. In the end, I just switched to a search parameter and angular handles it much better.

Jason Young
  • 3,683
  • 4
  • 32
  • 39
0

I needed to do this but after fussing around trying to get the $locationChange~ events to get it to work I learned that you can actually do this on the route using resolve.

$routeProvider.when(
    '/page',
    {
        templateUrl : 'partial.html',
        controller : 'PageCtrl',
        resolve : {
            load : ['$q', function($q) {
                var defer = $q.defer();

                if (/*you only changed the idea thingo*/)
                    //dont reload the view
                    defer.reject('');
                //otherwise, load the view
                else
                    defer.resolve();

                return defer.promise;
            }]
        }
    }
);
Hashbrown
  • 12,091
  • 8
  • 72
  • 95
0

With AngularJS V1.7.1, $route adds support for the reloadOnUrl configuration option.

If route /foo/:id has reloadOnUrl = false set, then moving from /foo/id1 to /foo/id2 only broadcasts a $routeUpdate event, and does not reload the view and re-instantiate the controller.

georgeawg
  • 48,608
  • 13
  • 72
  • 95