Taille de texte par Defaut
Agrandir la Taille du Texte
Réduire la Taille du Texte
Options d'accessibilité
Informatique

Philosophie ∅-Contrôleur

Rine
12 octobre 2015
Pas de commentaires

 Expérience des contrôleurs

Plantons le décor, alors que j’étais tranquillement en train de coder à coup de {{ }} dans mes contrôleurs, je me suis dit :

« bon sang, ce bout de code est génial et tellement pratique, je vais l’utiliser ailleurs ».

Appelons-le Dédé.
Qu’à cela ne tienne, je copie/colle la fonction de $scope Dédé dans mon second contrôleur, ce qui ne me prend que 5 secondes grâce à mon bac+5 en copié/collé.

Mais, comme j’ai un peu la poisse les jours en –di, ça ne marche pas du premier coup donc je me rends compte que pour fonctionner, Dédé a besoin d’être initialisé comme il faut, et dépend d’autres variables qui ne sont (hélas) pas dans le $scope de mon second contrôleur… Ca n’est pas très grave car l’héritage de $scope peut me sauver… pour cette fois (si.vous.aimez.les.chaines). Ah, et n’oublions pas de rajouter la (ou les) divs HTML qui vont avec Dédé dans la seconde vue !! (Vous savez la div qui va tout décaler votre css déjà un peu olé-olé, ben c’est elle…)

Voilà ! Rien de sorcier, c’est beau, c’est frais, c’est comité et 2 semaines plus tard, vous avez pleins de petits Dédés dans vos vues et vos contrôleurs… Mais le temps passe et il est temps pour Dédé de passer à la V2 !

  • Réalisation N°1 : Pour que Dédé passe à la V2 il faudra modifier tous les Dédés (bon, y’aura qu’à le faire faire au stagiaire ça lui fera les pieds)… Mais où sont-ils passés ? Pas de panique, rapide CTRL+F sur le nom d’une fonction… 14 résultats trouvés ?! Oups…
  • Réalisation N°2 : Mine de rien, les Dédés ont tous pris leur indépendance de leur côté, et chacun y est allé de son petit comportement à lui, ajouté par-ci par-là au gré des évolutions (les vôtres, ou celles de vos compatriotes buveurs de café)… Au point qu’on ne peut même plus les reconnaître, voire qu’ils vont devenir tellement spécifiques qu’ils seront des nœuds à comportement one-shot potentiellement incompatibles avec les autres Dédés (Murphy quand tu nous tiens…)

 

Voilà, Dédé nous a montré une première limite de l’utilisation abusive de contrôleurs…

Il y en a d’autres, comme le fait de vite se perdre dans les méandres de l’héritage de $scope (on parle de données semi-globales car un contrôleur a accès à toute variable de scope de sa chaine d’héritage….).
On aura vite perdu le fil de notre donnée et il devient alors labyrinthique, voire rocambolesque de savoir d’où elle vient, quelles transformations elle a subi dans un de ses contrôleurs pères, et qui l’utilise et dans quel ordre… On est à la limite de l’inceste d’accès aux données (et c’est dégueu).
Renommer une variable par exemple deviendra vite cauchemardesque, car X contrôleurs peuvent en avoir besoin, tandis que Y autres contrôleurs vont y accéder pour la modifier… Bref, bonne chance !

Bon je pense qu’on aura compris pour la partie « bashage des contrôleurs », mais attention ! Je ne dis pas qu’ils sont le mal absolu, qu’il faut tout brûler… Ils peuvent encore convenir dans bien des cas ! Seulement c’est difficilement réutilisable et maintenable, et c’est dommage car c’est ce qu’on attend de notre code !

Si vous vous êtes reconnu dans cette intro, que vous chercher à mutualiser votre code, ou au moins certains de vos composants ou widgets…  Alors une solution pourrait s’imposer à vous, basculer vos contrôleurs vers des directives…

Petit rappel contrôleur/directive

Contrôleur :

Sans revenir sur les détails précis, on peut dire que le contrôleur sert à relier des services à leur vues en gérant des logiques un peu complexes d’affichage des données via une variable de $scope. Pas de gestion d’interaction très fin ici, car manipuler des éléments du DOM devient vite compliqué et est généralement considéré comme un anti-pattern.

Directive :

