Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Angular 1: les directives

Poster un commentaire

Après les services, le binding et les routes, les directives sont le dernier grand pilier de l’architecture Angular. Partons à leur découverte ce qui clôturera ma série de tutoriel Angular 1 avant d’attaquer Angular 2.

Pour des raisons que j’ignore, la majorité des exemples que je trouve sur le net sont, me semble t’il, très compliqués. Je préfère nettement faire 10 démos très simples, plutôt qu’une seule qui démontre 10 concepts en même temps.

 

Alors c’est quoi?

Les sites modernes ne se contentent plus d’utiliser HTML comme un langage statique de description de pages. On a besoin de transformer HTML en véritable langage et notamment disposer d’un support pour l’extensibilité.

En attendant l’arrivée future des Web Component, les directives Angular fournissent une alternative qui a le mérite de fonctionner pour tous les navigateurs ou presque.

De façon très simple, les directives sont un outil pour étendre les fonctionnalités d’HTML.

Première directive

Dans Angular, tout commence par une fonction et une directive est une fonction introduite avec la méthode directive().

var app.module('app',[]);
 
app.directive('secondDirective', function() {
   return {
      // ma directive
   };
});

On va créer une directive qui transforme une <div> en une zone de saisie. Côté code:


app.directive('secondDirective', function() {
return {
   template: "<input type='text' ng-model='prompt2' /> {{prompt2}}<br/>"
}});

Le code HTML est le suivant:


<h1>Angular 1: première directive</h1>
<second-directive></second-directive>

 

Et il s’affiche:

2016-04-15_18-47-05

Ca fonctionne. On aurait pu poser le template dans un fichier html annexe, dans ce cas on aurait utilisé la propriété templateUrl qui attend le chemin vers ce fichier.

 

Vous êtes sans doute surpris par le nom de la directive:

second-directive

Qui n’est pas celui déclaré dans le code HTML:

secondDirective

Angular n’aime pas la notation Camel et demande que les majuscules soient précédées par un tiret (-). Cela explique quelque chose qui m’avait troublé lorsque j’ai débuté en Angular:

ngRepeat —> ng-repeat

ngOptions —> ng-options

 

Si on essaye secondDirective à la place de second-directive on voit vite que cela ne marche pas, mais on peut mettre second-Directive.

 

Comme je l’ai expliqué dans la première partie de ce tutoriel, il existe 4 types de directives:

  1. les balises
  2. les attributs
  3. les classes
  4. Commentaires

On fixe le type avec le paramètre restrict de la directive. Notre directive fonctionne par défaut en mode attribut ou élément:

<u>mode attribut</u>
<div second-directive></div>
 
<u>mode élément</u><br/>
<second-directive></second-directive>

Qui affiche:

2016-04-15_19-18-11

Tout se passe comme si on avait implicitement ceci:


app.directive('secondDirective', function($compile) {
   return {
   restrict:"EA",
   link: function($scope,element,attrs,controller) {
      ...
   }
}});

Les restrictions possibles sont:

  • E
    Elément
  • A
    Attribut
  • C
    Classe
  • M
    Commentaire

Elles sont cumulables bien entendu et si on part de:

restrict: « EACM »

 

Alors on peut écrire au choix:

<u>mode attribut</u>
<div second-directive></div>
 
<u>mode élément</u>
<br/>
<second-directive></second-directive>
 
<u>mode classe</u>
<br/>
<div class='second-directive'></div>
 

<u>mode commentaire</u>
<!-- directive: second-directive -->

 

Note: Je n’ai jamais réussi à voir la restriction de commentaire fonctionner, mais ça ne me manque pas. La  bonne pratique est d’éviter les restrictions de classe et de commentaire.

 

Vous trouverez le code ici:

https://plnkr.co/edit/a3kY59CRHZaPjwzEGrnB

Le scope

Dans l’exemple précédent, j’ai démontré le fonctionnement des différentes restriction en posant plusieurs fois la même directive sur la page. Ce que j’ai pudiquement passé sous silence est ceci:

2016-04-16_20-58-49

On remplit une zone de saisie et elle se remplissent toutes en même temps. Il est temps de parler de la maitrise du scope dans les directives.

 

