196

It's been asked before, and from the answers it doesn't look good. I'd like to ask with this sample code in consideration...

My app loads the current item in the service that provides it. There are several controllers that manipulate the item data without the item being reloaded.

My controllers will reload the item if it's not set yet, otherwise, it will use the currently loaded item from the service, between controllers.

Problem: I would like to use different paths for each controller without reloading Item.html.

1) Is that possible?

2) If that is not possible, is there a better approach to having a path per controller vs what I came up with here?

app.js

var app = angular.module('myModule', []).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/items', {templateUrl: 'partials/items.html',   controller: ItemsCtrl}).
      when('/items/:itemId/foo', {templateUrl: 'partials/item.html', controller: ItemFooCtrl}).
      when('/items/:itemId/bar', {templateUrl: 'partials/item.html', controller: ItemBarCtrl}).
      otherwise({redirectTo: '/items'});
    }]);

Item.html

<!-- Menu -->
<a id="fooTab" my-active-directive="view.name" href="#/item/{{item.id}}/foo">Foo</a>
<a id="barTab" my-active-directive="view.name" href="#/item/{{item.id}}/bar">Bar</a>
<!-- Content -->
<div class="content" ng-include="" src="view.template"></div>

controller.js

// Helper function to load $scope.item if refresh or directly linked
function itemCtrlInit($scope, $routeParams, MyService) {
  $scope.item = MyService.currentItem;
  if (!$scope.item) {
    MyService.currentItem = MyService.get({itemId: $routeParams.itemId});
    $scope.item = MyService.currentItem;
  }
}
function itemFooCtrl($scope, $routeParams, MyService) {
  $scope.view = {name: 'foo', template: 'partials/itemFoo.html'};
  itemCtrlInit($scope, $routeParams, MyService);
}
function itemBarCtrl($scope, $routeParams, MyService) {
  $scope.view = {name: 'bar', template: 'partials/itemBar.html'};
  itemCtrlInit($scope, $routeParams, MyService);
}

Resolution.

Status: Using search query as recommended in the accepted answer allowed me to provide different urls without reloading the main controller.

app.js

var app = angular.module('myModule', []).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/items', {templateUrl: 'partials/items.html',   controller: ItemsCtrl}).
      when('/item/:itemId/', {templateUrl: 'partials/item.html', controller: ItemCtrl, reloadOnSearch: false}).
      otherwise({redirectTo: '/items'});
    }]);

Item.html

<!-- Menu -->
<dd id="fooTab" item-tab="view.name" ng-click="view = views.foo;"><a href="#/item/{{item.id}}/?view=foo">Foo</a></dd>
<dd id="barTab" item-tab="view.name" ng-click="view = views.bar;"><a href="#/item/{{item.id}}/?view=foo">Bar</a></dd>

<!-- Content -->
<div class="content" ng-include="" src="view.template"></div>

controller.js

function ItemCtrl($scope, $routeParams, Appts) {
  $scope.views = {
    foo: {name: 'foo', template: 'partials/itemFoo.html'},
    bar: {name: 'bar', template: 'partials/itemBar.html'},
  }
  $scope.view = $scope.views[$routeParams.view];
}

directives.js

app.directive('itemTab', function(){
  return function(scope, elem, attrs) {
    scope.$watch(attrs.itemTab, function(val) {
      if (val+'Tab' == attrs.id) {
        elem.addClass('active');
      } else {
        elem.removeClass('active');
      }
    });
  }
});

The content inside my partials are wrapped with ng-controller=...

Coder1
  • 13,139
  • 15
  • 59
  • 89
  • found this answer: http://stackoverflow.com/a/18551525/632088 - it uses reloadOnSearch: false, but also it checks for updates to the url in the searchbar (e.g. if user clicks back button & url changes) – Rusty Rob Mar 24 '15 at 07:41

12 Answers12

121

If you don't have to use URLs like #/item/{{item.id}}/foo and #/item/{{item.id}}/bar but #/item/{{item.id}}/?foo and #/item/{{item.id}}/?bar instead, you can set up your route for /item/{{item.id}}/ to have reloadOnSearch set to false (https://docs.angularjs.org/api/ngRoute/provider/$routeProvider). That tells AngularJS to not reload the view if the search part of the url changes.

Gonzalo.-
  • 12,512
  • 5
  • 50
  • 82