Ce sont des modules qui encapsulent une vue HTML (ou template) avec son modèle de données. La gestion de l’affichage et de l’interaction entre le modèle et les données peuvent être très fine grâce à la possibilité de manipuler des éléments du DOM JQuery-like. Une directive peut être liée à un template et donc va injecter celui-ci à l’endroit où elle sera utilisée, ou bien être utilisée sur un élément de DOM sans injecter d’élément de DOM (on y reviendra).

 Pourquoi une directive ?

Angular en comporte déjà un grand nombre que vous utilisez surement déjà à tour de bras sans vous douter de la chance que vous avez… Vous en doutez ?
Pensez à votre dernier code Angular, maintenant souvenez-vous de toutes les fois où vous avez innocemment tapé un ‘ng-repeat’ ou ‘ng-change’… Bien maintenant visualisez-vous en train de devoir copier-coller (et maintenir) tout le comportement de votre ng-repeat dans tous les contrôleurs ou vous en avez eu besoin… Bien. Vous savez maintenant.

Déclarer et utiliser une directive…

Comme je l’ai rappelé plus haut, une directive encapsule :

  • Son template HTML (ou pas)
  • Un $scope isolé qui contrôle ce template et interagit avec lui
  • Une fonction qui link le DOM à des comportements

On a donc au final toute la fonctionnalité, sa vue, ses variables d’entrées, son comportement… On retrouve Dédé dans sa totalité !

Reprenons l’exemple plus haut : J’ai besoin d’utiliser Dédé à plusieurs endroits, je peux appeler ma directive simplement depuis mon DOM autant de fois que je veux. La directive va injecter directement son template dans le DOM appelant. Le code quant à lui est en un seul endroit et se trouve donc beaucoup plus maintenable (sans parler de lisibilité du code !). Envoie ta V2, même pas peur !

Par exemple, voici le code de la directive Dédé :

myApp.directive(‘dédé’, function () {
  return {
    restrict: 'E',
    replace: true,
    template: '<a href="this.is.my.directive.com" >'+{{texteIci}}+'</a>',
 link: function (scope, element, attrs) {
      // DOM manipulation/events here!
    }
  };
});

On appelle la directive dans notre DOM:

<ul>
	<li><dede></dede></li>
	<li><dede></dede></li>
</ul>

Sympa et beaucoup plus lisible non ? Une fois compilée, notre page HTML ressemblera à ceci:

<ul>
	<li> <a href="this.is.my.directive.com"></a> </li>
	<li> <a href="this.is.my.directive.com"></a> </li>
</ul>

Le $scope isolé

Question gestion des paramètres d’entrée, une directive peut prendre des variables qui seront mappées explicitement à sa variable de scope qui lui est propre (d’où l’appellation $scope isolé), elles peuvent être du simple texte, des variables, même des fonctions. Pour cela rien de plus simple il suffit de les déclarer dans le $scope de la directive :

myApp.directive(‘dédé’, function () {
  return {
    restrict: 'E',
    replace: true,
    template: '<a href="this.is.my.directive.com" >'+{{texteIci}}+'</a>',
    scope: {
      monTexte: ‘=’
    }
    link: function (scope, element, attrs) {
      scope.texteIci = scope.monTexte;
    }
  };
});

Les paramètres qu’on ajoute sont dans l’appel de la directive:

<ul>
	<li><dede mon-texte=’toto’></dede></li>
	<li><dede mon-texte=’tata’></dede></li>
</ul>

Et notre HTML final:

<ul>
	<li> <a href="this.is.my.directive.com">toto</a> </li>
	<li> <a href="this.is.my.directive.com">tata</a> </li>
</ul>

Ici on a ajouté en paramètre une variable ‘monTexte’, on aurait très bien pu passer un simple string, ou même carrément une fonction, les possibilités sont grandes ! Pardon ? Vous voulez un exemple ?! Il suffisait de demander 🙂

Diversifier ses paramètres

Un observateur éclairé se sera demandé ce que fiche ce ‘=’ au plein milieu du $scope de Dédé…. Et bien c’est par ce biais qu’on renseigne la nature du paramètre de la directive :

  • @ passe l’argument sous forme de string
  • =  va binder la variable passée depuis le $scope englobant dans le $scope de Dédé
  • & permet de passer une fonction