On peut contrôler le scope d’une directive avec sa propriété scope qui attend un objet ou bien un booléen.

Scope permet de sélectionner 3 types de scope:

  1. scope courant
  2. scope isolé
  3. nouveau scope

 

On peut monter un exemple simple pour expliquer la différence. Tout d’abord on complète le code avec ce contrôleur:


app.controller('moncontroller', function($scope) {
   $scope.valeur1 = 1;
   $scope.valeur2 = 2;
   $scope.valeur3 = 3;
});

Scope courant

Voici une directive:


app.directive('directive1', function() {
   return {
      template: "<input type='text' ng-model='valeur1' /> {{valeur1}}<br/>",
   }
});
 

Lorsque scope n’est pas définit où si sa valeur est false, la directive utilise le scope courant. C’est le cas de figure rencontré en début de chapitre.

Les deux copies d’écran ci dessous montrent ce qui se passe au chargement de la page (gauche) et après une saisie (droite).

2016-04-17_13-25-33

Le comportement démontre que la propriété de modèle du scope de la balise HTML et celui de la directive est le même. Vu depuis Batarang, il n’y a qu’un seul scope:

2016-04-17_21-22-51

Dans ce mode la directive créée un modèle dans le contrôleur qui sera partagé par toutes les autres instances.

Scope isolé

Le cas suivant est celui-ci:


app.directive('directive2', function() {
return {
   template: "<input type='text' ng-model='valeur2' /> {{valeur2}}<br/>",
   scope: {}
   }
});

La différence est la présence de la propriété scope qui est associée à un objet JavaScript. Voici le HTML qui est similaire au cas précédent:

<u>Scope isolé</u>
<div ng-controller='moncontroller'>
<p>Hors directive: {{valeur2}}</p>
<p>Dans la directive: </p><span directive2></span>
<p>Dans la directive: </p><span directive2></span>
 </div>

Le comportement est tout à fait différent:

2016-04-17_21-30-46

On est dans le cas du scope isolé. Il est dit isolé car il n’a pas de relation d’héritage avec le scope de la balise HTML à laquelle la directive est rattachée.

Cela signifie que le scope de la directive n’hérite pas de la propriété valeur2 du contrôleur. Nous le constatons sur l’image de gauche qui n’affiche pas de valeurs, mais nous avons tout de même un scope local qui n’interagit en rien avec celui de l’élément (image droite).

Pas d’héritage certes, mais la hiérarchie est toujours là, on peut invoquer $parent:


app.directive('directive2', function() {
   return {
   template: 
   "<input type='text' ng-model='valeur2' /> {{valeur2}}<br/>{{$parent.valeur2}}",
   scope: {}
}
});

Et il s’affiche:

2016-04-17_21-58-35

Ce qui montre que la directive peut accéder au scope du contrôleur.

Le scope isolé est le choix que l’on fera si on a besoin de créer des directives indépendantes et réutilisables, c’est à dire une directive qui ne peut lire ou modifier accidentellement le contexte parent.

Comment passer des paramètres à un scope isolé par un moyen plus constructif que $parent? Par les attributs :


app.directive('param1', function() {
return {
   template: "<input placeHolder='{{nomParam}}' type='text' />",
   scope: {
      nomParam: "@"
   }
}
});

Je détaillerai plus loin le tag @.

Dans le HTML:


Paramètre pour la directive: <input ng-model='saisie' />
<div ng-controller='moncontroller'>
<span param1 nom-param='{{saisie}}'></span>
</div>

On déclare le nom de l’attribut dans l’objet scope. Il nous servira à alimenter la propriété placeHolder de <input>.

Remarquez la syntaxe utilisée dans le code HTML, c’est celle de la directive. L’affichage donne ceci:

2016-04-18_11-02-25

Testez cet exemple, vous constaterez la directive reçoit de façon dynamique sa valeur du scope courant.

 

Il reste à détailler le sens du @ utilisé dans la syntaxe.

Il existe 3 tags qui correspondent à 3 façons de passer les paramètres:

  • @
    binding one- way
  • =
    binding two-way.
  • &
    Binding sur une fonction

 

