How to resolve application-wide resources centrally in AngularJS with ui-router
AngularUI Router is an amazing tool.
If you have read my article on resource resolution, you already know how to make sure that promises are resolved before controllers are instantiated. In fact, if you haven't read the article yet, I strongly recommend that your read it first before reading this article.
The challenge
Resolving promises before controllers are instantiated ensures that critical data is available in your application at the time you need it. In many cases information such as configuration data or translation dictionaries is required to be present at all times in your application.
You could hard code the information in your AngularJS code, but what if you need to load the information from your server?
And what if you need that information in every state?
The normal approach
Armed with the trick from my article on resource resolution, we can easily solve the problem like this:
// Use $stateProvider to configure your states.
$stateProvider
.state('home', {
url: '/home',
resolve: {
// Get AngularJS resource to query
Config: 'Config',
// Use the resource to fetch data from the server
config: function(Config){
return Config.get().$promise;
},
// Extract the configuration data
// that we need for the home state
data: function(config){
return config.homeConfigData;
}
},
views: {
'content': {
templateUrl: '/home.html',
// The config data is guaranteed to be resolved
// when the HomeCtrl is instantiated
controller: 'HomeCtrl'
}
})
.state('articles', {
url: '/articles',
resolve: {
// Get AngularJS resource to query
Config: 'Config',
// Use the resource to fetch data from the server
config: function(Config){
return Config.get().$promise;
},
// Extract the configuration data
// that we need for the articles state
data: function(config){
return config.articlesConfigData;
}
},
views: {
'content': {
templateUrl: '/articles.html',
// The config data is guaranteed to be resolved
// when the HomeCtrl is instantiated
controller: 'ArticlesCtrl'
}
});
This works like a charm, but the code to fetch the configuration becomes repetitive.
If your application has many states that require the same information, this would rapidly become hard to maintain.
Centralize resolution logic
Ui-router has this extremely handy feature where child states inherit resolves from their parent states. From the ui-router docs:
Child states will inherit resolved dependencies from parent state(s), which they can overwrite. You can then inject resolved dependencies into the controllers and resolve functions of child states.
So to avoid code duplication, let's introduce an abstract root state and call it main
:
// Use $stateProvider to configure your states.
$stateProvider
.state('main', {
url: '/main',
// Make this state abstract so it can never be
// loaded directly
abstract: true,
// Centralize the config resolution
resolve: {
// Get AngularJS resource to query
Config: 'Config',
// Use the resource to fetch data from the server
config: function(Config){
return Config.get().$promise;
}
})
// The home state now becomes a child state of main
// so it can access the resolves of main
.state('main.home', {
url: '/home',
resolve: {
// Inject the config resolved in the main state
// and extract the data we need
data: function(config){
return config.homeConfigData;
}
},
views: {
'content@': {
templateUrl: '/home.html',
// The config data is guaranteed to be resolved
// when the HomeCtrl is instantiated
controller: 'HomeCtrl'
}
})
// The articles state also becomes a child state of main
// so it can access the resolves of main
.state('main.articles', {
url: '/articles',
resolve: {
// Inject the config resolved in the main state
// and extract the data we need
data: function(config){
data = config.articlesConfigData;
}
},
views: {
'content@': {
templateUrl: '/articles.html',
// The config data is guaranteed to be resolved
// when the HomeCtrl is instantiated
controller: 'ArticlesCtrl'
}
});
All shared code has now been centralized in the main
parent state. The state is abstract
, because we don't want visitors to be able to directly navigate to the state. We just want to use the state to perform resource resolution for its child states.
Update 2016-01-04: View a working example here.
Ensure resolution in child state
Notice that the config
from the main
state is injected as an argument in the resolve from the child state.
Since the config
in the main state returns a promise, it is guaranteed to be resolved by the time the controller of the child state is instantiated.
It is important to note that this is only guaranteed when the config
in the parent state returns a promise.
If the parent state would have looked like this:
// Use $stateProvider to configure your states.
$stateProvider
.state('main', {
url: '/main',
// Make this state abstract so it can never be
// loaded directly
abstract: true,
// Centralize the config resolution
resolve: {
// Get AngularJS resource to query
Config: 'Config',
// Use the resource to fetch data from the server
// but don't return the promise
config: function(Config){
return Config.get();
}
});
then you can rest assured that config
will eventually be resolved, but this time it is not guaranteed to be resolved by the time the child state controller is instantiated.
Resolve like a pro
With this powerful technique you can:
- create hierarchies of promise based dependencies
- centralize logic to resolve application-wide dependencies
- specify which resolves are required to be resolved in each state individually
Taking control over dependency resolution allows you to take your application to a whole new level by letting you control exactly what data is available at the time you need it.
Centrally. Hierarchically. Per state. Using promises. Like a pro.
Sergey Goliney has created a nice plnkr here that demonstrates how the child states wait for the parent resolves to be resolved. Thanks Sergey!