myApp.directive(‘dédé’, function () {
  return {
    restrict: 'E',
    replace: true,
    template: '<a href="this.is.my.directive.com" >'+{{texteIci}}+'</a>',
    scope: {
      monTexte: ‘=’,
      monString: '@',
      maFonction: '&'
    }
    link: function (scope, element, attrs) {
      scope.texteIci = scope.monTexte;

      scope.doSomething = function doSomething(){
            dédé.passe.a.l_action;
            maFonction(); //Dédé ne sait pas du tout ce qui se passe là dedans, mais il le fait !
      };

    }
  };
});

Côté HTML, rien d’extravagant, c’en est presque ennuyeux ! Il suffit de passer dans le paramètre la variable du $scope englobant qui contiens la fonction…

<ul>
	<li><dede mon-texte=’toto’ mon-string='abcd' ma-fonction='faisCommeCi()'></dede></li>
	<li><dede mon-texte=’tata’ mon-string='efgh' ma-fonction='faisCommeCa()'></dede></li>
</ul>

Voilà donc ça vous permet de conserver le même code de directive, mais en lui autorisant des comportements différents en réponse à des évènements…

Par exemple : Si Dédé est à Lyon, il demandera un pain au chocolat pour petit-déjeuner, mais il est à Toulouse il lui faudra demander une chocolatine… La fonction reste la même, seule l’implémentation change car elle dépend du contexte, mais tant que Dédé petit-déjeune, peu lui importe le reste…


Enfin, je l’avais mentionné plus haut mais une directive peut très bien ne pas comporter de template et simplement définir un comportement qui s’appliquera sur l’élément de DOM sur lequel on l’injecte (exemple : ng-hide).

Plus d’héritage implicite, plus d’accès sauvage aux données du $scope, un seul point d’entrée et le code commence déjà à aller mieux. Bien sûr tout n’est pas rose on peut continuer à se retrouver face à des problèmes aussi, mais il faut beaucoup plus le vouloir car tout est plus explicite et organisé !
Et rien ne vous empêche d’imbriquer les directives entre elles, en passant de l’une à l’autre les données de $scope dont elle a besoin, on s’évite ainsi le risque de laisser à une directive l’accès à des données qu’elle ne doit pas toucher.

(Attention cependant à ne pas tomber dans le syndrome dit ‘de la matriochka’, qui vous filera tout autant mal à la tête qu’une bonne rasade de vodka, ah ils sont forts ces russes).

Interaction avec le DOM

Concernant la manipulation du DOM, le contrôleur de la directive peut prendre en paramètre l’élément sur lequel la directive s’applique. Angular intègre un mini JQuery qui couvrira les plus utiles des fonctions pour manipuler le DOM.

link: function (scope, element, attrs) {
      scope.texteIci = scope.monTexte;

      scope.display = function() {
        element.find('li').addClass('maPuceEstTropBelle');
      };

}

Passer à l’action

Comme vous avez pu vous rendre compte, la différence entre contrôleur et directive se situe plus au niveau conceptuel (encapsulation du template, isolation du scope…) qu’au niveau des fonctionnalités.

Passer de l’un à l’autre ne demande donc à priori pas beaucoup plus de travail qu’un sacrifice humain à cthulhu transfert des fonctions de $scope du contrôleur à la directive, puis un petit tri de vos variables de $scope pour trouver les bons paramètres (un grand nettoyage de votre $scope se fera de lui-même à ce moment-là), en enfin, remplacer tous vos éléments de DOM ng-contrôlés par des appels de directive.

Le sel hic demeure l’imbrication de vos $scopes et contrôleurs. Si ils s’imbriquent trop les uns dans les autres et abusent de l’héritage, vous pourrez vous retrouver face à un plat de nouilles bien épicé. Ne perdez pas espoir, car comme une directive est bien isolée, vous pouvez faire la transition petit à petit en commençant par les composant les plus ‘profonds’ et en remontant petit à petit jusqu’à vos (ex-)contrôleurs de plus haut niveau !

A vous de trouver une proportion directive/contrôleurs qui vous convient et permet tout de même de garder un code lisible et navigable.

Voir les choses en grand

Car avoir des tas de composants de type combo-box, sliders , calendriers, boites de dialogue & cie qui ont un comportement (presque) 100% pimpé par nos soins, c’est rudement pratique et vachement modulable, mais bon au fond on sait qu’on peut faire mieux que ça pour arranger ce plat de nouilles ng-contrôlé qu’est devenue notre appli !!