Anders Ekdahl
  • 22,685
  • 4
  • 70
  • 59
  • Thanks for that. I'm trying to figure out where I would then change the controller. – Coder1 Feb 20 '13 at 07:44
  • Got it. Added a controller param to my view array, then added `ng-controller="view.controller"` to the `ng-include` directive. – Coder1 Feb 20 '13 at 08:42
  • You would create a watch on $location.search() in itemCtrlInit and depending on the search parameter, update $scope.view.template. And then those template could be wrapped with something like `
    ... your template content...
    `. EDIT: Nice to see you solved, it would be great if you updated your question with how you solved, perhaps with a jsFiddle.
    – Anders Ekdahl Feb 20 '13 at 08:42
  • I'll update it when I'm 100% there. I'm having some quirks with scope in the child controllers and ng-click. If I load directly from foo, ng-click executes within the foo scope. I then click on bar, and ng-clicks in that template get executed in the parent scope. – Coder1 Feb 20 '13 at 09:24
  • If you can isolate your issue in a jsFiddle/plnkr/etc, I'm sure we can help you with it. – Anders Ekdahl Feb 20 '13 at 09:46
  • I wasn't sure how to jsFiddle multiple partials. I left my progress above, which does address the initial question of being able to use urls without reloading the root controller. I'm going to have to start back on the scope issues later today. You definitely got me on the right track, thanks! – Coder1 Feb 20 '13 at 10:29
  • Scope issues were un-related. This is resolved, and I updated the solution. Thanks again. – Coder1 Feb 20 '13 at 11:47
  • 1
    Should be noted that you don't actually need to reformat your URLS. This may have been the case when the reponse was posted, but the documentation (http://docs.angularjs.org/api/ngRoute.$routeProvider) now says: _[reloadOnSearch=true] - {boolean=} - reload route when only $location.search() or $location.hash() changes._ – Rhys van der Waerden Feb 11 '14 at 01:35
  • For anyone interested, same parameter exists for UI router - you can set on state "reloadOnSearch: false" – Illidan Aug 08 '15 at 18:41
  • Wow. Finally realize why my app was so slow. I was changing the search params every time the user interacted. Didn't know it was reloading everything. – Chris Chevalier Nov 18 '16 at 17:53
94

If you need to change the path, add this after your .config in your app file. Then you can do $location.path('/sampleurl', false); to prevent reloading

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]);
    };
}])

Credit goes to https://www.consolelog.io/angularjs-change-path-without-reloading for the most elegant solution I've found.

ThisClark
  • 14,352
  • 10
  • 69
  • 100
Vigrond
  • 8,148
  • 4
  • 28
  • 46
  • 6
    Great solution. Is there anyway to get the browser's back button to "honor" this? – Mark B Jul 30 '14 at 21:52
  • 6
    Be aware that this solution keeps the old route, and if you ask for $route.current, you will get the previous path, not the current. Otherwise it is a great little hack. – jornare Aug 08 '14 at 13:37
  • 4
    Just wanted to say that the original solution was acutally posted by "EvanWinstanley" here: https://github.com/angular/angular.js/issues/1699#issuecomment-34841248 – chipit24 Sep 17 '14 at 04:25
  • 2
    I've been using this solution for a while and just noticed that on some path changes, reload registers as false even when a true value is passed in. Has anyone else had any issues like this? – Will Hitchcock Apr 13 '15 at 17:53
  • 1
    This solution still makes the view reload. – Lajos Mészáros Aug 20 '15 at 10:26
  • 2
    I had to add `$timeout(un, 200);` before return statement as sometimes `$locationChangeSuccess` did not fire at all after `apply` (and route was not changed when really needed). My solution clears things up after invocation of path(..., false) – snowindy Oct 11 '15 at 10:32
  • tried this solution and found that some $scope variables cannot use after route change.so need to make all objects and variables again since it returns false when try to use $scope.variables after change a sub page – Hashan Apr 26 '16 at 03:55
  • 1
    Is there a way to implement the same using ui-router ? – Kunal Dethe Jun 02 '16 at 17:57
  • Check out my solution, added some stuff yours didn't. Feel free to take what's missing and comment on mine and I'll delete my answer. – Oded Niv Sep 27 '16 at 10:05
  • Works great at `AngularJS 1.5` – Thiago Pereira Dec 06 '18 at 13:17
  • Hey I just copy-paste but I get TypeError: $rootScope.$on is not a function. What am I missing? Which is the minimal AngularJS version to use this approach? – Rober May 28 '19 at 08:16
18

why not just put the ng-controller one level higher,

<body ng-controller="ProjectController">
    <div ng-view><div>

And don't set controller in the route,

.when('/', { templateUrl: "abc.html" })

it works for me.

