I’ve made quite a few posts detailing the use of ui-router. These posts include general menus, tabs, and other “knowledge” of state mechanisms. The one thing missing though is maintaining state, or route, data.
A good example of where state data is important is in the usage of the uib-tabset. When the tab is changed, and the ui-state is changed, a new controller is instantiated and the current controller is disposed. If the user then chooses to go back to the previous tab, the cycle starts again and it’s like a fresh start. Data is lost and data is reloaded. I mitigated this a bit by keeping track of the previous href “state” and pushing relevant parameters in the href/query string.
This, in many cases, is good enough. However, if there is a fair bit of data pulled from, or load put on, the server, it’s not the most elegant solution. I mean, why re-get all of the data when we won’t need to?
Take a look at the previous source code for the “appStates” service:
(function () { var long2know; try { long2know = angular.module("long2know") } catch (err) { long2know = null; } if (!long2know) { angular.module('long2know.services', ['ngResource', 'ngAnimate']); angular.module('long2know.controllers', []); angular.module('long2know.directives', []); angular.module('long2know.constants', []); angular.module('long2know', [ 'long2know.services', 'long2know.controllers', 'long2know.directives', 'long2know.constants' ]); } var appStates = function ($state) { var states = [ { name: 'state1', heading: "Tab 1", route: "tabs.state1", active: false, isVisible: true, href: $state.href("tabs.state1") }, { name: 'state2', heading: "Tab 2", route: "tabs.state2", active: false, isVisible: true, href: $state.href("tabs.state2") }, { name: 'state3', heading: "Tab 3", route: "tabs.state3", active: false, isVisible: true, href: $state.href("tabs.state3") }, { name: 'state4', heading: "Tab 4", route: "tabs.state4", active: false, isVisible: true, href: $state.href("tabs.state4") } ]; return { states: states }; }; appStates.$inject = ['$state']; angular.module('long2know.services') .factory('appStates', appStates); })()
Since Angular treats this service as a singleton, we can easily, and simply attach another object to each state. Let’s call it “data.” The “states” take on this format:
var params = updateLocSearch(); var params = updateLocSearch(){ name: 'state1', heading: "Tab 1", route: "tabs.state1", active: false, isVisible: true, href: $state.href("tabs.state1"), data: {} }
With that in place, on any of my controllers where one wants to maintain state data, two methods can be defined to get/set the data. Keep in mind that the navigationService always allows us to get the current state.
getStateData = function () { var params = updateLocSearch(); var currentState = navigationService.currentState(); var searchParams = $location.search(); var href = $state.href(currentState.route, params).replace(/~2F/g, '%2F'); if (href === currentState.href) { return currentState.data && Object.keys(currentState.data).length > 0 ? currentState.data : false; } else { return false; } }, setStateData = function () { navigationService.currentState().data = { someMember: vm.someMember, } }, setStateFromData = function (data) { vm.someMember = data.someMember; }
Notice that a method called “updateLocSearch” is mentioned. This method is simply updating the URL query parameters.
updateLocSearch = function () { isAutoLocChange = false; var params = getParamsForSearch(); params.searchProperty = undefined; $location.search(params); return params; },
The “getParamsForSearch” method is a method I want bore you with since it’s pretty application specific. In an effort to describe it, though, suffice it to say it’s looking at objects on the controller (this or vm) and pushing them into a new object which, in turn, is used to update the URL query parameters.
Finally, the controller, in its initialization method, must wire-up the appropriate mechanisms to reload and set the data. Note that I have a flag indicating whether location change success trigger should do anything (isAutoLocChange) so that I know if the URL changed or if the code at hand changed it, and a flag to indicate whether or not this is the first pass through with data (isInitWIthData). With this small bit of logic in place within each of the controllers, state data is maintained across ui-router state changes.
var promises = []; var data = getStateData(); if (data) { isAutoLocChange = true; isInitWithData = true; setStateData(data); $log.log('Previous state data loaded.'); } else { // Push our init promises onto the stack } var promise = $q.all(promises).then(function () { vm.initComplete = true; $scope.$on('$locationChangeSuccess', function (event, toState, toParams) { if ($state.includes('root.logging') && toState.indexOf('logging#/logData') !== -1) { if (!isInitWithData) { getQueryParameters(); if (isAutoLocChange) { executeSearch(); } } isInitWithData = false; } }); }); navigationService.setOnNavigateCallback(function () { setStateData(); });
PS – I’ll provide a fully working application in the future that utilizes this pattern.