AngularJS-Shield-large
AngularJS

AngularJS expliqué en Patrons de conception, épisode 1

Il est souvent plus simple d’apprendre une nouvelle technologie en essayant de retrouver des concepts que l’on maitrise, et voir comment ils y sont implémentés.

Le but de cette série d’articles est de décrire comment les différents patrons de conception logiciel et aussi ceux d’architecture sont implémentés par (et aussi dans) AngularJS ou n’importe quelle Single Page Application codée avec AngularJS.

Disclaimer

  1. Cette série d’articles n’a pas pour vocation d’expliquer en détails les principes d’architecture logiciels ou de la POO.
  2. Cette série d’articles est ma contribution en francais aux travaux menés par Minko Gechev autours de l’utilisation des patrons de conceptions dans AngularJS. Vous pouvez retrouver la totalité de cette contribution sur mon compte Github.

Notez que la série d’articles publiée sur ce blog apporte des compléments d’informations — lorsqu’il y’en a besoin — par rapport au document publié sur Github. Le but aussi est de vous inciter à poster vos commentaires, je suis sûr que vous aurez des choses à dire vous aussi ^^

Introduction

Cette série d’articles commence par un rappel du framework AngularJS. Dans cette vue d’ensemble, nous décrivons les différents composants du framework – les directives, filtres, contrôleurs, services et scope. La second section liste et décrit les différents patrons de conception qui sont implémentés par le framework. Ces patrons sont groupés par les composants AngularJS dans lesquels ils sont implémentés. Lorsqu’un patron est utilisé dans plusieurs composants, cela sera mentionné explicitement.

La dernière section contient quelques patrons d’architecture souvent rencontrés dans la plupart des applications AngularJS.

Rappel d’AngularJS

AngularJS est un framework JavaScript développé par Google. Il propose de solides bases pour le développement d’application mono-page (Single-Page Application – SPA).

Une SPA est une application qui une fois chargée, ne requiert pas un rechargement total de la page lorsque l’utilisateur interagit avec elle. Cela signifie que toutes les ressources de l’application (données, templates, scripts et styles) devrait être chargées lors de la première requête, voire mieux, à la demande.

Le constat étant que les applications SPA de type CRUD ont des caractéristiques communes, AngularJS fournit de base tout un ensemble de mécanismes et fonctionnalités tels que :

  • le binding bi-directionnel
  • l’injection de dépendances
  • la séparation des préoccupations
  • les tests
  • une abstraction

La séparation des préoccupations est rendue possible en divisant une application AngularJS en plusieurs composants, tels que :

  • les vues partielles
  • les contrôleurs
  • les directives
  • les services
  • les filtres

Ces composants peuvent être groupés dans des modules, qui offre une abstraction supplémentaire pour mieux gérer la complexité des SPA. Chaque composant encapsule une partie bien spécifique et bien définit de la logique de l’application.

Les vues partielles

Les vues partielles sont de l’HTML. Elles peuvent contenir des expressions AngularJS au sein des éléments HTML ou des attributs. L’une des différences majeures entre AngularJS et les autres frameworks réside dans le fait que les templates AngularJS ne sont pas un format intermédiaire qui devrait être transformé en HTML (ce qui est le cas de mustache.js ou handlebars, par exemple).

Initialement, chaque SPA charge le fichier index.html Dans le cas d’AngularJS, ce fichier contient du code HTML standard enrichit avec des éléments, attributs et commentaire dont le but est de configurer et de démarrer l’application. Chaque interaction de l’utilisateur avec l’application requiert simplement le chargement d’une vue partielle ou le chargement d’un état de l’application, à travers le biding de données fournit par AngularJS.

Exemple de vue partielle

<html ng-app>
 <!-- La balise body est enrichit avec la directive ng-controller -->
 <body ng-controller="MyController">
   <input ng-model="foo" value="bar">
    <!-- la balise button est enrichit avec la directive ng-click 
          et l'expression 'buttonText' est entourée par des "{{ }}" -->
   <button ng-click="changeFoo()">{{buttonText}}</button>
   <script src="angular.js"></script>
 </body>
</html>

Avec les expressions, les vues partielles définissent quelles actions doivent traiter les actions de l’utilisateur. Dans l’exemple précédent, la valeur de l’attribut ng-click précise que la méthode changeFoo du scope courant sera invoquée.

Les contrôleurs

Les contrôleurs d’AngularJS sont des fonctions JavaScript qui gèrent les interactions de l’utilisateur avec l’application web (par exemple, les événements de la souris, des touches claviers…etc), en attachant des méthodes au scope. Toues les dépendances externes d’un contrôleur sont fournit via le mécanisme d’ID ou injection de dépendances d’AngularJS. Les contrôleurs ont également pour but de fournir le modèle à la vue en attachant des données dans le scope. Nous pouvons voir ces données comme des modèle vue (view model)

function MyController($scope) {
  $scope.buttonText = 'Cliquer moi pour changer foo !';
  $scope.foo = 42;

  $scope.changeFoo = function () {
    $scope.foo += 1;
    alert('Foo a été changé');
  };
}

Par exemple, si nous associons le contrôleur de l’exemple ci-dessus avec la vue de l’exemple dans la section précédente, l’utilisateur va pouvoir interagir avec l’application de différentes manières :

  1. Changer la valeur de foo en saisissant une valeur dans le champ de saisie. Ceci va immédiatement refléter la valeur de foo grâce au mécanisme de biding bi-directionnel.
  2. Changer la valeur de foo en cliquant sur le bouton, qui aura le libellé Cliquer moi pour changer foo !