windmaomao
  • 7,120
  • 2
  • 32
  • 36
  • great tip, it works perfectly for my scenario too! thank you very much! :) – daveoncode Mar 17 '15 at 11:23
  • 4
    This is a great "the russians used pencils" answer, works for me :) – inolasco Mar 19 '15 at 21:53
  • 1
    I spoke too soon. This workaround has the issue that if you need to load values from $routeParams in the controller, they will not load, default to {} – inolasco Mar 19 '15 at 23:49
  • @inolasco: works if you read your $routeParams in $routeChangeSuccess handler. More often than not, this is probably what you want anyway. reading just in the controller would only get you the values for the URL that was originally loaded. – plong0 May 19 '15 at 21:14
  • @plong0 Good point, should work. In my case though, I only needed to get the URL values when the controller loads upon page load, to initialize certain values. I ended up using the solution proposed by vigrond using $locationChangeSuccess, but yours is another valid option as well. – inolasco May 20 '15 at 22:11
13

For those who need path() change without controllers reload - Here is plugin: https://github.com/anglibs/angular-location-update

Usage:

$location.update_path('/notes/1');

Based on https://stackoverflow.com/a/24102139/1751321

P.S. This solution https://stackoverflow.com/a/24102139/1751321 contains bug after path(, false) called - it will break browser navigation back/forward until path(, true) called

Community
  • 1
  • 1
Daniel Garmoshka
  • 5,849
  • 39
  • 40
10

Though this post is old and has had an answer accepted, using reloadOnSeach=false does not solve the problem for those of us who need to change actual path and not just the params. Here's a simple solution to consider:

Use ng-include instead of ng-view and assign your controller in the template.

<!-- In your index.html - instead of using ng-view -->
<div ng-include="templateUrl"></div>

<!-- In your template specified by app.config -->
<div ng-controller="MyController">{{variableInMyController}}</div>

//in config
$routeProvider
  .when('/my/page/route/:id', { 
    templateUrl: 'myPage.html', 
  })

//in top level controller with $route injected
$scope.templateUrl = ''

$scope.$on('$routeChangeSuccess',function(){
  $scope.templateUrl = $route.current.templateUrl;
})

//in controller that doesn't reload
$scope.$on('$routeChangeSuccess',function(){
  //update your scope based on new $routeParams
})

Only down-side is that you cannot use resolve attribute, but that's pretty easy to get around. Also you have to manage the state of the controller, like logic based on $routeParams as the route changes within the controller as the corresponding url changes.

Here's an example: http://plnkr.co/edit/WtAOm59CFcjafMmxBVOP?p=preview

Lukus
  • 1,038
  • 1
  • 11
  • 11
  • 1
    Not sure that the back button would behave correctly in plnkr... As I mentioned, you have to manage some things on your own as the route changes.. it's not the most code-worthy solution, but it does work – Lukus Sep 25 '13 at 21:15
2

I use this solution

angular.module('reload-service.module', [])
.factory('reloadService', function($route,$timeout, $location) {
  return {
     preventReload: function($scope, navigateCallback) {
        var lastRoute = $route.current;

        $scope.$on('$locationChangeSuccess', function() {
           if (lastRoute.$$route.templateUrl === $route.current.$$route.templateUrl) {
              var routeParams = angular.copy($route.current.params);
              $route.current = lastRoute;
              navigateCallback(routeParams);
           }
        });
     }
  };
})

//usage
.controller('noReloadController', function($scope, $routeParams, reloadService) {
     $scope.routeParams = $routeParams;

     reloadService.preventReload($scope, function(newParams) {
        $scope.routeParams = newParams;
     });
});

This approach preserves back button functionality, and you always have the current routeParams in the template, unlike some other approaches I've seen.

parliament
  • 21,544
  • 38
  • 148
  • 238
1

Answers above, including the GitHub one, had some issues for my scenario and also back button or direct url change from browser was reloading the controller, which I did not like. I finally went with the following approach:

1. Define a property in route definitions, called 'noReload' for those routes where you don't want the controller to reload on route change.

.when('/:param1/:param2?/:param3?', {
    templateUrl: 'home.html',
    controller: 'HomeController',
    controllerAs: 'vm',
    noReload: true
})

2. In the run function of your module, put the logic that checks for those routes. It will prevent reload only if noReload is true and previous route controller is the same.

fooRun.$inject = ['$rootScope', '$route', '$routeParams'];

function fooRun($rootScope, $route, $routeParams) {
    $rootScope.$on('$routeChangeStart', function (event, nextRoute, lastRoute) {
        if (lastRoute && nextRoute.noReload 
         && lastRoute.controller === nextRoute.controller) {
            var un = $rootScope.$on('$locationChangeSuccess', function () {
                un();
                // Broadcast routeUpdate if params changed. Also update
                // $routeParams accordingly
                if (!angular.equals($route.current.params, lastRoute.params)) {
                    lastRoute.params = nextRoute.params;
                    angular.copy(lastRoute.params, $routeParams);
                    $rootScope.$broadcast('$routeUpdate', lastRoute);
                }
                // Prevent reload of controller by setting current
                // route to the previous one.
                $route.current = lastRoute;
            });
        }
    });
}

