Les directives d'AngularJS

Publié le 17 juillet 2014 par Numa Claudel | front

Cet article est publié sous licence CC BY-NC-SA

Pour faire suite à mes précédents billets, aujourd’hui je vais vous parler des directives d’AngularJS. Je vous en avais parlé rapidement pour vous dire qu’AngularJS fournit déjà pas mal de directives pour les besoins les plus courants, mais que l’on peut compléter la liste.

Les directives sont les modules qui servent à la manipulation de DOM, à « binder » des évènements et définir leurs actions. Elles se traduisent par des composants HTML qui vont être réutilisables.

 Par où commencer

Partons de son rôle dans une vue, il faut définir comment ce composant doit être utilisé :

  • est ce qu’il va prendre place dans la vue comme une balise HTML
  • est ce qu’il va enrichir une autre balise
  • quels paramètres il va accepter

Il faut aussi définir son nom. Une directive se crée avec la méthode de module directive :

angular.module('monApp').directive('maDirective', function() {});

Cette directive se nomme donc maDirective, et vous constatez que son nom est en camelCase. C’est cette forme qu’il faudra utiliser. Une recommandation est à prendre en compte ici : il faut définir un préfixe pour identifier vos directives. Ceci permet d’éviter les collisions entre des directives de même nom de différentes sources. Par exemple chez Synbioz une directive est préfixée de synbioz ou sz. La précédente directive se nommerait alors szMaDirective.

A savoir que les directives seront disponibles dans les vues sous plusieurs formes : sz-ma-directive, sz:ma:directive, data-sz-ma-directive et x-sz-ma-directive. AngularJS nous laisse invoquer nos directives sous ces différentes syntaxes pour être compatible avec plusieurs validateurs HTML.

Une fois ces étapes terminées, on peut attaquer le développement du cœur de la directive. Il n’y a pas de limitation, ce peut être une fonctionnalité entièrement créée par nos soins, mais il est aussi possible d’englober et d’enrichir des composants externes à AngularJS. Notez d’ailleurs que pour utiliser une fonctionnalité extérieure, il est préférable de l’englober dans une directive. Les erreurs pourront ainsi être interceptées et manipulées dans l’application. Dans le cas contraire les erreurs éventuelles seront tout simplement ignorées.

Evènement sur le scroll

Pour commencer, une directive permettant de détecter lorsque l’utilisateur se trouve au bas d’une portion « scrollable ». On la nommera szBottomScroll. On veut donc utiliser cette directive sur un élément de type div ou autre. Nul besoin de paramètres, tout ce qu’on lui demande c’est de nous tenir informé si l’utilisateur atteint le bas de la portion.

var directives = angular.module('directives', []);
directives.directive('szBottomScroll', function() {
  return {
    link: function(scope, element, attrs) {
      var raw = element[0];
      var atBottom = false;
      element.on('scroll', function() {
        if (raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) {
          scope.$emit('atBottom');
          atBottom = true;
        } else {
          if(atBottom) {
            scope.$emit('notAtBottom');
            atBottom = false;
          }
        }
      });
    }
  };
});

Cette directive nous retourne un objet qui contient une méthode nommée link. Cette méthode est liée à l’élément sur lequel nous placerons la directive. On retrouve donc en paramètre le scope courant, c’est à dire le scope du controleur qui gère la portion de vue qui contient cet élément, l’élément lui-même, et les attributs pouvant se trouver sur cet élément.

Il ne reste plus qu’a faire comme d’habitude pour « binder » un évènement, ici c’est le scroll qui nous intéresse. Et pour finir on se sert de la méthode $emit du scope, pour informer notre application de la position de la barre de défilement.

La méthode $emit envoi un événement vers les couches de scope supérieures tandis que $broadcast l’envoi vers les couches inférieures. Ces évènements s’interceptent avec la méthode $on. Voici un exemple illustrant le fonctionnement.

Découper une vue

On peut rapidement se trouver avec un fichier de vue à rallonge, dans ce cas il est intéressant de pouvoir partitionner celle-ci, ce qui est très simple avec des directives.

