AngularJs : Où sont mes models ?

Lorsqu’on commence à développer une nouvelle application avec le Framework Angular, il est très aisé d’obtenir un résultat rapide et fonctionnel. C’est l’un des atouts majeurs d’AngularJs vis-à-vis de ses concurrents.

Même si AngularJs semble plus proche du pattern MVVM (MVW selon Igor Mina pour ModelViewWhatever) que MVC, on fait vite l’analogie avec les frameworks MVC backends classiques que la plupart des développeurs ont l’habitude d’utiliser (Symfony, Spring, …).

The $scope object could be considered the ViewModel that is being decorated by a function that we call a Controller.

MVC. Modèle, Vue, Contrôleur… Mais au fait, où est le modèle sur Angular (angular models) ? Lorsque j’ai découvert AngularJs, ayant eu auparavant l’occasion de développer quelques grosses applications avec EmberJs, je m’attendais à hériter d’un type NgModel quelconque. Mais la philosophie Angular est radicalement différente de celle d’Ember (http://emberjs.com/guides/models) à ce sujet :

Unlike other frameworks, there is no need to inherit from proprietary types in order to wrap the model in accessors methods. Angular models are plain old JavaScript objects. This makes your code easy to test, maintain, reuse, and again free from boilerplate.

Donc en gros, un modèle, c’est un objet javascript tout simple. Voici donc un article de blog :

{
  title: "Article 1",
  description: "Gouzigouza",
  content: "The content"
}

Cela tombe bien car cela ressemble étrangement à ce que je pourrais récupérer directement depuis mon backend via une API REST en faisant quelque chose de ce genre via $http ou grâce à $resource :

function MonController($http) {

  $http.get('url/vers/mon/api/articles/1').then(function(articleData) {
    // Injection dans le scope pour afficher mon article dans une vue
    $scope.article = articleData.data;

  }).then(null, function(rejection) {
    // manage error
  });
}

Notez que la logique de récupération d’un article ici implémentée dans un controller aurait tout intérêt à être elle-même extraite dans un service dédié :

angular.module('monModule').factory('ArticlesService', function($http) {

  return {
    getArticle: function(id) {

      return $http.get('url/vers/mon/api/articles/' + id).then(function(articleData) {
        return articleData.data;
      });
    }
  }
}

Un problème simple

Tout cela est très bien mais hormis dans un contexte de consultation simple de la donnée, il est peu probable qu’il n’y ait pas un peu de traitement à faire sur cet objet pour l’utiliser. Par exemple, si je veux gérer une description courte à afficher dans ma vue.

Logique dans le controller ?

Une première approche un peu naïve serait de faire quelque chose du genre :

function MonController($http) {
  // ...
  $scope.shortDescription = function(description) {
    return description.substring(0, 100);
  };
}

Pas terrible de laisser cette logique dans notre controller. Face à ce problème, différents points de vues s’affrontent et il est parfois difficile de s’y retrouver.

La logique au sein du modèle

Une première solution serait peut-être d’implémenter notre logique justement au niveau de notre modèle en faisant quelque chose du style :

function Article (data) {

  this.title = data.title;
  this.description = data.description;
  // ...
}

Article.getShortDescription = function() {
  return this.description.substring(0, 100);
}

Ou plus simplement avec un angular.extend par exemple :

function Article (data) {

  angular.extend(this, {
    title: "",
    description: "",

    getShortDescription: function() {
      return this.description.substring(0, 100);
    }
  }, data);
}

Pour rendre ce bout de code exploitable et injectable proprement, il suffit d’utiliser une nouvelle Factory :

angular.module('monModule').factory('Article', function() {

  var Article = function (data) {
    angular.extend(this, {
      title: "",
      description: "",

      getShortDescription: function() {
        return this.description.substring(0, 100);
      }
    }, data);
  }

  return Article;
}

Il nous suffit alors de modifier notre service de récupération d’un article pour faire quelque chose de ce type :

angular.module('monModule').factory('ArticlesService', function($http, Article) {

  return {
    getArticle: function(id) {

      return $http.get('url/vers/mon/api/articles/' + id).then(function(articleData) {
        return new Article(articleData.data);
      });
    }
  }
}

Et depuis un controller par exemple :

angular.module('monModule').controller('ArticleController', function(ArticlesService) {

  ArticlesService.getArticle(100).then(function(article) {
    $scope.shortDescription = article.getShortDescription();
  }).then(null, function(article) {
    // handle error
  };
}

Une autre approche : les filtres Angular

Certains militent contre le besoin de créer des services spécifiques pour représenter un modèle. L’argument qui prévaut est le suivant :

Your model lives on the server

Grosso modo, aucune logique business ne doit être implémentée au sein d’un projet Angular et la seule logique qui appartient au front, est la logique de présentation.

L’auteur de cet article préfère traiter notre problème via l’utilisation d’un filtre Angular.

A filter formats the value of an expression for display to the user. They can be used in view templates, controllers or services and it is easy to define your own filter.

Pour répondre différemment à notre problème on peut alors implémenter un filtre de la manière suivante :

angular.module('monModule', []).filter('summerizeArticle', function() {

  return function(article) {
    return article.description.length > 100 ? article.description.substring(0, 100) + "..." : article.description;
  };
});

Et si la problématique est suffisamment générique pour être valable hors du contexte d’un article, on peut alors modifier notre filtre de la sorte :

angular.module('monModule', []).filter('summerize', function() {

  return function(input) {
    return input.length > 100 ? input.substring(0, 100) + "..." : input;
  };
});

Quelques ressources intéressantes pour aller plus loin :