AngularJS-Shield-large
AngularJS

AngularJS expliqué en patrons de conception, épisode 4 : Le Scope

Dans cet épisode, nous allons voir comment les patrons de conception traditionnels sont implémentés par le scope d’AngularJS.

Cet article fait partie d’une série d’articles consacrés à l’utilisation des patrons de conceptions par et dans les composants d’AngularJS.

Observer

Ce patron établit une relation un à plusieurs entre des objets, où lorsqu’un objet change, plusieurs autres objets sont avisés du changement. Dans ce patron, un objet le sujet tient une liste des objets dépendants les observateurs qui seront avertis des modifications apportées au sujet. Quand une modification est apportée, le sujet emmet un message aux différents observateurs.

Observer

Il existe deux façon pour communiquer entre les scopes (ou les contrôleurs) dans une application AngularJS. La première consiste à appeler les méthodes des parents depuis les scopes enfants. Ceci est rendu possible car le scope enfant hérite du prototype du scope parent, comme expliqué dans les sections précédentes (voir la section sur le Scope) ; Cette première façon de faire autoriser un sens de communication : de l’enfant vers le parent. Cependant, il est souvent nécessaire d’appeler des méthodes d’un scope enfant, ou notifier un scope enfant suite à un événement déclenché dans le contexte du parent. AngularJS propose de base un patron Observer, qui permet ce genre de communication.

Un autre cas d’usage de ce patron : lorsque plusieurs scopes sont concernés par un certain événement, qui est déclenché dans un autre contexte. Ceci permet en effet de découpler les scopes.

Chaque $scope dans AngularJS possède trois méthodes : $on, $emit et $broadcast. La méthode  $on accepte un topic et une fonction de callback. Cette callback peut être considérée comme un observer, un objet implémentant l’interface Observer:

function ExampleCtrl($scope) {
  $scope.$on('event-name', function handler() {
    //body
  });
}

De cette manière, le $scope courant souscrit à un événement de type event-name. Lorsque cet événement est déclenché à n’importe quel niveau du $scope courant, que ce soit au niveau de ses parents ou ses enfants, la fonction handler sera invoquée.

Les méthodes $emit et $broadcast sont utilisées pour déclencher et propager les événements, vers $scope parents pour $emit et $scope enfants pour $broadcast. Voici un exemple :

function ExampleCtrl($scope) {
  $scope.$emit('event-name', { foo: 'bar' });
}

Dans cet exemple, le $scope déclenche l’événement event-name et le propage vers tous les $scope parents. Ce qui signifie que chaque $scope parent ayant souscrit à event-name sera notifié et sa fonction handler sera appelée. La méthode $broadcast fonctionne de la même manière, à la différence près qu’elle propage l’événement vers tous les $scope enfants.

Chaque $scope a la possibilité de souscrire plusieurs actions à un même événement. Dit autrement, il peut associer plusieurs observer à un même événement.

Ce patron est également connu sous le nom de publish/subscribe (ou producteur/consommateur).

Pour les bonnes pratiques concernant ce patron, voir Le patron Observer en tant que Service externe

Chaîne de responsabilité

le patron de conception Chaîne de responsabilité permet à un nombre quelconque de classes d’essayer de répondre à une requête sans connaître les possibilités des autres classes sur cette requête.

Chain of Responsibilities

Comme cité précédemment, les $scopes dans AngularJS forment une hiérarchie connue sous le nom de chaîne de scope. Certains d’entre eux peut être isolés, ce qui signifie qu’ils n’héritent pas des prototypes de leurs parents ; cependant, chaque $scope connait son parent direct grâce à la propriétés $parent qu’il contient.

Lorsque les méthode $emit et $broadcast sont appelées, les $scope deviennent alors comme un bus d’événement, ou plus précisément, une chaine de responsabilité. Une fois l’événement est a été déclenché, que ce soit vers les parents ou les enfants, chaque $scope peut :

  • traiter l’événement et le passer au $scope suivant dans la chaîne.
  • traiter l’événement et stopper sa propagation.
  • passer directement l’événement au $scope suivant sans le traiter.
  • stopper directement la propagation de l’événement.

Dans l’exemple suivant, nous pouvons constater que ChildCtrl déclenche un événement, qui est propager vers le haut de la chaîne. Chaque $scope parent – celui créé par ParentCtrl et l’autre créé par MainCtrl- traite l’événement en affichant dans la console "foo received". Si un $scopeconsidère qu’il doit stopper la propagation de cet événement, il doit appeler la méthode stopPropagation()sur l’événement en question.

Voici l’exemple de code :

myModule.controller('MainCtrl', function ($scope) {
  $scope.$on('foo', function () {
    console.log('foo received');
  });
});

myModule.controller('ParentCtrl', function ($scope) {
  $scope.$on('foo', function (e) {
    console.log('foo received');
  });
});

myModule.controller('ChildCtrl', function ($scope) {
  $scope.$emit('foo');
});

Les Handler figurant dans le diagramme UML correspondent aux différents $scope injectés dans les contrôleurs.

Command

Ce patron emboîte une demande dans un objet, permettant de paramétrer, mettre en file d’attente, journaliser et annuler des demandes.

Command

Avant de rentrer dans les explications de ce patron de conception, étudions comment AngularJS implémente le data biding (ou liaison de données).

Pour associer un modèle à une vue, nous utilisons la directive ng-bind, pour une liaison uni-directionnelle, et ng-model pour une liaison bi-directionnelle. Par exemple, si nous souhaitons que tous les changements du modèle soient reflétés dans la vue automatiquement :

<span ng-bind="foo"></span>

A chaque fois que le modèle foo subit un changement, le contenu de la balise span sera mis à jour automagiquement. Voici un exemple avec une expression AngularJS :

<span ng-bind="foo + ' ' + bar | uppercase"></span>

Dans cet exemple, le contenu de la balise span sera le résultat de la concaténation des valeurs des modèles foo et  bar. Mais que ce passe-t-il sous le capot ? Que fait AngularJS réellement ?

Chaque $scopepossède une méthode $watch. Lorsque le compilateur d’AngularJS traverse le DOM est rencontre une directive ng-bind, il créé un observateur ( watcher) basé sur l’expression rencontrée : ion foo + ' ' + bar | uppercase comme ceci :

$scope.$watch("foo + ' ' + bar | uppercase", function update() { /* body */ });

La callback update sera déclenchée à chaque fois que la valeur de l’expression vient à changer. Dans notre exemple, la callback met à jour le contenu de la balise span.

Voici un aperçu du début de l’implémentation de la méthode $watch:

$watch: function(watchExp, listener, objectEquality) {
  var scope = this,
      get = compileToFn(watchExp, 'watch'),
      array = scope.$$watchers,
      watcher = {
        fn: listener,
        last: initWatchVal,
        get: get,
        exp: watchExp,
        eq: !!objectEquality
      };
//...

Nous pouvons considérer l’objet watcher comme une commande. L’expression de la commande est évaluée à chaque itération de la boucle de digestion ou "$digest". Lorsque AngularJS détecte un changement dans l’expression, la fonction listener est invoquée. La commande watcherencapsule tout le nécessaire pour observer une expression donnée, et déléguer l’exécution de la commande à la fonction listener, le receiver dans le diagramme UML. Le  $scope quant à lui est l’équivalent du Client et la boucle de $digest est le Invoker.

Dans les prochains épisodes…

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

Standard