3. Finally, in the controller, listen to $routeUpdate event so you can do whatever you need to do when route parameters change.

HomeController.$inject = ['$scope', '$routeParams'];

function HomeController($scope, $routeParams) {
    //(...)

    $scope.$on("$routeUpdate", function handler(route) {
        // Do whatever you need to do with new $routeParams
        // You can also access the route from the parameter passed
        // to the event
    });

    //(...)
}

Keep in mind that with this approach, you don't change things in the controller and then update the path accordingly. It's the other way around. You first change the path, then listen to $routeUpdate event to change things in the controller when route parameters change.

This keeps things simple and consistent as you can use the same logic both when you simply change path (but without expensive $http requests if you like) and when you completely reload the browser.

CGodo
  • 1,478
  • 14
  • 15
1

Since about version 1.2, you can use $location.replace():

$location.path('/items');
$location.replace();
Brent Washburne
  • 12,904
  • 4
  • 60
  • 82
0

Here's my fuller solution which solves a few things @Vigrond and @rahilwazir missed:

  • When search params were changed, it would prevent broadcasting a $routeUpdate.
  • When the route is actually left unchanged, $locationChangeSuccess is never triggered which causes the next route update to be prevented.
  • If in the same digest cycle there was another update request, this time wishing to reload, the event handler would cancel that reload.

    app.run(['$rootScope', '$route', '$location', '$timeout', function ($rootScope, $route, $location, $timeout) {
        ['url', 'path'].forEach(function (method) {
            var original = $location[method];
            var requestId = 0;
            $location[method] = function (param, reload) {
                // getter
                if (!param) return original.call($location);
    
                # only last call allowed to do things in one digest cycle
                var currentRequestId = ++requestId;
                if (reload === false) {
                    var lastRoute = $route.current;
                    // intercept ONLY the next $locateChangeSuccess
                    var un = $rootScope.$on('$locationChangeSuccess', function () {
                        un();
                        if (requestId !== currentRequestId) return;
    
                        if (!angular.equals($route.current.params, lastRoute.params)) {
                            // this should always be broadcast when params change
                            $rootScope.$broadcast('$routeUpdate');
                        }
                        var current = $route.current;
                        $route.current = lastRoute;
                        // make a route change to the previous route work
                        $timeout(function() {
                            if (requestId !== currentRequestId) return;
                            $route.current = current;
                        });
                    });
                    // if it didn't fire for some reason, don't intercept the next one
                    $timeout(un);
                }
                return original.call($location, param);
            };
        });
    }]);
    
Oded Niv
  • 2,557
  • 2
  • 22
  • 20
  • typo `path` at the end should be `param`, also this doesn't work with hashes, and the url in my address bar doesn't update – deltree Oct 18 '16 at 00:58
  • Fixed. Hmm weird it works for me though on html5 URLs. May still help someone I guess... – Oded Niv Oct 18 '16 at 05:35
0

Add following inside head tag

  <script type="text/javascript">
    angular.element(document.getElementsByTagName('head')).append(angular.element('<base href="' + window.location.pathname + '" />'));
  </script>

This will prevent the reload.

Vivek
  • 11,938
  • 19
  • 92
  • 127
0

There is simple way to change path without reloading

URL is - http://localhost:9000/#/edit_draft_inbox/1457

Use this code to change URL, Page will not be redirect

Second parameter "false" is very important.

$location.path('/edit_draft_inbox/'+id, false);
Dinesh Vaitage
  • 2,983
  • 19
  • 16
0

I couldn't make any of the answers here to work. As a horrible hack, I store in local storage a timestamp when I change the route, and check at page initialization whether this timestamp is set and recent, in that case I don't trigger some initialization actions.

In controller:

window.localStorage['routeChangeWithoutReloadTimestamp'] = new Date().getTime();
$location.path(myURL);

In config:

.when(myURL, {
            templateUrl: 'main.html',
            controller:'MainCtrl',
            controllerAs: 'vm',
            reloadOnSearch: false,
            resolve:
            {
                var routeChangeWithoutReloadTimestamp =
                    window.localStorage['routeChangeWithoutReloadTimestamp'];
                var currentTimestamp = new Date().getTime();
                if (!routeChangeWithoutReloadTimestamp ||
                        currentTimestamp - routeChangeWithoutReloadTimestamp >= 5000) {
                    //initialization code here
                }
                //reset the timestamp to trigger initialization when needed
                window.localStorage['routeChangeWithoutReloadTimestamp'] = 0;
            }
});

I used a timestamp rather than a boolean, just in case the code is interrupted before having a chance to reinit the value stored before changing route. The risk of collision between tabs is very low.

FBB
  • 1,414
  • 2
  • 17
  • 29