Comme promis, voici la suite du billet relatif à la résolution de routes où je vous expliquerais comment mutualiser et sortir le code métier de vos configurations de route. On va rester sur notre exemple précédent et rajouter deux nouvelles routes : /pages et /page/:id qui vont donc faire la même chose que /post* mais pour des pages. Et des pages, ce sont des posts avec une propriété page==true
. Et donc, les deux routes /post/:id et /page/:id vont avoir quasiment le même resolve :
.state('main.post', {
url: '/post/:id',
templateUrl: 'views/post-edit.html',
controller: 'PostEditCtrl',
resolve: {
post: ['$stateParams', '$state', '$timeout', 'Posts', function($stateParams, $state, $timeout, Posts) {
var post = new Posts({
id: $stateParams.id
}),
promise = post.load();
promise.catch(function(err) {
$timeout(function() {
$state.go('main.posts');
}, 1);
});
return promise;
}]
}
})
.state('main.page', {
url: '/page/:id',
templateUrl: 'views/page-edit.html',
controller: 'PageEditCtrl',
resolve: {
page: ['$stateParams', '$state', '$timeout', 'Posts', function($stateParams, $state, $timeout, Posts) {
var page = new Posts({
id: $stateParams.id,
page: true
}),
promise = page.load();
promise.catch(function(err) {
$timeout(function() {
$state.go('main.pages');
}, 1);
});
return promise;
}]
}
})
Deux problèmes dans cette implémentation :
- On a du code métier dans la config,
- On a du code dupliqué
On pourrait répondre simplement au second problème en écrivant cette fonction en tête du bloc config :
function resolvePost(page) {
return ['$stateParams', '$state', '$timeout', 'Posts', function($stateParams, $state, $timeout, Posts) {
var post = new Posts({
id: $stateParams.id,
page: !!page
}),
promise = post.load();
promise.catch(function(err) {
$timeout(function() {
$state.go(page ? 'main.pages' : 'main.posts');
}, 1);
});
return promise;
}];
};
…
.state('main.post', {
url: '/post/:id',
templateUrl: 'views/post-edit.html',
controller: 'PostEditCtrl',
resolve: {
post: resolvePost()
}
})
.state('main.page', {
url: '/page/:id',
templateUrl: 'views/page-edit.html',
controller: 'PageEditCtrl',
resolve: {
page: resolvePost(true)
}
})
Mais ça ne résoud pas le premier problème. On va rapidement se retrouver avec énormément de code métier dans ce bloc de config. Et en plus, c'est pas testable.
Nous allons donc utiliser les possiblités offertes par les providers !
Providers
Le provider, c'est la forme ultime du service. Ce design pattern permet de donner accès à un provider afin de le configurer avant d'injecter son service. C'est justement ce qu'on fait avec $stateProvider ou $routeProvider. Et bien nous allons nous servir de cette particularité pour ranger du code métier dans notre service mais uniquement dans le contexte de la configuration.
Voici comment fonctionne un provider :
module('monApp')
.provider('Posts', function PostsProvider() {
// Ici, on peut préparer les données préalables à la construction du service
// On peut aussi, et surtout, exposer des propriétés du provider
this.foo = 'bar';
this.setSomething = function() {};
var provider = this;
// C'est uniquement ceci qui sera retourné lors de l'injection du service
this.$get = {
this.foo = provider.foo;
};
// Tous le reste n'est accessible qu'en mode config
});
Quand ce service sera injecté grâce à Posts
, seule la propriété $get
sera retournée. Cependant, nous pouvons charger l'ensemble du provider depuis une config :
angular.module('monApp')
.config(['PostsProvider', function(PostsProvider) {
PostsProvider.foo = 'rab';
}])
.controller(['Posts', function(Posts) {
console.log(Posts.foo); // >> 'rab'
}]);
Et donc, vous l'avez compris, c'est ici que nous allons ranger notre fonction de resolution.
angular.module('monApp')
.provider('Posts', function() {
this.$get = ['$http', function($http) {
function Posts() {};
Posts.prototype.load = function() {
…
};
return Posts;
}];
this.resolve = function(page) {
return ['$stateParams', '$state', '$timeout', 'Posts', function($stateParams, $state, $timeout, Posts) {
var post = new Posts({
id: $stateParams.id,
page: !!page
}),
promise = post.load();
promise.catch(function(err) {
$timeout(function() {
$state.go(page ? 'main.pages' : 'main.posts');
}, 1);
});
return promise;
}];
};
})
.config(['$stateProvider', 'PostsProvider', function($stateProvider, PostsProvider) {
$stateProvider
.state('main.post', {
url: '/post/:id',
templateUrl: 'views/post-edit.html',
controller: 'PostEditCtrl',
resolve: {
post: PostsProvider.resolve()
}
})
.state('main.page', {
url: '/page/:id',
templateUrl: 'views/page-edit.html',
controller: 'PageEditCtrl',
resolve: {
page: PostsProvider.resolve(true)
}
});
}])
.controller(['PostEditCtrl', function(post) {
scope.post = post;
}])
.controller(['PageEditCtrl', function(page) {
scope.page = page;
}]);
Et voilà ! Plus de code métier dans la config, et plus de duplication de code ! On peut aussi tester la méthode PostsProvider.resolve
sans difficulté. Pensez juste à charger votre service avant la config. C'est une erreur qui peut arriver facilement car généralement, on définit le module de l'app et la config des routes dans le même fichier (app.js). Il faudra écrire les routes dans un autre module et bien faire en sorte qu'il soit chargé après les providers.