Le binding two-way a ses particularités. Examinons cette directive:


app.directive('param2', function() {
   return {
      template: "<input ng-model='nomParam' type='text' />",
      scope: {
         nomParam: "="
      }
}});

 

Le code HTML est le suivant:

<div ng-controller='moncontroller'>
   Paramètre pour la directive:
   <input type='text' ng-model='nomParamCtrl' />      <br/><br/> 
 
   <span param2 ng-model='saisie2' nom-param='nomParamCtrl'></span>
   {{nomParamCtrl}} 
</div>

 

Le binding ne se fait pas sur une expression, mais sur le nom d’une variable. Si on déclare:

nom-param='{{nomParamCtrl}}’

Un message d’erreur apparaît:

Syntax Error: Token ‘{‘ invalid key at column 2 of the expression [{{nomParamCtrl}}] starting at [{nomParamCtrl}}].

 

Le point intéressant est le paramètre nomParamCtrl qui fait partie du scope du contrôleur. NomParam quand à lui fait partie du scope de la directive. NomParam est tagué ‘=’, cela signifie qu’il va recevoir non pas une valeur, mais une référence vers une propriété, nomParamCtrl en l’occurrence. Les deux propriétés seront donc liées par un binding two-way. Vérifions le!

 

Si on saisit une entrée dans <input> on a ceci:

2016-04-18_15-22-09

Si je complète la saisie dans la directive:

2016-04-18_15-25-19

Ca fonctionne bien dans les deux sens.

 

Pour finir nous pouvons binder sur une fonction. On ajoute la fonction dans le scope global, c’est à dire le contrôleur:


$scope.upperCase = function (valeur) {
   $scope.nomParam3 = valeur.toUpperCase();
}

Le code HTML est le suivant:


<div ng-controller='moncontroller'>
   <input ng-model='nomParam3' />
   <span param3 upper="upperCase(nomParam3)"></span>
   {{nomParam3}}
</div>

Et la directive qui va bien:


app.directive('param3', function() {
   return {
   template: "<input ng-click='upper()' value='Majuscule' type='button' />",
   scope: {
      upper: "&"
   }
}});

On définit un paramètre ‘upper’ qui est déclaré comme étant une fonction avec le tag ‘&‘. Cette fonction sera appelée lors d’un click sur l’élément.

A son niveau la directive n’a pas la moindre idée de ce que fait la fonction, elle attend juste qu’elle lui soit passée comme valeur de upper. C’est ce qui est fait dans le code HTML qui décide d’injecter la méthode upperCase() définie dans le contrôleur. Cette méthode tourne en majuscules la propriété de modèle une fois cliqué sur le bouton:

2016-04-18_13-44-44

 

Peut être délicat, mais pas très compliqué.

Nouveau scope

Le dernier cas examiné est celui du nouveau scope.


app.directive('directive3', function() {
return {
   template: "<input type='text' ng-model='valeur3' /> {{valeur3}}<br/>",
   scope:true
}
});

Il suffit d’assigner true à la propriété scope.

2016-04-17_22-05-53

Cette fois encore on a clairement deux scopes indépendants. Si vous observez la partie gauche vous voyez que le scope de la directive hérite de celui de l’élément HTML. C’est bien un héritage puisque la valeur dans le scope de l’élément n’est pas affectée contrairement au scope global.

 

Vous trouverez ici le code que nous venons de commenter:

https://plnkr.co/edit/1vwFmgWuEb42bPYkM4aT

La transclusion

Ce terme n’existe pas dans le dictionnaire, il n’a de sens que dans le contexte Angular. Le cas de figure qui nous occupe est celui-ci:


<ma-directive>Comment vas tu?</ma-directive>

Avec une directive telle celle-ci:


app.directive('maDirective2', function() {
   return {
      restrict:"E",
      template:"<p>Hello Amethyste!</p>"
   }
});

Il s’affiche ceci:

2016-04-18_16-28-37

Le contenu de la balise est perdu.

Parfois on souhaiterai le récupérer. C’est pour ces cas que l’on active la transclusion:


app.directive('maDirective', function() {
   return {
      restrict:"E",
      transclude:true,
      template:"<p>Hello Amethyste!<br/><span ng-transclude></span></p>"
   }
});

On met à true la propriété transclude de la directive et on utilise la directive ngTransclude pour désigner l’emplacement du HTML qui a été capturé.

2016-04-18_16-31-45

La transclusion est le fait de capturer le HTML associé à l’élément sur lequel une directive est posée pour le déplacer dans le contexte de la directive.

Le projet de démo:

https://plnkr.co/edit/LdAbM6mL82asmlVTePf2?p=preview

Cycle de vie d’une directive

Lorsque le moteur Angular traite une vue, les directives sont exécutées en plusieurs phases :

  • Parcours du DOM
    Découverte des directives utilisées, enregistrement avec l’élément HTML où la directive est attachée.
  • Compilation
    Compile la vue HTML et retourne une fonction dite fonction template que l’on invoque ensuite pour lier le scope et le template HTML.
    La phase est exécutée une fois par déclaration de directive.
    Il est assez rare que l’on mette du traitement ici.
  • Controller
    Création du contrôleur de la directive. Sert essentiellement à la communication entre directives. C’est l’objet du chapitre suivant.
    Rien ne se passe si la directive ne déclare pas de contrôleur.
    Phase exécutée une fois par instance de la directive.
  • PreLink
    Phase exécutée avant que les directives présentes dans l’élément HTML ne soient exécutées et juste avant la compilation.
    Le scope a été associé au template, mais la transclusion reste à faire.
    Cette phase sert typiquement à enrichir le scope courant avant de lancer les directives enfants.
  • PostLink
    Phase exécutée juste après la compilation.
    C’est plutôt ici que l’on injecte du traitement, en tout cas plus souvent que dans PreLink.
    Angular fait la transclusion et interprète les expressions et les bindings.

On peut intervenir à chacune de ces étapes, mais certains sont plus utiles que d’autres.

On alimente ces phases à l’aide des propriétés de la directive. Je vais plutôt m’intéresser à link car c’est le plus usuel, la plupart des traitements liés à la directives ont en effet été exécutés à ce stade.

La propriété link de la directive attend la syntaxe générale suivante:


link:{
   function prelink(scope,element,attrs, controller) {...},
   function postlink(scope,element,attrs, controller) {...}
}

La propriété link expose donc deux méthodes qui correspondent à chaque phase du même nom. Dans la majorité des cas il n’y a pas de personnalisation de la phase prelink, il est alors possible de ne fournir qu’une fonction à link:


link: function(scope,element,attrs, controller) {...}

Les méthodes link attendent jusqu’à 5 paramètres:

  1. le scope courant
  2. l’élément html sur lequel la directive est posée
  3. les attributs de l’élément HTML
  4. le contrôleur
  5. Fonction de transclusion

 

D’autres façons d’appeler link existent.

  • La directive se réduit à la méthode link:

app.directive(‘maDirective’, function() {
   return function postLink() { … }
});

  • Où bien on fait un appel depuis compile:

app.directive(‘maDirective’, function() {
   return {
      compile: function(element, attrs) {
         // traitement de compile...
 
         return function postLink() { … };
      }
   }
});

Cette dernière syntaxe existe aussi sous cette forme:

app.directive(‘maDirective’, function() {
   return {
      compile: function(element, attrs) {
      // traitement de compile...
 
      return {
            function preLink() { … };
            function postLink() { … };
         }
      }
   }
});

A quoi peut servir cette syntaxe au fait? La raison est que si vous redéfinissez compile, alors link ne sera pas exécutée. Si vous avez besoin d’un comportement durant la phase link, vous devrez donc utiliser une des syntaxes ci-dessus.

Important: lors des phases compile, element ne correspond pas à la vue finale, mais au template de vue.

 

Il se pose la question de savoir si on doit intervenir sur l’étape compile ou bien link. C’est important car compile est appelée une seule fois, mais link à plusieurs reprises.

Compile va rassembler les traitements communs aux directives, c’est à dire ceux qui n’ont pas besoin de connaître le contexte, la transclusion, le modèle… Le DOM n’est pas créé

