Déporter du code dans un provider

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.

Hadrien

Hi, I'm a french Javascript Lead Developer, Web Architect from Toulouse, France. I've worked for 12 years for many projects with YUI, AngularJS, Aurelia.io and now React and React native.

Toulouse, France https://hadrien.eu