197

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=...

1
  • found this answer: 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) Commented Mar 24, 2015 at 7:41

12 Answers 12

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.

Sign up to request clarification or add additional context in comments.

10 Comments

Thanks for that. I'm trying to figure out where I would then change the controller.
Got it. Added a controller param to my view array, then added ng-controller="view.controller" to the ng-include directive.
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 <div ng-controller="itemFooCtrl">... your template content...</div>. EDIT: Nice to see you solved, it would be great if you updated your question with how you solved, perhaps with a jsFiddle.
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.
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 (docs.angularjs.org/api/ngRoute.$routeProvider) now says: [reloadOnSearch=true] - {boolean=} - reload route when only $location.search() or $location.hash() changes.
|
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.

11 Comments

Great solution. Is there anyway to get the browser's back button to "honor" this?
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.
Just wanted to say that the original solution was acutally posted by "EvanWinstanley" here: github.com/angular/angular.js/issues/1699#issuecomment-34841248
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?
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)
|
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.

5 Comments

great tip, it works perfectly for my scenario too! thank you very much! :)
This is a great "the russians used pencils" answer, works for me :)
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: 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 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.
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

Comments

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

1 Comment

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
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.

Comments

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.

Comments

1

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

$location.path('/items');
$location.replace();

Comments

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

2 Comments

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
Fixed. Hmm weird it works for me though on html5 URLs. May still help someone I guess...
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.

Comments

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

1 Comment

this is not correct for AngularJS docs.angularjs.org/api/ng/service/$location
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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.