Link va rassembler les traitements qui sont dépendants du contexte. Le DOM est créé on pourra donc intervenir sur les instances.

 

Voyons maintenant quelques démos.

app.directive('maDirective1', function() {
   return {
      scope: {
      saisie: '='
   },
   template:"<input ng-model='saisie' />",
   link : function postlink(scope) {
      scope.$watch("saisie", function(nom) {
         console.log(nom);
      })
   }
}
});
 

Vous devriez pouvoir reconstituer le code HTML. On invoque un postLink. La méthode pose un watch sur la propriété du modèle ‘saisie’. Chaque modification de ‘saisie’ entraîne l’affichage d’un log dans la console.

Toutefois c’est un message d’erreur qui vous attend:

Expression ‘undefined’ in attribute ‘saisie’ used with directive ‘maDirective1’ is non-assignable!

 

La solution consiste à déclarer le modèle isolé de la façon suivante:


scope: {
   saisie: '=?'
}

Le tag ‘?‘ indique que le paramètre est facultatif et donc qu’il peut ne pas être définit à un instant donné. C’est certainement ce qui va se produire au chargement initial de la directive. Faites ensuite une saisie et:

2016-04-19_10-22-37

L’exemple n’est pas des plus intéressant. Mais on pourrait l’enrichir un peu:


app.directive('maDirective1', function() {
   return {
      scope: {
         saisie: '=?',
         message:"=?"
      },
      template:"<input ng-model='saisie' />{{message}}",
      link : function postlink(scope) {
         scope.$watch("saisie", function(nom) {
            scope.message = "Salut " + nom;
         })
      }
   }
});

C’est un exemple de modification du scope via une directive. Le plus souvent c’est au DOM que l’on s’attaque. Par exemple:

app.directive('maDirective2', function($compile) {
   return {
      link: function($scope,element) {
         var template="<input type='text' ng-model='prompt' /> {{prompt}}<br/>";
 
         element.html(template);
         $compile(element.contents())($scope);
 
         //angular.element(element).append($compile(template)($scope));
   }
}});

Puisque l’on a besoin d’accéder au DOM on utilise la surcharge avec le paramètre ‘element’. Que fait t’on?

On commence par définir le template HTML par lequel on va remplacer l’élément HTML (4).

On remplace ensuite la div par le template (6). Je parle de la ligne 7 plus loin.

La ligne en commentaires (9) est une syntaxe alternative dans laquelle angular.element(element) parcourt le DOM à la recherche de la DIV sur laquelle on a posé la directive.

Côté affichage:

2016-04-19_10-51-12

On a vu des méthodes plus simples pour faire la même chose, mais l’important est le principe.

$compile compile une chaîne HTML et retourne une fonction dite fonction template que l’on invoque ensuite pour lier le scope et le template HTML. Si on supprime la ligne 7, on observe ceci:

2016-04-19_10-53-08

La transformation HTML fonctionne, mais pas la partie Angular.

De la même façon on peut accéder à toutes les méthodes et propriétés de l’éléments sur laquelle la directive est posées via les paramètre element et attrs.

Note: la manipulation du DOM se fait rarement dans preLink car à ce stade la transclusion n’a pas encore été effectuée. On n’a donc pas encore le DOM définitif.

Et comme toujours la démo:

https://plnkr.co/edit/5dBPQ1FgpmdNgNlDghqz

Priorités entre directives

Avant de voir comment faire communiquer des directives, je voudrai évoquer la question des priorités. Plusieurs directives peuvent en effet être rattachées à une même vue HTML. Il va donc falloir gérer l’ordre dans lesquelles elles seront traitées.

La priorité d’une directive se fixe avec sa propriété priority qui attend un entier. Plus la valeur est élevée, plus la directive est prioritaire.

En d’autres termes:

  • Les phases compile, controller seront exécutées plus tôt.
  • Les phases preLink et postLink seront exécutées plus tard

Regardons cet exemple pour clarifier le point:

app.directive('bassePriorite', function() {
   return {
   priority:1,
   template:'<p>Hello bassePriorite</p>',
   compile:function() {
      console.log('compile: bassePriorite');
 
      return {
         pre: function() {
               console.log('prelink: bassePriorite');
            },
         post:function() {
               console.log('postlink: bassePriorite');
            }
         }
      }
   }
});
 