En plus, si  le stagiaire on a pris la peine de ranger toutes les boulettes de viandes de notre plat de nouilles par taille et granularité, c’est quand même dommage de devoir encore traverser tout le plat de nouilles emmêlés pour s’en apercevoir, non ?

( Quoi ? Moi, aimer les métaphores culinaires ? Du tout…).

Les directives peuvent aussi s’utiliser à une échelle plus grande de votre appli Angular et englober toute une vue ! Pour ça vous pouvez déclarer vos directives en tant que vue au niveau du routage de vos URLs, pour eux qui utilisent le $stateProvider, voici un exemple de routage de l’app.js:

   $stateProvider
      .state('login', {
        url: '/login',
        templateUrl: 'path/to/login.tpl.html',
        controller: 'LoginViewCtrl'
      })
      .state('logged.list.films', {
        url: '^/films',
        templateUrl: 'path/to/filmsList.tpl.html',
        controller: 'FilmsViewCtrl'
      })
      .state('logged.list.actors', {
        url: '^/films/:filmId',
        templateUrl: 'path/to/actorsList.tpl.html',
        controller: 'ActorsViewCtrl'
      })

Ici on a donc à vue de nez, une page de login, puis deux pages HTML pour display deux listes, une avec des films films, et une des acteurs d’un film (identifié par son ID)… On imagine bien que la différence fonctionnelle et graphique doit être absolument énorme entre ces deux vues !!

On pourrait par exemple imaginer une seule directive capable d’afficher ces deux listes de la même manière (ou pas d’ailleurs) selon des paramètres…

Le fichier app.js sera lui aussi un peu allégé :

   $stateProvider
      .state('login', {
        url: '/login',
        template: '<login></login>'
      })
      .state('logged.list.films', {
        url: '^/films',
        template: '<my-list></my-list>'
      })
      .state('logged.list.actors', {
        url: '^/films/:filmId',
        template: '<my-list></my-list>'
      })

Mais – me direz vous – on m’a promis des paramètres pour ma directive !? Remboursé !

Bon oui j’avoue je n’ai pas (encore ?) trouvé dans la littérature de quoi affirmer qu’on peut utiliser le paramètre ‘:filmId’ directement dans la déclaration du template de app.js (ça serait beau oui…).

<mcGyver> Par contre on pourrait surement tenter d’utiliser le $stateParams  dans la directive my-list pour vérifier l’existence de cette valeur et agir en conséquence 🙂 </mcGuyver>

Donc ici aussi on peut s’économiser deux fichiers de contrôleurs (qui devaient probablement se ressembler très fort) et deux fichier HTML (qui devaient se ressembler encore plus fort) pour n’avoir qu’une seule directive (et éventuellement le template HTML dans un fichier à part). Encore une fois vous serez seul juge pour bien placer la limite de l’overkill, et trouver l’équilibre entre:

« Avoir 10.000 contrôleurs qui font un peu tous pareil » / « Avoir une directive qui a 10.000 paramètres et comportements différents »

(Diviser pour mieux régner, mais l’union fait la force… )

 Conclusion

Tout le long du cycle de vie de votre application, des composants vont émerger et vous voudrez chercher à mutualiser leur code, pour les rendre non seulement plus réutilisables, mais aussi plus maintenables et plus robustes au cours du temps.

Les directives semblent être une bonne option pour l’instant car elles garantissent une bonne isolation des variables de $scope, et une grande flexibilité d’utilisation. Elles vous forceront aussi à découper mieux votre code, qui s’en trouvera plus lisible, et puis qui sait, plus adaptables à la prochaine feature Angular qui remplacera les directives !

Bonus

On m’annonce dans l’oreillette qu’en plus de tout cela, la venue imminente d’Angular 2 va venir mettre un coup de boxon dans votre appli puisque cette version sera très largement favorable à l’utilisation plus poussée des directives. Donc ça serait vraiment dommage de se priver d’une occasion de faire le ménage dans votre appli tout en se préparant à passer à la dernière version Angular !! Et il serait encore plus dommage de manquer l’article  qui y sera consacré un peu plus tard dans ce blog (lien à venir) !

Voilà, vous ne pourrez plus dire que vous ne saviez pas !

Commentaires 0

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.