Tous les éléments, attributs, commentaires ou classes CSS personnalisés permettant d’enrichir l’HTML sont appelés des directives AngularJS.

Le scope

Dans AngularJS, le scope est un objet JavaScript qui est exposé au vues partielles. Le scope peut contenir plusieurs propriétés – primitives, objets ou méthodes. Toutes les méthodes attachées au scope peuvent être invoquées en évaluant l’expression AngularJS à l’intérieur de la vue associée au scope en question, ou simplement via un appel direct à la méthode par un composant donné. En utilisant les directives adéquates, les données attachées au scope peuvent être attachées (data-bound) de telle sorte que chaque changement dans la vue met à jour – immédiatement et automatiquement – une propriété du scope ; ainsi que chaque changement d’une propriété du scope est immédiatement reflété dans la vue.

Une autre caractéristique très importante des scopes réside dans le fait que les scopes sont hiérarchisés en suivant le modèle de la chaîne des prototypes de JavaScript (excepté les scopes isolés). De cette manière, chaque scope fils a la possibilité d’invoquer des méthodes de ses parents, puisque ces derniers sont des propriétés directes ou indirectes de son prototype.

L’héritage des scopes est illustré dans l’exemple suivant :

<div ng-controller="BaseCtrl">
  <div id="child" ng-controller="ChildCtrl">
    <button id="parent-method" ng-click="foo()">méthode du parent</button>
    <button ng-click="bar()">méthode du fils</button>
  </div>
</div>
function BaseCtrl($scope) {
  $scope.foo = function () {
    alert('Base foo');
  };
}

function ChildCtrl($scope) {
  $scope.bar = function () {
    alert('Child bar');
  };
}

Le contrôleur ChildCtrl est associé à l’élément div#child, mais puisque le scope injecté par childCtrl hérite le prototype du scope de son parent (celui injecté par le contrôleur BaseCtrl). Ceci fait que la méthode foo est accessible par button#parent-method.

Les directives

Dans AngularJS, les directives sont l’endroit où toutes les manipulations du DOM doivent être implémentées. Lorsque vous devez manipuler le DOM, vous devez créer une directive ou réutiliser celles qui existent.

Chaque directive possède un nom et une logique associée. Dans le cas le plus simple, une directive contient seulement un nom et une définition de la fonction postLink, qui encapsule toute la logique requise pour la directive. Pour les cas les plus complexes, une directive peut contenir d’autres propriétés tels que :

  • un template
  • une fonction compile
  • une fonction link
  • etc…

Pour utiliser une directive dans une vue, il suffit de référencer son nom. Par exemple :

myModule.directive('alertButton', function () {
  return {
    template: '<button ng-transclude></button>',
    scope: {
      content: '@'
    },
    replace: true,
    restrict: 'E',
    transclude: true,
    link: function (scope, el) {
      el.click(function () {
        alert(scope.content);
      });
    }
  };
});
<alert-button content="42">Cliquer moi</alert-button>

Dans cet exemple, la balise <alert-button></alert-button> sera remplacé par la balise button. Lorsque l’utilisateur cliquera sur ce bouton, le message 42 sera affiché.

Nous n’irons pas plus loin dans les explications des directives. Ce n’est pas le but de ce papier.

Les filtres

Dans AngularJS, les filtres sont responsables d’encapsuler toute la logique nécessaire pour formater des données. Souvent, les filtres sont utilisés au sein des vues, mais il est également possible de les appeler dans des contrôleurs, directives, services ainsi que d’autres filtres grâce à l’injection de dépendances.

Voici une définition d’un filtre dont le rôle est de transformer une chaîne de caractères en majuscule :

myModule.filter('uppercase', function () {
  return function (str) {
    return (str || '').toUpperCase();
  };
});

Ce filtre peut être utilisé au sein d’une vue en utilisant le symbole | d’UNIX :

<div>{{ name | uppercase }}</div>

Au sein d’un contrôleur, le filtre peut être utilisé de cette façon :

function MyCtrl(uppercaseFilter) {
  $scope.name = uppercaseFilter('foo'); //FOO
}

Les services

Dans AngularJS, les services sont responsables d’accueillir la logique métier des composants, la logique de persistance, les appels XHR, WebSockets, etc. Lorsque le contrôleur devient trop gros, le code superflu et répétitif devrait être déplacé dans un service.

myModule.service('Developer', function () {
  this.name = 'Foo';
  this.motherLanguage = 'JavaScript';
  this.live = function () {
    while (true) {
      this.code();
    }
  };
});

Le service peut être injecté dans n’importe quel composant, supportant l’injection de dépendances (les contrôleurs, d’autres services, les filtres, les directives).

function MyCtrl(Developer) {
  var developer = new Developer();
  developer.live();
}

Voilà ! Nous arrivons à la fin de ce premier épisode.

Dans les prochains épisodes…

Dans le prochain, nous allons voir comment les patrons de conception classiques sont utilisés par AngularJS.

Si vous le souhaitez, vous pouvez accéder directement à la section qui vous intéresse en cliquant sur l’un des liens ci-dessous (les liens seront mis à jour au fur et à mesure de la rédaction des articles correspondents) :

Standard