app.directive('hautePriorite', function() {
   return {
   priority:2000,
   template:'<p>Hello hautePriorite</p>',
   compile:function() {
      console.log('compile: hautePriorite');
 
      return {
         pre: function() {
               console.log('prelink: hautePriorite');
            },
         post:function() {
               console.log('postlink: hautePriorite');
            }
         }
      }
   }
});

Tout est dans les logs. Côté visuel:

2016-04-19_11-59-43

Les logs ressemblent à ceci:

2016-04-19_12-02-07

On observe une alternance dans l’exécution des directives sur compile selon la priorité. Les événements link de la directive hautePriorite s’effectuent tous après ceux de la directive bassePriorite.

 

Pour l’exemple qui suit, il me faut une directive qui ne cherche pas à modifier le DOM afin de ne pas entrer en conflit avec l’autre et générer un message du style:

Multiple directives [hautePriorite (module: app), bassePriorite (module: app)] asking for template on: <div basse-priorite= » » haute-priorite= » »>

Donc:


app.directive('autrePriorite', function() {
   return {
      priority:500,
      compile:function() {
         console.log('compile: autrePriorite);
         return {
            pre: function() {
               console.log('prelink: autrePriorite);
            },
            post:function() {
               console.log('postlink: autrePriorite);
            }
         }
      }
   }
});

Essayons maintenant ce code:


<ul>
   <li ng-repeat = "i in [1,2,3]">
      <div basse-priorite autre-priorite>Item: {{i}}</div>
   </li>
</ul>

Voici les logs obtenus:

2016-04-19_12-26-18

Les phases preLink sont tout d’abord exécutée dans l’ordre des priorités suivie de la phase postLink dans l’ordre inverse des priorités.

Il est possible de poser une propriété de directive terminal à true. Lorsque le moteur Angular rencontre cette propriété, les directives du même élément HTML de priorité plus basse ne seront pas traitées.

Les contrôleurs

Premier contact

Un dernier concept de directive important: les contrôleurs. Malgré la similitude de nom, ils n’ont rien à voir avec les contrôleurs Angular.

Dans notre contexte le contrôleur est un objet que des directives vont utiliser pour communiquer entre elles. On peut imaginer une Api spécifique à la directive.

La mise en œuvre s’effectue en 3 étapes:

  1. Création des contrôleurs
  2. création d’un lien entre directives
  3. récupération des contrôleurs dans link

 

On déclare un contrôleur avec la propriété controller de la directive qui attend une fonction construite comme le contrôleur Angular. Elle est donc compatible avec l’injection de dépendance. Voici un exemple:

app.controller('moncontroller', function($scope) {
   $scope.message = 'Un ';
});
 
app.directive('directive1', function() {
   return {
      template: '<p>{{message}}!</p>',
      controller: function($scope){
            $scope.message = $scope.message + "Deux ";
         }
      }
})

Et la vue:

<body ng-controller='moncontroller'>
<h1>Directive de contrôleur</h1>
 
<h2>Premier exemple</h2>
<directive1></directive1>
</body>

Et pour finir l’affichage:

2016-04-19_18-50-31

Pour l’instant je trouve que cette affaire ressemble furieusement à ce qui a été vu avec link, on pourrait d’ailleurs monter un exemple:


app.directive('directive2', function() {
   return {
      template: '<p>{{message1}}!</p>',
      controller: function($scope){
            $scope.message1 = $scope.message1 + "Deux ";
         },
      link: function(scope) {
            scope.message1 = scope.message1 + "Trois ";
         }
   }
})

2016-04-19_18-57-33

La différence dépend de quand le code doit tourner. La version courte :

  • avant la compilation –> controller
  • après la compilation -> link. Typiquement tout ce qui est manipulation du DOM

La version longue :

http://jasonmore.net/angular-js-directives-difference-controller-link/

 

On a ensuite besoin d’établir un lien  entre les directives pour qu’elles puissent récupérer leurs contrôleurs. C’est le rôle de la propriété require.

Regardons ces deux directives:

app.directive('directive3', function() {
   return {
      controller:function($scope) {  }
   }
})
 
app.directive('directive4', function() {
   return {
      require: 'directive3',
      transclude: true,
      template: "<span ng-transclude></span><b>Frédo</b>"
      ,link: function($scope,element,attrs,lesControllers) {}
   }
})

Directive3 expose un contrôleur qui sera consommé par directive4. Observez comment nous avons paramétré require. Directive4 sait qu’elle doit obtenir un contrôleur de la part de directive3. Ce contrôleur sera récupéré par link à travers le paramètre lesControllers.

Essayons dans un premier temps cette vue :


<directive4></directive4>
<directive3></directive3>

L’exécution provoque une exception:

Controller ‘directive3’, required by directive ‘directive4’, can’t be found!

Le message nous explique que directive3 attend le contrôleur de directive4 et ne le trouve pas. Il ne suffit pas de poser n’importe où directive3 pour que cela fonctionne, il y a une sémantique à respecter. On a deux cas de figure.

Avec la syntaxe utilisée dans require, les directives doivent se trouver sur le même élément HTML comme ceci:

<div directive3 directive4>Hello </div>

Et il s’affiche:

2016-04-19_23-23-04

Une autre sémantique est assez courante:

<div directive3>
   <div directive4>Hello </div>
</div>

Si vous testez vous retombez sur le message d’erreur qui précède. La syntaxe require est la suivante:


require: '^directive3'

Le nom de la directive parente est précédée de l’accent circonflexe (^). Cette syntaxe est compatible avec la première sémantique.

Il arrive aussi que le lien soit facultatif, dans ce cas:


require: '?directive3'

Le tag est le point d’interrogation (?).

 

Require nous permet de créer des liens avec une autre directive, mais on peut aussi le faire avec plusieurs. Il suffit de lui associer un tableau de directives:


app.directive('directive4', function() {
   return {
      require: ['directive3','ngModel'],
      transclude: true,
      template: "<span ng-transclude></span><b>Frédo</b>"
      ,link: function($scope,element,attrs,controllers) {
            var directive3 = controllers[0];
            var ngmodel = controllers[1];
         }
      }
})

Observez comment on récupère les directives. On peut mixer les sémantiques:


require: ['^directive3','ngModel']

La directive ngModel

La directive ngModel et son contrôleur ngModelController est d’usage très fréquent. Elle  permet de binder une zone de saisie comme <input> à un modèle:

https://docs.angularjs.org/api/ng/directive/ngModel#!

On peut également l’utiliser pour fournir des services de validation, gestion des états du contrôle…

Voyons quelques exemples.

Un validateur numérique va valider qu’une saisie soit un nombre compris entre 0 et 10:


app.directive('numValidator', function() {
   return {
   require: 'ngModel',
   restrict: 'A',
   link: function($scope, element, attrs, ngModel) {
         ngModel.$validators.numInput = function(value) {
            return !value || /^[0-9]$/.test(value);
      }
   }
}
})

La directive a un lien avec ngModel. Le contrôleur est donc récupéré dans link. Si l’on injecte qu’un seul contrôleur, il est d’usage courant de donner à la propriété le nom de la directive ou de son contrôleur, cela améliore la lisibilité. Link ajoute une méthode numInput à $validators. Cette propriété du contrôleur ngModel permet d’ajouter un validateur.

 

la directive n’a pas besoin de template.

Le code HTML sera le suivant:


<form name="form">
   <input name="saisie" ng-model='numInput' num-Validator />
   <span ng-show="form.saisie.$error.numInput">Saisie incorrecte</span>
</form>

On a une zone de saisie décorée de la directive numValidator suivit d’une zone de texte pour afficher un éventuel message d’erreur.

Vous pouvez vérifier son fonctionnement:

2016-04-20_19-07-52

 

C’est un exemple je pense assez typique de l’usage des directives communicantes.

 

Comme toujours, le projet avec le code:

https://plnkr.co/edit/eEwKMuxWBwPiAwD4ZLet?p=preview

Bibliographie

 

 

 

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s