Le framework MVC côté client de Google, AngularJS, a le vent en poupe grâce aux fonctionnalités qu’il fournit, son design pensé pour la testabilité et sa courbe d’apprentissage. Cependant, certains éléments du framework sont plus difficiles à appréhender, c’est le cas des directives.
La directive est l’un des composants les plus complexes du framework mais aussi l’un des plus puissants. Les directives sont en charge des manipulations du DOM et peuvent être utilisées pour étendre le HTML en créant ses propres DSL.
Dans ce premier article, nous allons parcourir une partie de l’API des directives via différentes implémentations d’un gestionnaire d’onglets :
- avec les directives fournies par le framework ;
- avec une implémentation style jQuery ;
- en séparant le modèle et la vue.
Utiliser les directives fournies par AngularJS
Grâce aux différentes directives fournies par le framework, il est tout à fait possible de créer un gestionnaire d’onglets sans avoir besoin de créer sa propre directive.
En partant d’une structure HTML simple :
<ul> <li>Onglet 1</li> <li>Onglet 2</li> </ul> <div> <div>Contenu 1</div> <div>Contenu 2</div> </div>
Le comportement attendu :
- Au clic sur l’onglet 1, j’affiche le contenu 1.
- Au clic sur l’onglet 2, j’affiche le contenu 2.
peut s’implémenter très simplement via un booléen représentant l’état de notre gestionnaire d’onglets :
<ul ng-init="firstTabSelected = true"> <li ng-click="firstTabSelected = true">Onglet 1</li> <li ng-click="firstTabSelected = false">Onglet 2</li> </ul> <div> <div ng-show="firstTabSelected">Contenu 1</div> <div ng-hide="firstTabSelected">Contenu 2</div> </div>
Et voilà, notre gestionnaire d’onglets (enfin, de deux onglets) est opérationnel en utilisant seulement les directives élémentaires du framework. La syntaxe déclarative permet de comprendre son comportement sans trop de difficulté. Voir le gestionnaire d’onglets en action.
Cependant, ce style d’implémentation comporte un certain nombre d’inconvénients :
- L’ajout d’onglets supplémentaires va devenir rapidement très laborieux et la lisibilité du HTML va également se dégrader.
- Si nous souhaitons réutiliser ce composant à un autre endroit de notre application, nous allons devoir dupliquer cette logique.
Instrumentaliser le HTML à la jQuery
Pour pallier les inconvénients de l’implémentation précédente, nous pouvons en élaborer une nouvelle à base de manipulation du DOM comme nous l’aurions fait avec jQuery.
Déclarons une directive tabs
chargée de gérer nos onglets :
angular.module('article').directive('tabs', function() { return function link(scope, tabsElement) { }; });
Pour utiliser cette directive, il nous suffit d’ajouter l’attribut tabs
sur la balise de notre choix. Toutes les opérations que nous souhaitons effectuer sur le DOM doivent être déclarées dans la fonction link
. Cette fonction s’exécute à chaque fois qu’AngularJS rafraîchit la balise où la directive est présente. Nous l’utilisons ici avec deux paramètres :
scope
: Le scope de l’élément courant (plus d’infos).
tabsElement
: Un wrapper jQuery de l’élément sur lequel a été déclaré la directive.
Rajoutons les éléments nécessaires à notre HTML pour pouvoir démarrer notre implémentation :
<div tabs> <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> </div>
Les classes nav-tabs
et tab-content
vont nous servir à identifier respectivement les onglets et leur contenu.
La classe active
nous indiquera l’onglet et le contenu sélectionné.
Implémentons la fonction de link
:
angular.module('article').directive('tabs', function() { return function link(scope, tabsElement) { tabsElement.find(".nav-tabs li").on('click', selectThisTab); function selectThisTab() { var tab = angular.element(this); var tabContent = tabsElement.find(".tab-content div").eq(tab.index()); tabsElement.find(".active").removeClass('active'); tab.addClass('active'); tabContent.addClass("active"); } }; });
Voir cette directive en action.
Cette implémentation est très proche de ce que l’on aurait pu faire en jQuery mais cela n’est pas anodin. En effet, il faut garder à l’esprit qu’AngularJS s’appuie sur jQuery, ou tout du moins une version simplifiée si l’utilisateur ne fournit pas cette librairie. (plus d’infos)
Cette solution présente tout de même certains inconvénients :
- Si la structure HTML change, il faudra revoir l’implémentation.
- Le HTML ne traduit pas la relation entre l’onglet et son contenu.
Séparer le modèle et la vue
L’implémentation précédente est fortement couplée au HTML, cependant les concepts d’AngularJS tendent à forcer le développeur à séparer le modèle et la vue pour faciliter la testabilité et la réutilisabilité du code. Les directives n’échappent pas à ce principe en permettant d’encapsuler du comportement métier agissant sur un template.
Pour notre gestionnaire d’onglets, modélisons un onglet par un objet présentant un titre et un contenu.
{ title: 'Titre', content: 'Contenu' }
Réutilisons l’attribut tabs
pour identifier notre nouvelle directive et passons lui un tableau d’onglets :
<div tabs="[{ title: 'Tab 1', content: 'Content 1' },{ title: 'Tab 2', content: 'Content 2' }]"></div>
Ce div
est pour l’instant vide mais la directive sera chargée de le remplir.
Définissons le squelette de la directive tabs
:
angular.module('article').directive('tabs', function() { return { template: '', link: function(scope, tabsElement, attributes) { scope.tabs = $parse(attributes.tabs)(scope); } } })
Pour cette directive, nous ne renvoyons pas la fonction de link
mais un objet contenant la configuration de notre directive :
- Template : HTML qui sera généré lors de la compilation de la directive.
- Link : propriété stockant la fonction de
link
.
Nous pouvons récupérer le tableau d’onglets qui a été fourni à notre directive via :
- Attributes : objet permettant d’accéder aux attributs de l’élément sur lequel a été déclaré la directive ;
- $parse : service permettant de convertir une expression (plus d’infos).
Définissons le modèle métier de notre gestionnaire d’onglet, il peut être réduit à :
- une liste d’onglets ;
- un de ces onglets est sélectionné ;
- lorsque je sélectionne un onglet, il devient sélectionné.
Implémentons ce modèle dans la fonction de link
:
link: function(scope, tabsElement, attributes) { scope.tabs = $parse(attributes.tabs)(scope); var selectedTab = scope.tabs[0]; scope.isSelected = function(tab) { return selectedTab == tab; } scope.selectTab = function(tab) { selectedTab = tab; } }
Tous les objets et fonctions ajoutés à l’objet scope
seront accessibles depuis la vue.
Construisons maintenant le template qui réagira aux changements du modèle :
<ul class="nav-tabs"> <li ng-repeat="tab in tabs" ng-class="{'active': isSelected(tab)}" ng-click="selectTab(tab)"> {{ tab.title }} </li> </ul> <div class="tab-content"> <div ng-repeat="tab in tabs" ng-class="{'active': isSelected(tab)}"> {{ tab.content }} </div> </div>
La structure HTML utilisée dans les étapes précédentes a été enrichie avec certaines directives fournies par AngularJS :
- ng-repeat permet d’itérer sur les onglets pour afficher les titres et les contenus.
- ng-class permet d’ajouter la classe
active
uniquement pour l’onglet sélectionné. - ng-click permet de changer l’onglet sélectionné au clic sur le titre.
Il suffit d’ajouter ce HTML dans la propriété template
de la configuration de la directive pour que celle-ci soit opérationnelle. Voir cette directive en action.
Le principal inconvénient de cette solution est que le contenu d’un onglet ne peut pas contenir d’HTML. En effet, lorsque l’on stocke du HTML dans notre modèle, celui-ci est automatiquement échappé par AngularJS à l’affichage. Pour contourner ce comportement, nous pouvons utiliser la directive ng-bind-html mais nous verrons dans le prochain article comment ne pas en arriver là en évitant de stocker du HTML dans notre modèle.
Conclusion
Dans ce premier article, nous avons vu que :
- AngularJS nous fournit des éléments pour déclarer très simplement du comportement dans la vue.
- Les directives permettent d’encapsuler un modèle métier et la vue sur laquelle il s’applique.
Le code et les tests des trois étapes sont également disponibles sur mon github.
Dans le prochain article, nous poursuivrons le raffinement de notre gestionnaire d’onglets en parcourant plus profondément l’api des directives.