I had a problem: My application had multiple independent parts, which
needed their own states. For example, I have a toolbar that’s on top
and a sidebar on the right. The user can change each of the parts
without affecting the other, and setting it up as a normal ui-router
state tree will not work.
The standard ui-router has no concept of parallel states. Everything must be modeled as a tree, which means a setup like this doesn’t work. For example, changing the sidebar’s state would affect the toolbar’s state as well - which is not something we want.
Thankfully there’s ui-router-extras, which adds support for so-called “sticky states” or “parallel states”. We can use this to have as many individual parts, that have their own parallel state trees, as we want.
Let’s go through a small sample app and look how to set this up step by step. You can find the full sample app here so you can follow along more easily.
An easy way to set it up is to use cdnjs and simply include the necessary scripts like so:
It’s important each part of our application has its own ui-view. Each sticky state must have its own unique view.
We’ll also set up some buttons to toggle the states so we can see it in action.
First, we define our application’s module. Note that we need to include ui-router and the ui-router-extras core and sticky modules in our dependencies:
You can put other content into the base state’s template if you want, or set it up using
Each of the child states -
Next, we’ll set up the sidebar states. These work exactly the same way as the toolbar states - we have a base
Normally you could use the
We can work around it by manually changing the state in the application’s run-block. However, simply calling
Although ui-router is a bit limited when it comes to parallel states like this, it’s mostly fixed by ui-router-extras and workarounds like chaining the default state loading. One of ui-router’s 1.0 version goals is better support for scenarios like this, but until then, this is the best way to do it.
The standard ui-router has no concept of parallel states. Everything must be modeled as a tree, which means a setup like this doesn’t work. For example, changing the sidebar’s state would affect the toolbar’s state as well - which is not something we want.
Thankfully there’s ui-router-extras, which adds support for so-called “sticky states” or “parallel states”. We can use this to have as many individual parts, that have their own parallel state trees, as we want.
Let’s go through a small sample app and look how to set this up step by step. You can find the full sample app here so you can follow along more easily.
Setting up the necessary libraries
ui-router-extras recommends ui-router version 0.2.8 or newer. As for ui-router-extras itself, we can either install it completely, or just install the core and sticky modules which are needed for sticky states.An easy way to set it up is to use cdnjs and simply include the necessary scripts like so:
<script src="https://cdnjs.cloudflare.com/ajax/libs/ui-router-extras/0.1.0/modular/ct-ui-router-extras.core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ui-router-extras/0.1.0/modular/ct-ui-router-extras.sticky.min.js"></script>
ui-router-extras can also be installed via npm or bower.Setting up the page markup
Below we have a fairly simple page template for this example. We set up two named views,toolbar
and sidebar
, which will contain our sticky states, and some templates for their child-states.It’s important each part of our application has its own ui-view. Each sticky state must have its own unique view.
We’ll also set up some buttons to toggle the states so we can see it in action.
<body ng-app="stickystates">
<div ui-view="toolbar"></div>
<div ui-view="sidebar"></div>
<div ng-controller="StateController">
<button ng-click="$state.go('toolbar.state1')">Toolbar 1</button>
<button ng-click="$state.go('toolbar.state2')">Toolbar 2</button>
<button ng-click="$state.go('sidebar.state1')">Sidebar 1</button>
<button ng-click="$state.go('sidebar.state2')">Sidebar 2</button>
</div>
<script type="text/ng-template" id="toolbar-state1.html">
Toolbar state 1
</script>
<script type="text/ng-template" id="toolbar-state2.html">
Toolbar state 2
</script>
<script type="text/ng-template" id="sidebar-state1.html">
Sidebar state 1
</script>
<script type="text/ng-template" id="sidebar-state2.html">
Sidebar state 2
</script>
</body>
Setting up the states
We’ll set up two separate state trees: One for the toolbar and one for the sidebar.First, we define our application’s module. Note that we need to include ui-router and the ui-router-extras core and sticky modules in our dependencies:
var app = angular.module('stickystates', [
'ui.router',
'ct.ui.router.extras.core',
'ct.ui.router.extras.sticky'
]);
Next, we’ll set up the $stateProvider
. Both the toolbar and sidebar states are set up in the same way, the main difference being which templates and ui-view used.app.config(['$stateProvider', function($stateProvider) {
//set up the toolbar parent state, and its two child-states
$stateProvider.state('toolbar', {
sticky: true,
views: {
toolbar: { template: '<div ui-view></div>' }
}
})
.state('toolbar.state1', {
templateUrl: 'toolbar-state1.html'
})
.state('toolbar.state2', {
templateUrl: 'toolbar-state2.html'
});
}]);
Here we set up three states that we need for the toolbar. First, we set up the base state toolbar
. We set sticky: true
to make it a sticky states. Its template is just a div, containing a
ui-view. It’s important to set it up like this - if you have multiple
states accessing the same named view, even if they’re the children of
the sticky state, it will not work correctly.You can put other content into the base state’s template if you want, or set it up using
templateUrl
- just make sure you include a ui-view within the template for it. Otherwise this won’t work correctly.Each of the child states -
toolbar.state1
and toolbar.state2
- have a templateUrl
. The template contents are placed within the ui-view from the parent toolbar
state. If you want, you can include a state controller in addition to a template, or any other state properties.Next, we’ll set up the sidebar states. These work exactly the same way as the toolbar states - we have a base
sidebar
state with a ui-view, and two child states.//set up the sidebar's states, which are structured the same way
$stateProvider.state('sidebar', {
sticky: true,
views: {
sidebar: { template: '<div ui-view></div>' }
}
})
.state('sidebar.state1', {
templateUrl: 'sidebar-state1.html'
})
.state('sidebar.state2', {
templateUrl: 'sidebar-state2.html'
});
Note the only differences in the sidebar states are the state names, templates and the target ui-view.Setting up the button controller
For toggling between the states, we need to write a controller.app.controller('StateController', ['$scope', '$state', function($scope, $state) {
$scope.$state = $state;
}]);
In a real application, you probably wouldn’t want to add $state
directly into scope as here, but this works for demonstration purposes.Loading default states
Depending on how your application and views are set up, you may want to load a default state for each of your individual views.Normally you could use the
url
property on a state to
choose which to load, but if you want to load multiple defaults - for
example, if we want to load a state into both the sidebar and the
toolbar - then it won’t work, as ui-router requires each state to have a
unique URL.We can work around it by manually changing the state in the application’s run-block. However, simply calling
$state.go
twice in a row will not work. We need to chain it using the promise like so:app.run(['$state', function($state) {
$state.go('toolbar.state1').then(function() {
$state.go('sidebar.state1');
});
}]);
If you have many states and don’t want to repeat their names, you can also use a flag on the state definition object$stateProvider.state('sidebar.state1', {
preload: true,
templateUrl: 'sidebar-state1.html'
})
;
Here we set a preload
property on the state. Then, in the
run-block, we can load all states with the property set and load them
instead of having to hardcode them:app.run(['$state', '$q', function($state, $q) {
var preloads = $state.get().filter(function(s) { return s.preload; });
preloads.reduce(function(promise, nextState) {
return promise.then($state.go.bind($state, nextState));
}, $q.when());
}]);
Conclusion
With all of the above set up, you have a working application with parallel states which can each be manipulated independently from each other.Although ui-router is a bit limited when it comes to parallel states like this, it’s mostly fixed by ui-router-extras and workarounds like chaining the default state loading. One of ui-router’s 1.0 version goals is better support for scenarios like this, but until then, this is the best way to do it.
No comments:
Post a Comment