Publié par

Il y a 6 ans -

Temps de lecture 8 minutes

Libérer le potentiel des directives AngularJS

Suite à l’article d’initiation aux directives AngularJS, celui-ci a pour objectif de parcourir des concepts plus avancés de l’API des directives :

  • La transclusion ;
  • La communication entre directives.

Pour aborder ces différentes notions, nous allons poursuivre le raffinement de l’implémentation d’un gestionnaire d’onglets.

À la fin de l’article précédent, notre gestionnaire d’onglets s’utilisait de cette manière :

<div tabs="[{ title: 'Tab 1', content: 'Content 1' },{ title: 'Tab 2', content: 'Content 2' }]"></div>
  • L’attribut tabs permet d’identifier la directive.
  • La valeur de cet attribut contient la liste des onglets avec leur titre et leur contenu.

La directive génère la structure HTML suivante :

<ul class="nav-tabs">
  <li class="active">Tab 1</li>
  <li>Tab 2</li>
</ul>
<div class="tab-content">
  <div class="active">Content 1</div>
  <div>Content 2</div>
</div>
  • nav-tabs regroupe les titres des onglets ;
  • tab-content regroupe les contenus des onglets ;
  • active permet d’identifier le titre et le contenu de l’onglet sélectionné.

Transclusion

Malgré ce terme étrange, la transclusion est simplement, d’après wikipédia, l’inclusion d’un document dans un autre par référence. Dans le contexte d’AngularJS, une directive utilisant la transclusion est une directive encapsulant un template HTML, au sein duquel l’utilisateur peut insérer son propre HTML.

Grâce à cette notion, nous pouvons revoir notre gestionnaire d’onglet pour l’utiliser de cette façon :

<div tabs titles="['Tab 1', 'Tab 2']">
    <div>Content 1</div>
    <div>Content 2</div>
</div>

Deux étapes sont nécessaires pour que notre directive utilise la transclusion, tout d’abord mettre à true la propriété transclude de la configuration :

angular.module('article').directive('tabs', function($parse) {
  return {
    template: '',
    transclude: true,
    link: function(scope, element, attrs) {
    scope.titles = $parse(attrs.titles)(scope);

    }
  }
});

Il faut ensuite définir quel élément du template doit recevoir le HTML « transcludé », via la directive ng-transclude. Pour notre gestionnaire d’onglets, nous voulons que les contenus des onglets soient transcludés dans leur conteneur :

<ul class="nav-tabs">
  <li ng-repeat="title in titles">
   {{ title }}
  </li>
</ul>
<div class="tab-content" ng-transclude></div>

Après la phase de compilation, le HTML de notre gestionnaire d’onglets sera donc :

<div tabs titles="['Tab 1', 'Tab 2']">
  <ul class="nav-tabs">
    <li ng-repeat="title in titles">
      {{ title }}
    </li>
  </ul>
  <div class="tab-content" ng-transclude>
    <div>Content 1</div>
    <div>Content 2</div>
  </div>
</div>

Cependant, la transclusion introduit la contrainte suivante : le scope (ou contexte d’évaluation) appliqué au HTML transcludé n’est pas lié au scope de la directive.

En effet, le scope du HTML transcludé n’hérite pas du scope de la directive mais de celui du parent de la directive. Les éléments ajoutés au scope par la directive ne sont donc pas disponible dans le scope du HTML transcludé.

Par exemple, prenons une directive utilisant la transclusion et définissant une propriété value sur son scope :

link: function (scope) {
 scope.value = "only available inside directive";
}

Cette propriété n’est accessible ni dans le scope parent ni dans celui du HTML transcludé :

Capture d’écran 2013-11-12 à 22.07.06

Voir le détail de cet exemple.

Cette contrainte permet de rendre la directive facilement réutilisable en découplant son modèle de celui du HTML transcludé (Plus d’infos).

Implémentons la fonction de link de notre gestionnaire d’onglets :

link: function(scope, element, attrs) {
  scope.titles = $parse(attrs.titles)(scope);

  scope.selectTab = function(index) {
    element.find(".tab-content div").eq(scope.activeIndex).removeClass('active');
    element.find(".tab-content div").eq(index).addClass('active');
    scope.activeIndex = index;
  };
  scope.isSelected = function(index) {
   return index == scope.activeIndex;
  };

  scope.selectTab(0);
}

Augmentons ensuite le template de la directive pour implémenter ce modèle :

<ul class="nav-tabs">
  <li ng-repeat="title in titles"
      ng-class="{active: isSelected($index)"
      ng-click="selectTab($index)">
    {{ title }}
  </li>
</ul>
<div class="tab-content" ng-transclude></div>

La variable $index est fournie par la directive ng-repeat et représente l’index de l’élément dans le tableau qui est parcouru. Voir cette directive en action.

La transclusion résout le problème soulevé à la fin de l’article précédent. En effet, lorsque l’HTML est stocké en mémoire, AngularJS l’échappe automatiquement à l’affichage. Ici, le HTML est seulement inséré dans le template défini par la directive.

Cette implémentation présente tout de même certains inconvénients :

  • Le titre et le contenu d’un onglet sont découplés.
  • Le modèle manipule directement la classe active du contenu de l’onglet.

Communication entre directives

Une directive peut déclarer un contrôleur pour exposer son modèle. Les autres directives peuvent alors s’injecter ce contrôleur et interagir avec lui.

Utilisons cette notion pour améliorer notre gestionnaire d’onglets. La stratégie va être d’utiliser deux directives :

  • tabs : chargée de gérer le modèle des onglets ;
  • tab : chargée de gérer un onglet particulier.

