Angular provides some handy mechanisms for dealing with browser history. With these mechanisms it’s straight-forward to handle URL changes through direct user interaction or the browser back/forward buttons.
Typically, when entering an Angular app, you’ll have an abstract route defined and then use the $urlRouterProvider to define a default route. Well, when I say “you,” I should qualify that to say that this is how I typically define states with ui-router.
For example, in one app I worked on, the default is the “/payments” state.
$urlRouterProvider. when('/payment', '/payments') .otherwise("/payments");
So, browsing via link or directly to:
https://localhost/payment
Results in (2) history entries within the browser. If I do nothing else, clicking the back button would return me to the “Home” or landing page.
However, with default search properties and such, I typically force an initial search and update the URL.
$location.search(params);
This creates a 3rd browser history entry. So, the user, at this point, would have to click the back button twice to get back to the “Home” page. IMHO, that’s fine.
But, we can control the browser history through the HTML5 history API and Angular’s $location service.
For instance, I may want to intercept any location change (back/forward buttons or otherwise) and issue another history change. Here’s some code that would attempt to force the user to a specific history under certain conditions. This is pretty ugly/hacky and prone to fragility, though.
$rootScope.$on('$locationChangeSuccess', function (event, toState, toParams) { var baseUrl = $location.protocol() + '://' + $location.host() + ($location.port() !== 80 && $location.port() !== 443 ? ':' + $location.port() : '') + '/payment'; if (toState === baseUrl) { history.replaceState(hash, null, $location.absUrl()); $log.log("Entered default abstract route."); if (!firstLoad) { if (!attemptedToForceBack) { $log.log("Attempted to go back to the default state"); history.back(); } attemptedToForceBack = true; } firstLoad = false; } });
Another very easy way to handle this, in the cases where $location.search() is used to push history states to the browser is to conditionally replace the history on first load. This would be per controller.
I could define a var to let me know if the user came into the controller with query parameters:
var vm = this, hasQueryParams = false, isAutoLocChange = true;
In the controller’s init method, I check for any params:
locSearch = $location.search(); hasQueryParams = locSearch.pageNumber || locSearch.pageSize || ....
The init method which is executed on controller instantiation calls a method called “executeSearch” which updates the URL via the location service. Adding a simple condition based on hasQueryParams can/will replace the first entry in the browser history that we don’t care about. This is accomplished by telling the $location service to “replace” the browser state. Sure, we could do this ourselves using the window.history API (replaceState or pushState), but that potentially triggers the $location change events and another $digest cycle. Within the Angular framework, it’s generally better to utilize the framework methods.
There’s this bit of code within the “executeSearch” method:
isAutoLocChange = false; if (hasQueryParams) { $location.search(params); } else { $location.search(params).replace(); } isAutoLocChange = true;
Finally, within the controller itself, we listen for location changes. We want to execute our search whenever the location changes (back/forward button or typed url):
$scope.$on('$locationChangeSuccess', function (event, toState, toParams) { if ($state.includes('tabs.payments') && toState.indexOf('payment#/payments') !== -1) { getQueryParameters(); if (isAutoLocChange) { executeSearch(); } } });
Notice the other flag called isAutoLocChange. This is used to keep track of when the controller itself is modifying the URL through the $location service. Without bypassing the event handling when that var is false would put us into an infinite loop, potentially.