var directives = angular.module('directives', []);
directives.directive('szMonPartial', function() {
  return {
    restrict : 'EA',
    controller : 'monPartialCtrl',
    templateUrl : 'chemin/vers/mon_partial.html'
  };
});

Ce coup-ci la directive retourne un objet qui défini la manière dont je vais pouvoir l’utiliser, ainsi que le controleur en charge de ce partial et le chemin du partial. restrict comprend 4 options :

  • E pour élément. Ce qui donne : <sz-mon-partial></sz-mon-partial>
  • A pour attribut (valeur par défaut). Ce qui donne : <div sz-mon-partial></div>
  • C pour classe. Ce qui donne : <div class="sz-mon-partial"></div>
  • M pour commentaire. Ce qui donne : <!-- directive: sz-mon-partial -->

Directive « tooltip »

Faisons une directive qui nous permettra de générer des tooltips. On va la nommer szTooltip, on veut pouvoir s’en servir comme d’une balise qui prendra le texte « inline » à afficher, le contenu du tooltip et un minimum de paramétrage au travers d’attributs.

var directives = angular.module('directives', []);
directives.directive('szTooltip', function() {
  return {
    restrict: 'E',
    replace: true,
    transclude: true,
    scope: {
      text: '@',
      orientation: '@'
    },
    template : '<span class="tooltip" ng-mouseenter="display()" ng-mouseleave="hide()">' +
               '  ' +
               '  <div ng-class="orientation" ng-transclude></div>' +
               '</span>',
    link: function(scope, element, attrs) {
      scope.display = function() {
        element.find('div').addClass('display');
      };

      scope.hide = function() {
        element.find('div').removeClass('display');
      };
    }
  };
});

L’option replace indique à la directive de remplacer l’élément sur lequel elle est placée par son template. transclude lui indique de prendre le contenu de l’élément pour l’insérer dans de la balise de son template qui intègre la directive ng-transclude. L’option scope permet de générer un scope propre à la directive, auquel on peut passer des arguments en ajoutant des attributs sur l’élément, dans notre cas text et orientation. L’option template permet de définir un template en chaine de caractères et nous avons déjà vu les options restrict et link.

La fonction link définit 2 fonctions display et hide qui seront appelées par les directives ng-mouseenter et ng-mouseleave respectivement. Vous noterez également la présence de la directive ng-class qui permet de définir des classes en fonction des propriétés qui lui sont passée.

Trois options s’offrent à nous pour le passage d’argument au scope d’une directive :

  • @ passe l’argument sous forme de string (comme dans notre cas).
  • = « data-bind » la variable passé avec le scope englobant
  • & permet de passer une fonction définie

On peut également renommer les paramètres passés : par exemple on pourrait avoir une variable myText égale à text avec cette notation myText: '=text'.

On peut maintenant se servir de cette directive de cette manière :

<sz-tooltip text="mon tooltip" orientation="bottom">
  Une info-bulle ...
  Voilà ...
</sz-tooltip>

Au sujet des sélecteurs

Pour finir, si vous avez besoin d’un sélecteur d’élément du DOM, sachez qu’AngularJS contient un mini jQuery : jqLite. Celui-ci intègre un sous-ensemble des fonctionnalités de jQuery les plus souvent utiles, tout en se voulant très léger. J’en ai d’ailleurs utilisé certaines tout au long de cet article.

Si toutefois la totalité de l’API de jQuery vous est nécessaire, en le chargeant avant AngularJS dans l’appel des scripts, Angular notera sa présence et se servira alors des fonctionnalités complètes de jQuery plutôt que celles de jqLite.

 Conclusion

Au premier abord le concept des directives peut-être perturbant. Le réflexe quand on se trouve dans le contrôleur est d’utiliser un sélecteur et de manipuler notre élément très simplement, ce qui est très embêtant pour le développement de tests. Cependant en se forçant à utiliser les directives on en voit très rapidement l’intérêt, qui est de ne pas polluer la partie logique de l’application avec la manipulation de DOM, mais aussi de pouvoir réutiliser ces portions de code.

Pour terminer, notez que beaucoup de directives existent déjà, je pense notamment au travail de la Team AngularUI. Et enfin, je vous invite à vous reporter au guide sur les directives.

L’équipe Synbioz.

Libres d’être ensemble.