Ces directives vont s’utiliser de cette façon :

<div tabs>
    <div tab title="Tab 1" active="true">Content 1</div>
    <div tab title="Tab 2">Content 2</div>
</div>

Tout d’abord la directive tab :

angular.module('article').directive('tab', function() {
  return {
    transclude: true,
    replace: true,
    require: '^tabs',
    scope: true,
    template: '<div ng-class="{active: isActive()}" ng-transclude></div>',
    link: function (scope, element, attrs, tabsController) {

    }
  };
});
  • replace : Lorsqu’AngularJS va compiler notre directive, le balise utilisée pour déclarer notre directive va être remplacée par son template. Par défaut, le template remplace le contenu de la balise.
  • require : s’utilise avec le nom de la directive qui fournit le controller que l’on souhaite s’injecter. Le caractère ‘^’ permet de ne pas chercher le controller uniquement sur le même élément mais également sur les éléments parents (plus d’infos).
  • tabsController : le contrôleur est disponible en quatrième argument de la fonction de link.

Notre directive tab sera donc remplacée par son template à la compilation. Celui-ci encapsule le contenu de l’onglet et permet d’ajouter la classe active si l’onglet courant est sélectionné.

Nous allons tout d’abord enregistrer l’onglet courant auprès du contrôleur de tabs, celui-ci nous informera lorsque l’onglet sera sélectionné :

link: function (scope, element, attrs, tabsController) {
  var tab = tabsController.registerTab(attrs.title, attrs.active);
  scope.isActive = function () {
    return tabsController.isActive(tab);
  }
}

Passons à l’implémentation de la directive tabs, le contrôleur est une autre propriété de la configuration de la directive :

angular.module('article').directive('tabs', function() {
  return {
    transclude: true,
    template: '',
    controller: ['$scope', function(scope) {

    }]
  };
});

La déclaration d’un contrôleur de directive est semblable à la déclaration d’un contrôleur classique et on peut lui injecter le scope de l’élément courant. Le contrôleur peut donc enrichir le scope de l’élément courant et exposer des méthodes pour les autres directives. D’où l’implémentation :

function (scope) {
  var tabs = [];
  var activeTab;

  this.isActive = function (tab) {
    return tab == activeTab;
  };
  this.selectTab = function(tab) {
      activeTab = tab;
  };

  this.registerTab = function (title, isActive) {
    var tab = { title: title };
    tabs.push(tab);
    if (isActive) activeTab = tab;
    return tab;
  };

  scope.tabs = tabs;
  scope.isActive = this.isActive;
  scope.selectTab = this.selectTab;
}

Le template de la directive est similaire à celui de l’étape précédente :

<ul class="nav nav-tabs">
  <li ng-repeat="tab in tabs"
      ng-class="{active: isActive(tab)}"
      ng-click="selectTab(tab)">
    {{ tab.title }}
  </li>
</ul>
<div class="tab-content" ng-transclude></div>

Voir ces directives en action.

Conclusion

Les composants qui émergent au long du cycle de vie d’une application évoluent en fonction des besoins. AngularJS permet de créer rapidement des éléments avec du comportement mais fournit aussi des outils pour les raffiner et les transformer en composants robustes.

L’orientation testabilité du framework facilite l’évolution de nos composants. Si vous êtes allés voir les plunkers des différentes étapes, vous avez pu constater que les tests d’acceptance sont les mêmes pour les trois étapes et que seuls les tests unitaires diffèrent. Le code et les tests des étapes des deux articles sont également disponibles sur mon github.

Le guide et l’API des directives contiennent d’autres notions qui n’ont pas été abordées dans cet article, il y a de fortes chances que vous y trouviez le nécessaire pour implémenter de façon élégante vos composants HTML !

Publié par

Commentaire

5 réponses pour " Libérer le potentiel des directives AngularJS "

  1. Publié par , Il y a 6 ans

    Petite coquille au haut du paragraphe:
    « La directive génère la structure HTML suivante :

    Tab 1
    Tab 1

    Content 1
    Content 2

     »
    Le 2eme est un « Tab 2 » et non un « Tab 1 ».
    Cordialement,
    Xavier.

  2. Publié par , Il y a 6 ans

    C’est corrigé !
    Merci,
    Bastien.

  3. Publié par , Il y a 6 ans

    Hello,

    Merci pour cet article. Mais histoire d’être sur d’avoir compris, on utilise un controller dans une directive quand on veux pouvoir
    1. Gérer un état interne
    2. Faire communiquer les directives de même type / directives enfants

    J’ai souvent lu que pour faire dialoguer des directive, il était recommandé de passé par un service, dans quel cas cela peut il être intéressant ?

  4. Publié par , Il y a 6 ans

    Salut,

    1. On peut gérer un état interne dans une directive sans passer par un controller. Utiliser un controller permet d’exposer des méthodes pour modifier cet état.
    2. La communication par controller ne peut se faire qu’entre directives déclarées sur la même balise HTML ou entre directives déclarées sur la même hiérarchie de noeud DOM via la syntaxe ^.

    Un service peut également être utilisé pour communiquer entre directives mais présente pour moi deux inconvénients :
    1. Les manipulations DOM dans un service ne sont pas considérées commune une bonne pratique alors qu’elles sont tout à fait appropriées dans un controller de directive.
    2. Un service est un singleton et donc toutes les directives s’injectant ce service partageront la même instance. Dans le cas du controller, une instance est définie pour chaque directive. Un service me semble plus intéressant pour agir sur toutes les instances d’une directive alors qu’un controller est plus adapté pour agir sur une instance en particulier.

  5. Publié par , Il y a 6 ans

    Merci pour ces explications !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.