-
Notifications
You must be signed in to change notification settings - Fork 0
Home (old)
The goal of this project is to provide a set of Angular services and directives for
- managing the coarse-grained application UI via a hierarchical state machine abstraction
- managing views (with support for named and nested views) declaratively based on this state machine
- providing bi-directional mapping between the state and $location where this is desirable, but decouple state management from URL handling ("routing") as much as possible: Not every state has to map to a URL, and URLs can map to actions other than state transitions.
- defining application navigation in terms of the state machine, instead of via manually generated links.
- allowing (possibly asynchronous) application logic to be bound to state transitions, as well as giving the application the ability to prevent state transitions on a case-by-case basis.
The primary abstraction provided is that of a UI states that form a Hierarchical State Machine. Informally, a state corresponds to a place in the overall application UI, and navigating through the application means moving ("transitioning" in state machine lingo) between different states.
A state can define one or more views that define the application UI for that state. A view definition specifies a template and associated controller (optional) to load into a corresponding ui-view directive, as well as dependencies to resolve and inject into the controller (also optional).
In the simplest case, the top-level HTML of the application will contain a single ui-view
directive, and each state will define what to load into that directive when the application is in that state. However, a single view driven by a flat list of states is not enough for even moderately complex applications. The real power of these concepts comes from allowing views and states to be nested to form a hierarchy: The template loaded into a ui-view
by a state can itself contain further ui-view
directives that are then populated by child states.
- $state / $stateProvider: Manages state definitions, the current state, and state transitions. This includes triggering transition-related events and callbacks, asynchronously resolving any dependencies of the target state, and updating $location to reflect the current state. For states that have URLs, a rule is automatically registered with $urlRouterProvider that performs a transition to that state.
- ui-view directive: Renders views defined in the current state. Essentially ui-view directives are (optionally named) placeholders that gets filled with views defined in the current state.
- $urlRouter / $urlRouterProvider: Manages a list of rules that are matched against $location whenever it changes. At the lowest level, a rule can be an arbitrary function that inspects $location and returns true if it was handled. Support is provided on top of this for RegExp rules and URL patterns that are compiled into UrlMatcher objects via $urlMatcherFactory.
- $urlMatcherFactory: Compiles URL patterns with placeholders into UrlMatcher objects. In addition to the placeholder syntax supported by $routeProvider, it also supports an extended syntax that allows a regexp to be specified for the placeholder, and has the ability to extract named parameters from the query part of the URL.
- $templateFactory: Loads templates via $http / $templateCache. Used by $state
Full backwards compatibility with $routeProvider is not a primary goal this project. However, we are trying to have a clear migration path from $routeProvider to $stateProvider, and to use compatible terminology where this is possible and makes sense. At this point, most features of $routeProvider cleanly map to a sub-set of $stateProvider functionality, so that providing a compatibility shim that allows applications to be migrated bit by bit is feasible.
The new $stateProvider works similar to Angular's v1 router, but it focuses purely on state.
- A state corresponds to a "place" in the application in terms of the overall UI and navigation.
- A state describes (via the controller / template / view properties) what the UI looks like and does at that place.
- States often have things in common, and the primary way of factoring out these commonalities in this model is via the state hierarchy, i.e. parent/child states aka nested states.
A state in its simplest form can be added like this (typically within module.config):
<!-- in index.html -->
<body ng-controller="MainCtrl">
<section ui-view></section>
</body>
// in app-states.js (or whatever you want to name it)
$stateProvider.state('contacts', {
template: '<h1>My Contacts</h1>'
}
// main-controller.js
function MainCtrl($state){
$state.transitionTo('contacts');
}
The template is automatically placed into the lone ui-view
when the state is transitioned to.
Instead of writing the template inline you can load a partial. This is probably how you'll set templates most of the time.
$stateProvider.state('contacts', {
templateUrl: 'contacts.html'
}
Or you can use a template provider function which can be injected and has access to locals, like this:
$stateProvider.state('contacts', {
templateProvider: function ($timeout, $stateParams) {
return $timeout(function () { return '<h1>'+$stateParams.contactId+'</h1>' }, 100);
}
}
You can pair a template with a controller like this:
$stateProvider.state('contacts', {
template: '<h1>{{title}}</h1>',
controller: function($scope){
$scope.title = 'My Contacts';
}
}
You can use resolve to provide your controller with content or data that is custom to the state. This allows you to reuse controllers for similar objects that needs different dependencies.
$stateProvider.state('contacts', {
template: '<h1>{{title}}</h1>',
resolve: { title: 'My Contacts' },
controller: function($scope, title){
$scope.title = title;
}
}
There are also optional 'onEnter' and 'onExit' callbacks that get called when a state becomes active and inactive respectively. The callbacks also have access to all the resolved dependencies.
$stateProvider.state("contacts", {
template: '<h1>{{title}}</h1>',
resolve: { title: 'My Contacts' },
controller: function($scope, title){
$scope.title = 'My Contacts';
},
onEnter: function(title){
if(title){ ... do something ... }
},
onExit: function(title){
if(title){ ... do something ... }
}
}
States can be nested within each other. You can specify nesting in several ways:
You can use dot syntax to infer your heirarchy to the $stateProvider. Below, contacts.list
becomes a child of contacts
.
$stateProvider
.state('contacts', {});
.state('contacts.list', {});
Alternately, you can specify the parent of a state via the parent
property.
$stateProvider
.state('contacts', {});
.state('list', {
parent: 'contacts'
});
If you aren't fond of using string-based states, you can also use object-based states. The name
property goes in the object and the parent
property must be set on all child states, like this:
var contacts = {
name: 'contacts', //mandatory
templateUrl: 'contacts.html'
}
var contactsList = {
name: 'list', //mandatory
parent: contacts, //mandatory
templateUrl: 'contacts.list.html'
}
$stateProvider
.state(contacts)
.state(contactsList)
You can usually reference the object directly when using other methods and property comparisons:
$state.transitionTo(states.contacts);
$state.self === states.contacts;
$state.includes(states.contacts)
You can attach custom data to the state object (we recommend using a data
property to avoid conflicts).
// Example shows an object-based state and a string-based state
var contacts = {
name: 'contacts',
templateUrl: 'contacts.html',
data: {
customData1: 5,
customData2: "blue"
}
}
$stateProvider
.state(contacts)
.state('contacts.list', {
templateUrl: 'contacts.list.html',
data: {
customData1: 44,
customData2: "red"
}
})
With the above example states you could access the data like this:
function Ctrl($state){
console.log($state.current.data.customData1) // outputs 5;
console.log($state.current.data.customData2) // outputs "blue";
}
Child states will load their templates into their parent's ui-view
.
$stateProvider
.state('contacts', {
templateUrl: 'contacts.html'
controller: function($scope){
$scope.contacts = [{ name: 'Alice' }, { name: 'Bob' }];
}
})
.state('contacts.list', {
templateUrl: 'contacts.list.html';
});
function MainCtrl($state){
$state.transitionTo('contacts.list');
}
<!-- index.html -->
<body ng-controller="MainCtrl">
<div ui-view></div>
</body>
<!-- contacts.html -->
<h1>My Contacts</h1>
<div ui-view></div>
<!-- contacts.list.html -->
<ul>
<li ng-repeat="contact in contacts">
<a>{{contact.name}}</a>
</li>
</ul>
When the application is in a particular State (aka when a state is "active"), all it's ancestor states are implicitly active as well. In the sample, when "contacts.list" state is active, the "contacts" state is implicitly active as well. Child states inherit views (templates/controllers) and resolved dependencies from parent state(s), which they can override.
Here contacts.list
and contacts.detail
are both inheriting the controller from contacts
:
$stateProvider
.state('contacts', {
template: '<h1>My Contacts</h1>'
controller: function($scope){
$scope.contacts = [{ name: "Alice", favpet: "Mouse" },
{ name: "Bob", favpet: "Python" }];
}
})
.state('contacts.list', {
template: '<ul><li ng-repeat="contact in contacts">' +
'<a ui-state-ref="contacts.detail">{{contact.name}}</a>' +
'</li></ul>';
})
.state('contacts.detail', {
template: "{contact.name}}'s favorite pet is {{contact.favpet}}";
});
An abstract state can have child states but can not get activated itself. An 'abstract' state is simply a state that can't be transitioned to, because it's simply there to provide some UI or dependencies that are common to it's child states.
Here is an example where the 'contacts' state is abstract. It's main purpose is to provide its child states with access to the $scope.contacts data. $scope.contacts will be available to both child state views for interpolation.
$stateProvider
.state('contacts', {
abstract: true,
templateUrl: 'contacts.html',
controller: function($scope){
$scope.contacts = [{ name: "Alice" }, { name: "Bob" }];
})
.state('contacts.list', {
templateUrl: 'contacts.list.html'
})
.state('contacts.detail', {
templateUrl: 'contacts.detail.html'
})
You can name your views so that you can have more than one ui-view
per state. Let's say you had an application state that needed to dynamically populate a graph, some table data and filters for the table like this:
When setting multiple views you need to use the views
property on state. views
is an object. The property keys on views
should match your view names, like so:
<!-- somereportthing.html -->
<body>
<div ui-view="filters"></div>
<div ui-view="tabledata"></div>
<div ui-view="graph"></div>
</body>
$stateProvider
.state('report', {
views: {
'filters': { ... templates, controllers, resolve, etc ... },
'tabledata': {},
'graph': {},
}
})
Then each view in views
is can set up its own templates, controllers, and resolve data.
$stateProvider
.state('report',{
views: {
'filters': {
templateUrl: 'report-filters.html',
controller: function($scope){ ... controller stuff just for filters view ... }
},
'tabledata': {
templateUrl: 'report-table.html',
controller: function($scope){ ... controller stuff just for tabledata view ... }
},
'graph': {
templateUrl: 'report-graph.html',
controller: function($scope){ ... controller stuff just for graph view ... }
},
}
})
Every view gets assigned an absolute name that follows a scheme of viewname@statename, where viewname is the name used in the view directive and state name is the state's absolute name, e.g. contact.item. The previous example could also be written as such:
.state('report',{
views: {
'filters@report': {
templateUrl: 'report-filters.html',
controller: function($scope){ ... controller stuff just for filters view ... }
},
... other views ...
Notice that the view name is now specified as the absolute name, as opposed to the relative name. It is targeting the 'filters' view located in the 'report' state. This let's us do some powerful view targeting. Let's assume we had several nested views set up like this (this example is not realistic, its just to illustrate view targeting):
<!-- index.html -->
<body ng-app>
<div ui-view></div> <!-- Assume contacts.html plugs in here -->
<div ui-view="status"></div>
</body>
<!-- contacts.html -->
<h1>My Contacts</h1>
<div ui-view></div>
<div ui-view="detail"></div> <!-- Assume contacts.detail.html plugs in here -->
<!-- contacts.detail.html -->
<h1>Contacts Details</h1>
<div ui-view></div>
<div ui-view="info"></div>
Let's look at the various views you could target from within the contacts.detail state. Remember that if an @
is used then the view path is considered absolute:
$stateProvider
.state('contacts.detail', {
templateUrl: 'contacts.detail.html'
views: {
"info" : {} // relatively targets the "info" view in "contacts.detail" state
"" : {} // relatively targets the unnamed view in "contacts.detail" state
"detail@contacts" : {} // absolutely targets the "detail" view in parent "contacts" state
"@contacts" : {} // absolutely targets the unnamed view in parent "contacts" state
"status@" : {} // absolutely targets the "status" view in root unnamed state
"@" : {} // absolutely targets the unnamed view in root unnamed state
});
You can see how this ability to not only set multiple views within the same state but ancestor states could become a veritable playground for developer :).
Most states in your application will probably have a url associated with them. URL Routing was not an after thought to the state mechanics, but was figured into the design from the beginning (all while keeping states separate from url routing)
Here's how you set a basic url.
$stateProvider
.state('contacts', {
url: "/contacts",
templateUrl: 'contacts.html'
})
Now when the user accesses index.html/contacts
then the 'contacts' state would become active and the main ui-view
will be populated with the 'contacts.html' partial. Alternatively, if the user were to transition to the 'contacts' state via transitionTo('contacts')
then the url would be updated to index.html/contacts
Often, URLs have dynamic parts to them which are called parameters. There are several options for specifying parameters. A basic parameter looks like this:
$stateProvider
.state('contacts.detail', {
url: "/contacts/:contactId",
templateUrl: 'contacts.detail.html',
controller: function ($stateParams) {
// If we got here from a url of /contacts/42
expect($stateParams).toBe({contactId: 42});
}]
})
Alternatively you can also use curly brackets:
// identical to previous example
url: "/contacts/{contactId}"
However, a bonus to using curly brackets is the ability to set a Regular Expression rule for the parameter:
// will only match a contactId of one to eight number characters
url: "/contacts/{contactId:[0-9]{1,8}}"
As you saw previously the $stateParams service is an object that will have one key per url parameter. The $stateParams is a perfect way to provide your controllers or other services with the individual parts of the navigated url.
// If you had a url on your state of:
url: '/users/:id/details/{type}/{repeat:[0-9]+}?from&to'
// Then you navigated your browser to:
'/users/123/details//0'
// Your $stateParams object would be
{ id:'123', type:'', repeat:'0' }
// Then you navigated your browser to:
'/users/123/details/default/0?from=there&to=here'
// Your $stateParams object would be
{ id:'123', type:'default', repeat:'0', from='there', to='here' }