Can you change a path without reloading the controller in AngularJS?
Asked Answered
C

12

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

Cassycast answered 20/2, 2013 at 7:15 Comment(1)
found this answer: https://mcmap.net/q/129965/-updating-url-in-angular-js-without-re-rendering-view - 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)Smew
W
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.

Wanting answered 20/2, 2013 at 7:27 Comment(10)
Thanks for that. I'm trying to figure out where I would then change the controller.Cassycast
Got it. Added a controller param to my view array, then added ng-controller="view.controller" to the ng-include directive.Cassycast
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.Wanting
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.Cassycast
If you can isolate your issue in a jsFiddle/plnkr/etc, I'm sure we can help you with it.Wanting
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!Cassycast
Scope issues were un-related. This is resolved, and I updated the solution. Thanks again.Cassycast
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.Hollishollister
For anyone interested, same parameter exists for UI router - you can set on state "reloadOnSearch: false"Underproof
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.Bagwig
A
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.

Abdulabdulla answered 8/6, 2014 at 1:14 Comment(11)
Great solution. Is there anyway to get the browser's back button to "honor" this?Libb
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.Leitmotiv
Just wanted to say that the original solution was acutally posted by "EvanWinstanley" here: github.com/angular/angular.js/issues/1699#issuecomment-34841248Anaerobic
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?Close
This solution still makes the view reload.Supposititious
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)Dailey
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 pageUnwieldy
Is there a way to implement the same using ui-router ?Eyrie
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.Uta
Works great at AngularJS 1.5Decease
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?Conventional
V
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.

Valedictory answered 17/11, 2014 at 23:30 Comment(5)
great tip, it works perfectly for my scenario too! thank you very much! :)Creation
This is a great "the russians used pencils" answer, works for me :)Sphagnum
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 {}Sphagnum
@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.Amused
@Amused 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.Sphagnum
R
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://mcmap.net/q/127831/-can-you-change-a-path-without-reloading-the-controller-in-angularjs

P.S. This solution https://mcmap.net/q/127831/-can-you-change-a-path-without-reloading-the-controller-in-angularjs contains bug after path(, false) called - it will break browser navigation back/forward until path(, true) called

Revengeful answered 12/8, 2015 at 11:31 Comment(0)
P
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

Preraphaelite answered 16/9, 2013 at 5:31 Comment(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 workPreraphaelite
H
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.

Hastie answered 10/4, 2016 at 0:17 Comment(0)
C
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.

Coretta answered 2/10, 2016 at 20:5 Comment(0)
A
1

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

$location.path('/items');
$location.replace();
Aftertaste answered 11/12, 2018 at 4:32 Comment(0)
U
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);
            };
        });
    }]);
    
Uta answered 26/9, 2016 at 16:3 Comment(2)
typo path at the end should be param, also this doesn't work with hashes, and the url in my address bar doesn't updateApia
Fixed. Hmm weird it works for me though on html5 URLs. May still help someone I guess...Uta
P
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.

Providing answered 29/3, 2017 at 2:42 Comment(0)
S
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);
Sessler answered 17/5, 2018 at 11:47 Comment(1)
this is not correct for AngularJS docs.angularjs.org/api/ng/service/$locationSearcy
R
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.

Repulse answered 7/8, 2020 at 14:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.