Les directives Angular

Cet article fait partie de la série d’articles Angular from Scratch.

@gersonrepreza

Les directives permettent de modifier ou d’enrichir un élément du DOM en rajoutant ou en modifiant une propriété par programmation. Ces directives peuvent être utilisées à l’intérieur de composants.

Fonctionnellement les directives peuvent paraître semblables aux composants enfant toutefois la grande différence entre les directives et les composants est que la directive n’a pas de vue. En effet, il n’est pas possible d’utiliser un paramètre template ou templateUrl dans le décorateur @Directive() permettant de déclarer une directive.

Dans la documentation Angular, les directives sont découpées en 3 catégories:

  • Les composants: ce sont des directives avec une vue implémentée dans un fichier template. Les composants seront développés dans un article séparé.
  • Les autres types de directives ne disposent pas de vue mais elles permettent de modifier le DOM en ajoutant ou en supprimant des éléments du DOM:
    • Les directives attribut (i.e. attribute directives): ces directives peuvent modifier l’apparence et le comportement des éléments, composants et d’autres directives.
    • Les directives structurelles (i.e. structural directives): elles se distinguent des directives attribut car elles utilisent un modèle (i.e. template) pour modifier le DOM. Il ne faut pas confondre ce modèle avec le template d’un composant.

Dans un premier temps, on va expliciter les paramètres les plus importants du décorateur des directives @Directive(). Dans une 2e partie, on explicitera des indications sur les directives attribut puis sur les directives structurelles.

D’un point de vue implémentation, les directives doivent être déclarées avec le décorateur @Directive() et le paramètre selector. Ce paramètre est utilisé dans le template d’un composant de façon à indiquer où la directive sera résolue.

Par exemple, si une directive est définie de la façon suivante:

import { Directive, ElementRef, Renderer2 } from '@angular/core'; 

@Directive({ 
  selector: 'custom-directive' 
}) 
export class CustomDirective { 
  constructor(private elem: ElementRef, private renderer: Renderer2) {  
    let newText = renderer.createText('Text from directive');  
    renderer.appendChild(elem.nativeElement, newText); 
  }
} 

Elle peut être utilisée de la façon suivante dans le template d’un composant:

<p>Host component</p> 
<custom-directive></custom-directive>

Comme pour les composants, au moment d’afficher une vue, une résolution est effectuée en fonction du code se trouvant dans le template, pour savoir quelle directive doit être utilisée. Pour que la résolution réussisse, la directive doit être rajoutée dans le module où se trouve le composant, par exemple:

import { CustomDirective } from './custom-directive'; 

@NgModule({ 
  declarations: [ CustomDirective ],  
  ... 
}) 
export class CustomModule {}

Cette directive rajoute le texte 'Text from directive' dans le corps de l’objet du DOM:

L’affichage obtenu avec cet exemple est:

En rajoutant quelques éléments de style CSS dans le fichier de style du composant, on peut voir l’élément correspondant à la directive:

:host { 
  border: 1px solid black;  
  display: block;  
  padding: 5px; 
} 

custom-directive { 
  border: 1px solid black;  
  display: block; 
  background-color: lightgreen; 
} 

Le résultat devient:

Pour voir la différence entre un composant et une directive, on peut afficher l’inspecteur d’objets du DOM dans le browser.

Par exemple pour un composant, l’objet du DOM sera <ng-component>:

Pour une directive, l’objet du DOM sera le nom de l’élément utilisé dans le paramètre selector ou l’élément HTML qui est l’hôte de la directive:

Créer une directive avec le CLI Angular

Pour créer une directive avec le CLI Angular, il faut exécuter la ligne suivante:

ng generate directive <nom de la directive>  

Ou

ng g d <nom de la directive>

Cette commande crée:

  • un fichier contenant la classe de la directive nommé <nom de la directive>.directive.ts,
  • un fichier de test nommé <nom de la directive>.directive.spec.ts et
  • modifie le fichier du module Angular pour ajouter la directive dans le paramètre declarations du décorateur @NgModule().

Configuration des directives

Paramètre selector

Ce paramètre est le plus important car c’est lui qui va indiquer comment la directive sera utilisée. En fonction de la syntaxe utilisée, la directive sera utilisée comme un objet du DOM à part entière, une classe CSS ou un attribut dans un élément HTML etc…

Le paramètre selector s’utilise dans le décorateur @Directive():

@Directive({ 
  selector: <valeur> 
}) 
export class TestDirective {}

La syntaxe de la valeur peut être:

  • Directement le nom d’un élément HTML:
    Par exemple:

    selector: 'custom-directive'
    

    Dans ce cas, la directive doit être utilisée directement en tant qu’élément dans le template du composant:

    <custom-directive></custom-directive>
    
  • Attribut d’un élément HTML:
    selector: '[custom-directive]'
    

    Ce type de directive peut être utilisé en tant qu’attribut d’un élément HTML:

    <span custom-directive></span>
    

    L’attribut peut comporter une valeur:

    <span custom-directive="attribute value"></span>
    

    La directive est reconnue aussi dans le cas d’un property binding:

    <span [custom-directive]="bindedProperty"></span>
    

    Toutefois dans ce dernier cas, la directive devra comporter un paramètre d’entrée avec le même nom ou le même alias que le paramètre selector.

  • Classe CSS:
    Par exemple:

    selector: '.custom-directive' 
    

    Dans ce cas, la directive doit être utilisée dans une classe d’un élément HTML:

    <span class="custom-directive"></span>
    

On peut utiliser des conditions plus précises pour que la directive s’applique:

  • Suivant la valeur d’un attribut d’un élément HTML:
    Par exemple:

    selector: '[title=custom-directive]'
    

    Dans ce cas, l’élément HTML doit comporter l’attribut title="custom-directive":

    <span title="custom-directive"></span> 
    
  • Suivant un élément HTML et sur la valeur d’un de ces attributs:
    Par exemple:

    selector: 'p[title=custom-directive]' 
    

    Dans ce cas, l’élément HTML doit être spécifiquement un <p></p> et il doit comporter l’attribut title="custom-directive":

    <p title="custom-directive"></p>
    
  • Utiliser l’opérateur logique NOT à une condition:
    Par exemple:

    selector: ':not([custom-directive])'
    

    Dans ce cas, pour que la directive s’applique, l’élément HTML ne doit pas contenir d’attribut custom-directive:

    <span custom-directive>With attribute</span> 
    <span>Without attribute</span> 
    

    La directive ne sera appliquée que dans le 2e cas.

  • Utiliser l’opérateur logique OR entre 2 conditions:
    Par exemple:

    selector: '[custom-directive], [title=custom-directive]' 
    

    Dans ce cas, la directive s’appliquera si l’un des attributs suivants est présent:

    <span custom-directive></span> 
    <span title="custom-directive"></span> 
    

    La directive s’appliquera dans ces 2 cas d’utilisation de span.

Paramètre inputs

Ce paramètre utilisable dans le décorateur @Directive() permet d’injecter des paramètres d’entrée dans la directive à partir du composant hôte. Le nom de la donnée membre doit être indiqué directement sous forme de chaîne de caractères dans le paramètre inputs, par exemple:

import { Directive, ElementRef, Renderer2, OnInit } from '@angular/core'; 

@Directive({ 
  selector: 'custom-directive', 
  inputs: [ 'textToDisplay' ] 
}) 
export class CustomDirective implements OnInit { 
  textToDisplay: string; 
  
  constructor(private elem: ElementRef, private renderer: Renderer2) {} 

  ngOnInit(): void { 
    let newText = this.renderer.createText(this.textToDisplay); 
    this.renderer.appendChild(this.elem.nativeElement, newText); 
  } 
} 

Pour injecter la valeur à partir du template du composant dans lequel on utilise la directive on utilise un attribut:

<p>Host Component</p> 
<custom-directive textToDisplay='Value defined from component'></custom-directive> 

A l’exécution, la valeur est affichée dans la directive:

Dans cet exemple la valeur injectée est une chaine de caractère toutefois on peut injecter des objets avec des types plus complexes et effectuer un property binding dans le template du composant.

Par exemple, si on définit la directive de cette façon:

import { Directive, ElementRef, Renderer2, OnInit } from '@angular/core'; 

export class DisplayedText { 
  constructor(textToDisplay: string) {} 
} 
 
@Directive({ 
  selector: 'custom-directive', 
  inputs: [ 'textToDisplay' ] 
}) 
export class CustomDirective implements OnInit { 
  text:DisplayedText;

  constructor(private elem: ElementRef, private renderer: Renderer2) {} 

  ngOnInit(): void { 
    let newText = this.renderer.createText(this.text.textToDisplay); 
    this.renderer.appendChild(this.elem.nativeElement, newText); 
  }
} 

Il faut modifier la classe du composant pour injecter un objet avec le nouveau type:

import { DisplayedText } from './custom-directive'; 

@Component({ ... }) 
export class HostComponent { 
  injectedParameter: DisplayedText; 

  constructor() { 
    this.injectedParameter = new DisplayedText('Value defined from component'); 
  } 
} 

Ensuite dans le template du composant, on effectue un property binding:

<p>Host component</p> 
<custom-directive [text]='injectedParameter'></custom-directive> 

Utilisation d’un alias

Il est possible d’utiliser un alias pour éviter d’utiliser le même nom que la propriété/membre dans la directive. Dans le paramètre inputs dans le décorateur @Directive(), la syntaxe doit être:

inputs: [ '<propriété/membre dans la directive>: <attribut dans le template>' ] 

Par exemple, en utilisant le même exemple que précédemment:

import { Directive, ElementRef, Renderer2, OnInit } from '@angular/core'; 

@Directive({ 
  selector: 'custom-directive', 
  inputs: [ 'textToDisplay: inner-text' ] 
}) 
export class CustomDirective implements OnInit { 
  textToDisplay: string; 

  constructor(private elem: ElementRef, private renderer: Renderer2) {} 

  ngOnInit(): void { 
    let newText = this.renderer.createText(this.textToDisplay); 
    this.renderer.appendChild(this.elem.nativeElement, newText); 
  }
}

Dans le template du composant utilisant la directive, on utilise l’attribut inner-text au lieu d’utiliser directement le nom du membre:

<p>Host component</p> 
<custom-directive inner-text='Value defined from component'></custom-directive> 
Injecter des paramètres avec les attributs de l’élément hôte

Dans la plupart des cas suivant la formé utilisée dans le paramètre selector, les directives peuvent comporter des arguments en entrée ou en sortie. Par exemple si on considère une directive s’appliquant suivant le nom d’une classe CSS:

import { Directive, ElementRef, Renderer2, OnInit } from '@angular/core'; 

@Directive({ 
  selector: '.custom-directive', 
  inputs: [ 'textToDisplay' ]
}) 
export class CustomDirective { 
  textToDisplay: string; 

  constructor(private elem: ElementRef, private renderer: Renderer2) {} 
} 

La directive s’applique si la classe CSS "custom-directive" est appliquée, par exemple:

<span class="custom-directive"></span> 

On peut injecter les arguments dans la directive en les appliquant en tant qu’attributs de l’élément hôte qui est un élément HTML span dans cet exemple:

<span class="custom-directive" textToDisplay="Value defined from component"></span> 

@Input()

On peut utiliser une syntaxe différente pour injecter un paramètre d’entrée en utilisant le décorateur @Input() au lieu d’utiliser le paramètre inputs dans le décorateur @Directive(). Le comportement est le même qu’avec le paramètre inputs.

Pour utiliser le décorateur @Input(), il suffit de l’indiquer quand le membre est déclaré dans la classe de la directive:

@Input() <nom de la propriété/membre>: <type de la propriété/membre>; 

Par exemple, en reprenant l’exemple précédent:

import { Directive, ElementRef, Renderer2, OnInit, Input } from '@angular/ core'; 

@Directive({ 
  selector: 'custom-directive', 
}) 
export class CustomDirective implements OnInit { 
  @Input() textToDisplay: string;

  constructor(private elem: ElementRef, private renderer: Renderer2) {} 

  ngOnInit(): void { 
    let newText = this.renderer.createText(this.textToDisplay); 
    this.renderer.appendChild(this.elem.nativeElement, newText); 
  }
} 

Il n’y a pas de changement pour injecter la valeur dans le template du composant hôte:

<p>Host Component</p> 
<custom-directive textToDisplay='Value defined from component'></custom-directive> 

Utilisation d’un alias

Utiliser un alias est aussi possible avec le décorateur @Input() pour éviter d’utiliser le même nom que le membre dans la directive. La syntaxe doit être:

@Input('<alias>') <nom de la propriété/membre>: <type de la propriété/membre>;

Par exemple:

import { Directive, ElementRef, Renderer2, OnInit, Input } from '@angular/ core'; 

@Directive({ 
  selector: 'custom-directive', 
}) 
export class TestDirective implements OnInit { 
  @Input('inner-text') textToDisplay: string;

  constructor(private elem: ElementRef, private renderer: Renderer2) {} 

  ngOnInit(): void { 
    let newText = this.renderer.createText(this.textToDisplay); 
    this.renderer.appendChild(this.elem.nativeElement, newText); 
  }
} 

On doit ensuite, utiliser l’alias quand on consomme la directive:

<p>Host Component</p> 
<custom-directive inner-text='Value defined from component'></custom-directive> 
Raccourci: utiliser le même identifiant entre le selector de la directive et le paramètre

Si la condition d’application de la directive est la même que le nom du paramètre d’entrée de la directive, un seul attribut dans l’élément hôte permet d’appliquer la directive et d’injecter un argument, par exemple:

import { Directive, ElementRef, Renderer2, OnInit } from '@angular/core'; 

@Directive({ 
  selector: '[textToDisplay]',
  inputs: [ 'textToDisplay' ] 
}) 
export class CustomDirective { 
  textToDisplay: string; 

  constructor(private elem: ElementRef, private renderer: Renderer2) {} 
}

Il suffit d’utiliser un seul attribut pour utiliser la directive:

<span textToDisplay="Value defined from component"></span> 

Paramètre outputs

Ce paramètre est utilisable dans le décorateur @Directive() pour déclarer des évènements de la directive auxquels le composant hôte peut s’abonner. Le nom de la donnée membre doit être indiqué directement sous forme de chaîne de caractères dans le paramètre outputs, par exemple:

import { Directive, ElementRef, Renderer2, Output } from '@angular/ core'; 

@Directive({ 
  selector: 'custom-directive', 
  outputs: [ 'innerEvent' ]
}) 
export class CustomDirective { 
  innerEvent: EventEmitter<string> = new EventEmitter<string>();

  constructor(private elem: ElementRef, private renderer: Renderer2) {} 
} 

Pour illustrer l’abonnement à l’évènement innerEvent par le composant hôte, on va perfectionner cet exemple en permettant à la directive de rajouter un bouton clickable. Avec l’objet de type Renderer2, on rajoute un bouton; on ajoute un texte à ce bouton puis on ajoute une callback déclenchée quand on clique sur le bouton. Le code devient:

import { Directive, ElementRef, Renderer2 } from '@angular/ core'; 

@Directive({ 
  selector: 'custom-directive', 
  outputs: [ 'innerEvent' ]
}) 
export class CustomDirective { 
  innerEvent: EventEmitter<string> = new EventEmitter<string>();

  constructor(private elem: ElementRef, private renderer: Renderer2) { 
    let newButton = this.renderer.createElement('button'); 
    let buttonText = this.renderer.createText('Click Me'); 

    // On ajoute de texte au bouton 
    this.renderer.appendChild(newButton, buttonText); 

    // On ajoute la callback correspondant au clique sur le bouton 
    this.renderer.listen(newButton, 'click', () => this.buttonClicked());

    // On ajoute le bouton à l'élément 
    this.renderer.appendChild(this.elem.nativeElement, newButton); 
  } 
 
  private buttonClicked(): void { 
    this.innerEvent.emit('From inside');
  } 
} 

L’objet de type EventEmitter permet de déclencher l’évènement.

Pour s’abonner à l’évènement, le composant hôte doit effectuer un event binding. On modifie le template du composant pour s’abonner à l’évènement et pour rajouter un compteur qu’on incrémentera à chaque clique:

<p>Host Component</p> 
<custom-directive innerEvent='onDirectiveClicked($event)'></custom-directive> 
<p>Click count: {{clickCount}}</p> 

On modifie la classe du composant en rajoutant une implémentation pour la méthode onDirectiveClicked():

@Component({ ... }) 
export class HostComponent { 
  clickCount: number; 

  constructor() { 
    this.clickCount = 0; 
  } 

  onDirectiveClicked(message: string) { 
    this.clickCount++; 
    console.log(message); 
  } 
} 

Au moment de déclencher l’évènement dans la directive, on a ajouté un argument correspondant à une chaîne de caractères: 'From Inside'. On peut récupérer cet argument quand la callback onDirectiveClick() est exécutée.

Après exécution, on obtient l’affichage suivant:

Utilisation d’un alias

Il est possible d’utiliser un alias pour éviter d’utiliser le même nom que le membre dans la directive. Dans le paramètre outputs dans le décorateur @Directive(), la syntaxe doit être:

outputs: [ '<nom de la propriété/membre dans la directive>: <attribut dans le template>' ] 

Par exemple, en utilisant le même exemple que précédemment:

import { Directive, ElementRef, Renderer2 } from '@angular/ core'; 

@Directive({ 
  selector: 'custom-directive', 
  outputs: [ 'innerEvent: inner-event' ]
}) 
export class CustomDirective { 
  innerEvent: EventEmitter<string> = new EventEmitter<string>(); 

  // ... 
} 

On doit ensuite, utiliser l’alias quand on consomme la directive:

<p>Host Component</p> 
<custom-directive inner-event='onDirectiveClicked($event)'></custom-directive> 
<p>Click count: {{clickCount}}</p> 

@Output()

On peut utiliser une syntaxe différente pour déclarer un évènement en utilisant le décorateur @Output() au lieu d’utiliser le paramètre outputs dans le décorateur @Directive(). Le comportement est le même qu’avec le paramètre outputs.

Pour utiliser le décorateur @Output(), il suffit de l’indiquer quand le membre est déclaré dans la classe de la directive:

@Output() <nom de la propriété/membre>: <type de la propriété/membre>; 

Par exemple, en reprenant l’exemple précédent:

import { Directive, ElementRef, Renderer2, Output } from '@angular/ core'; 

@Directive({ 
  selector: 'custom-directive' 
}) 
export class CustomDirective { 
  @Output() innerEvent: EventEmitter<string> = new EventEmitter<string>();
 
  constructor(private elem: ElementRef, private renderer: Renderer2) { 
    let newButton = this.renderer.createElement('button'); 
    let buttonText = this.renderer.createText('Click Me'); 

    this.renderer.appendChild(newButton, buttonText); 
    this.renderer.listen(newButton, 'click', () => this.buttonClicked()); 
    this.renderer.appendChild(this.elem.nativeElement, newButton); 
  } 

  private buttonClicked(): void { 
    this.innerEvent.emit('From inside'); 
  } 
} 

Il n’y a pas de changements pour injecter la valeur dans le template du composant hôte:

<p>Host Component</p> 
<custom-directive innerEvent='onDirectiveClicked($event)'></custom-directive> 
<p>Click count: {{clickCount}}</p> 

Utilisation d’un alias

Utiliser un alias est aussi possible avec le décorateur @Output() pour éviter d’utiliser le même nom que le membre dans la directive. La syntaxe doit être:

@Output('<alias>') <nom de la propriété/membre>: <type de la propriété/membre>;

Par exemple:

import { Directive, ElementRef, Renderer2, Output } from '@angular/ core'; 

@Directive({ 
  selector: 'custom-directive' 
}) 
export class CustomDirective { 
  @Output('inner-event') innerEvent: EventEmitter<string> = new EventEmitter<string>(); 

  // ... 
} 

On doit ensuite, utiliser l’alias quand on consomme la directive:

<p>Host Component</p> 
<custom-directive inner-event='onDirectiveClicked($event)'></custom-directive> 
<p>Click count: {{clickCount}}</p> 

Paramètre providers

Le paramètre providers dans le décorateur @Directive() permet de configurer les injecteurs de la directive pour indiquer le provider de paramètres injectés par injection de dépendances. Voir la partie Injection de dépendances pour plus de détails.

@HostBinding()

Le décorateur @HostBinding() dans une directive permet d’effectuer un binding entre un membre de la directive et une propriété de l’objet du DOM qui est l’hôte de la directive.

Ainsi on définit la directive suivante utilisable en tant qu’attribut d’un élément HTML, on rajoute une propriété avec le décorateur @HostBinding() pour effectuer un binding entre la propriété style.background-color de l’élément hôte de la directive:

import { Directive, ElementRef, Renderer2, HostBinding } from '@angular/ core'; 

@Directive({ 
  selector: '[custom-directive]' 
}) 
export class CustomDirective { 
  @HostBinding('style.background-color') backgroundColor: string = 'lightgreen';
} 

En utilisant la directive en tant qu’attribut d’un élément HTML, la couleur d’arrière plain sera modifiée:

<p>Host Component</p> 
<p custom-directive>Text with background</p> 

Le résultat est du type:

@HostListener()

Le décorateur @HostListener() permet à la directive de s’abonner à un évènement du DOM de façon à ce qu’une fonction soit exécutée quand l’évènement est déclenché.

La directive suivante est utilisable en tant qu’attribut d’un élément HTML, on ajoute une méthode avec l’attribut @HostListener() de façon à déclencher son exécution si on clique sur l’élément hôte:

import { Directive, ElementRef, Renderer2, HostBinding } from '@angular/ core'; 

@Directive({ 
  selector: '[custom-directive]' 
}) 
export class CustomDirective { 
  @HostListener('click') onHostClicked(triggeredEvent) : void { 
    console.log(triggeredEvent);
  } 
} 

On utilise la directive en tant qu’attribut d’un élément HTML:

<p>Host Component</p> 
<p custom-directive>Click on the text</p> 

A l’exécution, l’affichage est:

Si on clique sur le texte, on verra les détails de l’évènement dans la console du browser:

On peut avoir une liste exhaustive des évènements des objets du DOM sur lesquels on peut utiliser le décorateur @HostListener() sur cette page: www.w3schools.com/jsref/dom_obj_event.asp.

Attribute directives

Les directives attribut (i.e. attribute directive) peuvent manipuler le DOM et les propriétés des éléments du DOM en utilisant les objets Angular ElementRef et Renderer2.

ElementRef et Renderer2

Dans le code de l’exemple présenté plus haut, 2 objets de type ElementRef et Renderer2 sont injectés dans la directive:

import { Directive, ElementRef, Renderer2 } from '@angular/core'; 

@Directive({ 
  selector: 'custom-directive' 
}) 
export class CustomDirective { 
  constructor(private elem: ElementRef, private renderer: Renderer2) {  
    let newText = renderer.createText('Text from directive');  
    renderer.appendChild(elem.nativeElement, newText); 
  }
} 

Ainsi:

  • Le paramètre injecté de type ElementRef est un wrapper de l’objet du DOM. La propriété ElementRef.nativeElement permet d’accéder directement à l’objet du DOM.
  • Le paramètre injecté de type Renderer2 permet d’obtenir une classe permettant d’effectuer des modifications sur l’objet du DOM.

Le type Renderer2 comporte de nombreuses fonctions pour modifier les propriétés d’objets dans le DOM comme:

  • Pour créer un nouvel élément:
    • createElement(): crée un élément HTML comme <div>, <ul>, etc…
    • createText(): crée un texte à rajouter à un élément existant comme on peut le voir dans l’exemple précédent.
  • Pour modifier l’emplacement des éléments dans le DOM:
    • appendChild() rajoute un nouvel élément à un élément existant.
    • insertBefore() rajoute un nouvel élément avant un élément et dans un élément parent.
    • removeChild() supprime un élément à l’intérieur d’un élément parent.
  • Pour trouver des éléments:
    • selectRootElement() retourne l’élément de plus haut niveau dans le DOM à partir du nom d’un élément enfant.
    • parentNode() renvoie le nœud parent d’un élément.
  • Pour modifier les éléments:
    • setAttribute() ajoute un attribut à un élément. Le terme “attribut” est utilisé pour qualifier l’attribut d’un élément HTML.
    • removeAttribute() supprime un attribut d’un élément. Le terme attribut est utilisé pour qualifier l’attribut d’un élément HTML.
    • setProperty() permet d’affecter une valeur à une propriété d’un objet dans le DOM.
    • addClass() rajoute une classe CSS à un élément.
    • removeClass() supprime une classe CSS d’un élément.
    • setStyle() rajoute un style en inline dans un élément HTML (sous forme d’attribut).
    • removeStyle() supprime l’attribut style en inline d’un élément HTML.
    • listen() pour ajouter un évènement à un élément.

Exemple de directive attribut

On se propose d’implémenter un exemple qui va renseigner les différents élément li (i.e. list item) d’une liste ordonnée ol (i.e. ordered list). La liste d’élément correspond à une liste d’utilisateur. L’utilisateur est affiché si la propriété User.canBeDisplayed est égale à true.

L’implémentation de la directive est:

export class User {
  constructor(public firstName: string, public lastName: string, 
    public canBeDisplayed: boolean) {}
}

@Directive({
  selector: '[displayUsers]'
})
export class DisplayUser implements OnInit {
  @Input('displayUsers') users: User[];

  constructor(private elem: ElementRef, private rendered: Renderer2) {}

  ngOnInit(): void {
    if (this.users) {
      this.users.forEach(user => {
        if (user.canBeDisplayed) {
          let newListItemElement = this.renderer.createElement('li');
          let listItemText = this.renderer.createText(user.firstName + ' ' +
            user.lastName);
          this.renderer.appendChild(newListItemElement, listItemText);
          this.renderer.appendChild(this.elem.nativeElement, newListItemElement);
        }
      });
    }
  }
}

L’implémentation du composant est:

Template Classe du composant
<p>Users are:</p>
<ol [displayUsers]="userList"></ol>
@Component({ ... }) 
export class HostComponent {
  userList: User[];

  constructor() {
    this.userList = [
      new User('Beatrix', 'Kiddo', true),
      new User('Bill', 'Bill', true),
      new User('Elle', 'Driver', false),
      new User('O-Ren', 'Ishii', true)
    ]
  }
}

Dans cet exemple, on crée une directive attribut pour ajouter des éléments HTML li (i.e. list item) pour afficher une liste d’utilisateurs:

<ol>
  <li><Prénom> <Nom></li>
  <li><Prénom> <Nom></li>
  <!-- ... -->
  <li><Prénom> <Nom></li>
</ol>

Ainsi:

  • Dans la directive, on injecte ElementRef et Renderer2 corrrespondant, respectivement, à l’objet dans lequel on va rajouter les éléments li et l’objet qui va permettre d’effectuer les modifications dans l’objet ElementRef.
  • Le paramètre selector de la directive '[displayUsers]' permet d’effectuer la résolution de la directive si un élément HTML contient un attribut displayUsers ou [displayUsers] (cette dernière forme permettant d’effectuer un property binding dans le template du composant).
  • Dans le corps de la méthode ngOnInit(), on implémente l’ajout des éléments li à proprement parlé.
  • Dans le template du composant, on ajoute l’élément HTML ol (i.e. ordered list) pour lister les éléments, on ajoute l’attribut [displayUsers] pour permettre la résolution de la directive et ajouter un property binding avec la propriété userList dans la classe du composant.

Le résultat est:

Structural directives

Les directives structurelles (i.e. structural directive) sont plus complexes à implémenter que les directives attribut car elles passent par l’intermédiaire d’un modèle (i.e. template) pour générer les éléments du DOM correspondant. En injectant des données dans ce modèle, la directive sera capable de générer plusieurs éléments du DOM.

Le modèle utilisé par les directives structurelles est objet de type TemplateRef identifié dans les vues par <ng-template>. La plupart du temps, <ng-template> n’apparaît pas explicitement dans les vues des composants hôte qui utilisent la directive. La plupart de temps, la microsyntaxe permet d’en exploiter les fonctionnalités les plus importantes.

Avant de rentrer dans les détails de l’implémentation des directives structurelles, on va expliquer comment <ng-template> fonctionne. Il n’est pas indispensable de connaître toutes les fonctionnalités de cet objet pour l’utiliser.

<ng-template>

<ng-template> est un objet complexe qui permet:

  • d’implémenter un modèle pour créer des vues et
  • d’utiliser ce modèle pour créer des vues.

Les vues créées ne sont pas de même niveau que les vues d’un composant, ce sont des vues intégrées (i.e. embedded view) qui sont placées dans une vue. Ces vues intégrées sont ajoutées dans le DOM en fonction de l’implémentation du modèle.

Par exemple, si on considère un composant suivant:

Template Classe du composant
<p>Host component</p> 
<ng-template> 
  <p>Template content</p> 
</ng-template> 
@Component({ ... }) 
export class HostComponent {} 

A l’exécution, il n’y aura aucun contenu correspondant à la partie <ng-template> ... </ng-template> car pour que la classe TemplateRef génère une vue intégrée (i.e. embedded view), il faut la créer explicitement par programmation. Pour créer une vue, il faut créer un view container (i.e. “conteneur de vue”) qui va servir d’hôte à la vue intégrée.

Affichage Code HTML généré

View container

Un view container (i.e. “conteneur de vue”) est capable d’utiliser un modèle de type TemplateRef pour créer une vue. Ainsi un modèle a besoin d’un conteneur pour produire une vue. Les vues, view containers et vues intégrées forment une hiérarchie où les uns sont imbriqués dans les autres.

Cette hiérarchie forme un arbre appelée arbre hiérarchique des vues (i.e. view hierarchy tree). Angular forme cet arbre en mémoire de façon à avoir une idée de l’ordre d’imbrication des éléments. Cette arbre est différent du DOM toutefois les modifications apportées dans l’arbre hiérarchique des vues sont répercutées dans le DOM, le browser répercute ces modifications ensuite dans l’affichage. De même les modifications dans le DOM sont scrutées par Angular et sont répercutées dans l’arbre hiérarchique des vues.

Pour qu’un vue provenant d’un modèle soit affichée et qu’elle soit rajoutée dans le DOM, elle doit se trouver dans un view container. Au moment où on sollicite le modèle, on peut:

  • appeler explicitement le view container pour qu’il crée la vue ou
  • Angular trouve le view container correspondant au modèle de façon à ce qu’il soit appelé pour créer la vue.

L’arbre hiérarchique des vues sert ainsi à savoir quelles sont les imbrications entre les éléments pour déterminer quelle peut être le view container correspondant à un modèle.

L’exemple plus haut n’affiche rien car aucun view container n’a été sollicité pour créer la vue correspondant au modèle. Pour qu’une vue intégrée soit créée, il faut explicitement le faire par programmation en utilisant la fonction ViewContainerRef.createEmbeddedView(), par exemple:

<p>Host component</p> 
<ng-template #template> 
  <p>Template content</p> 
</ng-template> 
<div #viewContainer></div> 
@Component({ ... }) 
export class HostComponent implements AfterViewInit { 
  @ViewChild('viewContainer', { read: ViewContainerRef }) viewContainer: ViewContainerRef; 
  @ViewChild('template', { read: TemplateRef }) template: TemplateRef<any>; 

  ngAfterViewInit(): void { 
    this.viewContainer.createEmbeddedView(this.template); 
  } 
} 

Dans ce code:

  • On rajoute l’élément <div #viewContainer> avec une variable référence de façon à pouvoir effectuer un binding avec la classe du composant. Cet élément va servir de view container au modèle.
  • On ajoute une variable référence au modèle <ng-template #template> de façon à effectuer un binding avec la classe du composant.
  • Avec les décorateurs @ViewChild(), on va créer des bindings entre les éléments du template et des membres de la classe du composant.
  • A l’instanciation des objets décorés avec @ViewChild() c’est-à-dire quand la méthode ngAfterViewInit() est déclenchée, on appelle explicitement ViewContainerRef.createEmbeddedView() pour créer la vue intégrée avec le modèle en paramètre et l’ajouter au view container.

L’affichage et le DOM contiennent le contenu du modèle:

Affichage Code HTML généré

Dans l’exemple précédent, on précise explicitement le view container qui est l’élément HTML <div> toutefois on peut aussi utiliser une vue fournie par Angular par injection. Par exemple, si on déclare un objet de type ViewContainerRef dans le constructeur du composant, Angular va injecter la vue parente du composant:

<p>Host component</p> 
<ng-template #template> 
  <p>Template content</p> 
</ng-template> 
<div #viewContainer></div> 
@Component({ ... }) 
export class HostComponent implements AfterViewInit { 
  @ViewChild('template', { read: TemplateRef }) template: TemplateRef<any>;  

  constructor(private parentViewRef: ViewContainerRef) {} 

  ngAfterViewInit(): void { 
    this.parentViewRef.createEmbeddedView(this.template);
  } 
} 

Le template sera, ainsi, rajouté à la vue parente du composant:

Affichage Code HTML généré

Injecter des données dans le modèle avec le contexte

Pour rendre le modèle moins statique, ngTemplate permet de déclarer des variables dont la valeur sera affectée en fonction d’un contexte. Ce contexte sera injecté dans le template de l’extérieur.

Pour déclarer des variables dans le modèle, il faut passer par des attributs et utiliser une syntaxe particulière avec let:

let-<nom de la variable>="<valeur de la variable>" 

Par exemple, pour définir 2 variables nommées contentPrefix et contentSuffix, la déclaration sera:

<ng-template 
  let-contentPrefix="messageContentPrefix" 
  let-contentSuffix="messageContentSuffix"> 
  <p>Content prefix: {{contentPrefix}}</p> 
  <p>Content suffix: {{contentSuffix}}</p> 
</ng-template> 
<div #viewContainer></div> 

Dans cet exemple, les valeurs des variables proviennent des propriétés messageContentPrefix et messageContentSuffix présentent dans le contexte. On peut affecter le contexte au moment de créer la vue intégrée avec ViewContentRef.createEmbeddedView():

@Component({ ... }) 
export class HostComponent implements AfterViewInit { 
  @ViewChild('template', { read: TemplateRef }) template: TemplateRef<any>; 
  @ViewChild('viewContainer', { read: ViewContainerRef }) viewContainer: ViewContainerRef; 

  ngAfterViewInit(): void { 
    this.parentViewRef.createEmbeddedView(this.template, 
    { 
      messageContentPrefix: 'This message is from...', 
      messageContentSuffix: '...the component.' 
    }); 
  } 
} 

Le contexte est injecté par l’intérmédiaire d’un object literal dont les propriétés correspondent à celles indiquées dans les déclarations du modèle.

Dans cet exemple, le contexte est un object literal car le modèle est de type TemplateRef<any>. Le type any donne la possibilité d’utiliser un contexte de n’importe quel type. On peut utiliser un contexte d’un type plus précis en le précisant dans le type du modèle (l’exemple de directive structurelle plus bas utilise un type plus précis).

En réalité cet exemple ne fonctionne pas et produit une erreur visible sur la console du browser du type:

ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. 
 Previous value: 'undefined'. Current value: 'This message is from...'. It seems like
 the view has been created after its parent and its children have been dirty checked. 
 Has it been created in a change detection hook?

Cette erreur se produit car le cycle de vie des objets de la vue n’est pas tout à fait respecté.

Détection des changements et cycle de vie d’une vue

Durant le cycle de vie d’une vue et pour afficher les différents éléments, un certain nombre d’évènements sont déclenchés successivement dont:

  1. ngOnInit() est déclenchée quand Angular affiche les propriétés par data binding et affecte les données d’entrée dans les composants ou les directives.
  2. ngDoCheck(): cette méthode est déclenchée à chaque fois qu’Angular détecte un changement nécessitant de mettre à jour une vue.
  3. ngAfterContentInit() est déclenchée après projection de contenu externe dans la vue d’un composant.
  4. ngAfterViewInit() est exécutée quand Angular initialise les vues et les vues enfant d’un composant.

Angular utilise la succession de ces évènements de façon à détecter les changements pour savoir quand les données affichées à l’écran ont été mises à jour. La détection de changement dans les données permet de mettre à jour l’affichage. A chaque détection d’un changement, la méthode ngDoCheck() sera exécutée.

Ainsi:

  • Le modèle se trouve dans une vue intégrée et est affiché juste avant le déclenchement de la méthode ngAfterViewInit().
  • De même, les propriétés ayant un binding avec des éléments sur la vue avec le décorateur @ViewChild() sont initialisées juste avant le déclenchement de l’évènement ngAfterViewInit().

La vue intégrée est créée, toutefois son contenu est modifié à cause de l’injection du contexte ce qui entraîne la détection d’un changement de données par Angular. Angular se comporte mal quand des changements sont détectées après exécution de la méthode ngAfterViewInit() d’où le message d’erreur ExpressionChangedAfterItHasBeenCheckedError lors de l’exécution de l’exemple précédent.

Une solution pour corriger cette erreur consiste à utiliser l’option { static: true } (disponible à partir d’Angular 8) au moment de déclarer les propriétés avec le décorateur @ViewChild():

@ViewChild('template', { read: TemplateRef, static: true }) template: TemplateRef<any>; 
@ViewChild('viewContainer', { read: ViewContainerRef, static: true }) viewContainer: ViewContainerRef; 

Avec le décorateur @ViewChild(), une résolution est effectuée pour trouver à quel élément dans la vue, la propriété doit être bindée. Suivant la valeur de l’option { static: true }, le comportement est le suivant:

  • true: la résolution est effectuée avant l’exécution de la détection des changements.
  • false (valeur par défaut): la résolution est effectuée après détection des changements.

Ainsi, avec { static: true }, les propriétés sont initialisées avant l’exécution de l’évènement ngOnInit() soit bien avant le déclenchement de ngAfterViewInit(). En corrigeant l’exemple précédent en plaçant la création de la vue intégrée dans ngOnInit(), on évite ainsi l’erreur:

  • Template:
    <ng-template #template 
      let-contentPrefix="messageContentPrefix" 
      let-contentSuffix="messageContentSuffix"> 
      <p>Content prefix: {{contentPrefix}}</p> 
      <p>Content suffix: {{contentSuffix}}</p> 
    </ng-template> 
  • Classe du composant:
    @Component({ ... }) 
    export class HostComponent implements OnInit { 
      @ViewChild('template', { read: TemplateRef, static: true }) 
        template: TemplateRef<any>; 
      @ViewChild('viewContainer', { read: ViewContainerRef, static: true }) 
        viewContainer: ViewContainerRef; 
    
      ngOnInit(): void { 
        this.parentViewRef.createEmbeddedView(this.template, 
        { 
          messageContentPrefix: 'This message is from...', 
          messageContentSuffix: '...the component.' 
        }); 
      } 
    } 
    

Le résultat est:

$implicit

Quand on déclare les variables utilisées dans le modèle, il n’est pas obligatoire d’indiquer explicitement de quelle propriété du contexte provient la valeur de la variable. Dans le contexte, on peut déclarer un propriété comme étant implicite en la nommant $implicit, ainsi toutes les variables pour lesquelles on ne précise pas la propriété d’où doit provenir la valeur, auront la valeur de la propriété $implicit.

Par exemple, si le modèle est déclaré de la façon suivante:

<ng-template #template
  let-contentPrefix="messageContentPrefix" 
  let-contentSuffix="messageContentSuffix"> 
  <p>Content prefix: {{contentPrefix}}</p> 
  <p>Content suffix: {{contentSuffix}}</p> 
</ng-template> 

La valeur des variables:

  • contentPrefix provient de la propriété messageContentPrefix dans le contexte.
  • contentSuffix provient de la propriété messageContentSuffix dans le contexte.

Le contexte doit comporter les 2 propriétés:

this.parentViewRef.createEmbeddedView(this.template, 
{ 
  messageContentPrefix: 'This message is from...', 
  messageContentSuffix: '...the component.' 
}); 

Si on considère que la propriété messageContentPrefix est implicite alors on peut omettre d’utiliser son nom dans le modèle:

<ng-template let-contentPrefix let-contentSuffix="messageContentSuffix"> 
  <p>Content prefix: {{contentPrefix}}</p> 
  <p>Content suffix: {{contentSuffix}}</p> 
</ng-template> 

Le contexte doit comporter une propriété implicite:

this.parentViewRef.createEmbeddedView(this.template, 
{ 
  $implicit: 'This message is from...',
  messageContentSuffix: '...the component.' 
}); 

Cette modification de l’implémentation ne change pas le comportement du modèle.

ngTemplateOulet et ngTemplateOutletContext

<ng-template> est un modèle mais il peut se comporter aussi comme une vue intégrée en utilisant les attributs ngTemplateOulet et ngTemplateOutletContext:

  • ngTemplateOutlet permet d’implémenter un modèle.
  • ngTemplateOutletContext permet d’affecter le contexte.

Ainsi, si on écrit:

<ng-template #viewTemplate let-content> 
  <p>Template content: {{content}}</p> 
</ng-template> 
<ng-template  
  [ngTemplateOutlet]="viewTemplate" 
  [ngTemplateOutletContext]="{ $implicit: 'OK' }"> 
</ng-template> 

L’élément avec la variable référence #viewTemplate permet de définir un modèle et le 2e élément <ng-template> permet de construire une vue intégrée en utilisant le modèle et le contexte.

Avec cette construction, il n’est pas nécessaire d’appeler explicitement la fonction ViewContainerRef.createEmbeddedView() pour créer la vue intégrée.

Le résultat est:

Affichage Code HTML généré

On peut affecter les valeurs des attributs ngTemplateOutlet et ngTemplateOutletContext par property binding. En modifiant l’exemple précédent:

<ng-template #viewTemplate let-content> 
  <p>Template content: {{content}}</p> 
</ng-template> 
<ng-template  
  [ngTemplateOutlet]="template" 
  [ngTemplateOutletContext]="context"> 
</ng-template> 
@Component({ ... }) 
export class HostComponent { 
  @ViewChild('viewTemplate', { read: TemplateRef, static: true }) 
    template: TemplateRef<any>; 

  context = { $implicit: 'OK' }; 
} 

Le résultat est le même que précédemment.

Utiliser ngTemplate avec une directive

Les composants sont des directives avec une vue, ainsi pour créer une vue intégrée (i.e. “embedded view”) à partir d’une directive:

  • On peut définir le modèle dans la vue d’un composant hôte,
  • Déclarer une directive structurelle en l’utilisant dans la vue du composant hôte,
  • Créer la vue intégrée par programmation à partir du modèle dans la directive.

Par exemple, si l’implémentation de la directive est:

@Directive({ 
  selector: '[template-directive]' 
})
export class TemplateDirective implements OnInit { 
  constructor(private parentViewRef: ViewContainerRef, private template: TemplateRef) {} 

  ngOnInit: void { 
    this.parentViewRef.createEmbeddedView(this.template, { $implicit: 'OK' }); 
  } 
} 

Et l’implémentation du composant hôte:

Template Classe du composant
<p>Host component</p> 
<ng-template template-directive let-content> 
  <p>Template content: {{content}}</p> 
</ng-template> 
@Component({ ... }) 
export class HostComponent {} 

Dans cet exemple:

  • On définit la directive structurelle (i.e. structural directive) TemplateDirective dont le paramètre selector '[template-directive]'.
  • Le constructeur de la directive permet d’injecter les paramètres:
    • parentViewRef contenant la vue parente c’est-à-dire la vue du composant hôte.
    • template contenant le modèle qui sera utilisé pour créer la vue dans la directive.
  • On implémente le modèle dans la vue du composant avec:
    • template-directive qui est l’attribut pour que la directive TemplateDirective soit utilisée.
    • let-content pour déclarer la variable content dont la valeur est affectée par la propriété $implicit du contexte.
  • La vue intégrée du modèle est créée dans la directive dans le corps de la méthode ngOnInit().

Si on ajoute un paramètre nommée textToDisplay en entrée de la directive pour renseigner la texte à afficher, le code devient:

@Directive({ 
  selector: '[template-directive]' 
}) 

export class TemplateDirective implements OnInit { 
  @Input() textToDisplay: string; 

  constructor(private parentViewRef: ViewContainerRef, private template: TemplateRef) {} 

  ngOnInit: void { 
    this.parentViewRef.createEmbeddedView(this.template, { $implicit: this.textToDisplay }); 
  } 
} 

Pour le composant hôte:

Template Classe du composant
<p>Host component</p> 
<ng-template template-directive 
  let-content textToDisplay="OK"> 
  <p>Template content: {{content}}</p> 
</ng-template> 
@Component({ ... }) 
export class HostComponent {} 

Enfin si on utilise un raccourci en utilisant le même identifiant entre le selector de la directive et le paramètre d’entrée, on peut simplifier la déclaration du modèle:

@Directive({ 
  selector: '[template-directive]' 
}) 

export class TemplateDirective implements OnInit { 
  @Input("template-directive") textToDisplay: string; 

  constructor(private parentViewRef: ViewContainerRef, private template: TemplateRef<any>) {} 

  ngOnInit: void { 
    this.parentViewRef.createEmbeddedView(this.template, { $implicit: this.textToDisplay }); 
  } 
} 

L’implémentation du composant hôte:

Template Classe du composant
<p>Host component</p> 
<ng-template template-directive="OK" let-content> 
  <p>Template content: {{content}}</p> 
</ng-template> 
@Component({ ... }) 
export class HostComponent {} 

Implémentation d’une directive structurelle

Une directive structurelle permet de créer des vues intégrées (i.e. “embedded view”) à partir d’un modèle. En injectant un contexte dans le modèle, une directive structurelle sera capable de créer une vue et de l’intégrer dans la vue d’un composant hôte.

L’implémentation d’une directive structurelle est similaire à l’exemple utilisé dans le paragraphe précédent:

  • On injecte les paramètres:
  • La directive crée elle-même la vue intégrée en exécutant la fonction ViewContainerRef.createEmbeddedView() et en fournissant le contexte.
  • On utilise une syntaxe particulière pour définir le modèle dans la vue du composant hôte. La syntaxe permet de définir à la fois un modèle et appeler la directive structurelle en utilisant un raccourci: *<nom de la directive structurelle>.

    Par exemple, si on utilise la syntaxe suivante:

    <p *templateDirective>Template content.</p> 
    

    Elle est équivalent à:

    <ng-template templateDirective> 
      <p>Template content</p> 
    </ng-template> 
    

Configurer une directive structurelle par microsyntaxe

Une écriture en microsyntaxe permet de se passer d’utiliser <ng-template></ng-template> pour définir le modèle. Cette microsyntaxe permet d’effectuer des affectations et d’exécuter des expressions en une ligne. Elle n’est qu’un raccourci syntaxique pour faciliter l’implémentation, son utilisation n’est pas obligatoire et il existe toujours un équivalent dans une syntaxe explicite avec <ng-template></ng-template>.

Par exemple:

<ng-template testDirective> 
  <p>Template content.</p> 
</ng-template> 

Est équivalent à:

<p *testDirective>Template Content.</p> 

La microsyntaxe n’est pas aussi facile que d’utiliser la syntaxe explicite avec ngTemplate car certaines règles sont implicites en utilisant la microsyntaxe, il faut avoir en tête ces règles pour ne pas être surpris par certains comportements.

D’une façon générale, pour utiliser la microsyntaxe, il faut respecter la syntaxe suivante:

*<nom paramètre selector de la directive>="<suite d'opérations en microsyntaxe>"

La partie correspondant à la suite d’opérations n’est pas obligatoire, on peut utiliser seulement la directive structurelle comme on l’a fait dans l’exemple plus haut avec *<nom paramètre selector de la directive>.

La suite d’opérations (le terme “opération” n’est pas utilisé dans la documentation Angular) doit respecter certaines règles:

  • Règle 1: si l’écriture en microsyntaxe retourne une valeur alors la directive doit comporter un paramètre d’entrée avec le même identifiant que son paramètre selector: lorsqu’on écrit: *testDirective="'OK'", implicitement cela signifie qu’on affecte une valeur à un paramètre d’entrée à la directive nommé testDirective (le même nom que le paramètre selector de la directive). Cette écriture implique plusieurs choses:
    • Que le paramètre d’entrée avec un nom ou un alias 'testDirective' existe dans la directive sinon une erreur est générée.
    • Que la valeur de l’attribut soit une valeur affectable au paramètre d’entrée. Dans notre exemple la valeur est une chaîne de caractère 'OK' mais la condition est toujours valable même dans le cas où on utilise la microsyntaxe.

    Ainsi, si on écrit:

    <p *testDirective="'This is a directive input message.'">Template content.</p> 
    

    Cette implémentation est équivalente à:

    <ng-template [testDirective]="'This is a directive input message.'"> 
      <p>Template content.</p> 
    </ng-template> 
    

    La directive doit comporter un paramètre d’entrée nommé testDirective ou un alias 'testDirective' comme dans l’exemple suivant:

    @Directive({ 
      selector: '[testDirective]' 
    }) 
    
    export class TestDirective implements OnInit { 
      @Input('testDirective') templateContent: string; 
     
      constructor(private parentViewRef: ViewContainerRef, 
        private templateRef: TemplateRef<any>) {} 
    
      ngOnInit(): void { 
        console.log(this.templateContent); 
        this.parentViewRef.createEmbeddedView(this.templateRef); 
      } 
    } 
    
  • Règle 2: il suffit que la 1ère opération fournisse une valeur pour que toute la suite d’opérations en microsyntaxe fournisse une valeur: pour que la suite d’opérations de la microsyntaxe fournisse une valeur il suffit que la 1ère opération fournisse une valeur. Il n’est pas obligatoire que les opérations fournissent elles aussi une valeur.

    Par exemple, si on écrit: *testDirective="'OK'; customValue as content", il y a 2 opérations mais seule la première fournit une valeur.

    Si il n’y a pas de paramètres d’entrée avec le même identifiant que la directive, il n’est pas nécessaire que la suite d’opération fournisse une valeur.

    Une opération générant une valeur peut être de nature différente:

    • Une valeur simple comme une chaîne de caractères (comme 'OK'), un nombre etc…
    • Une expression (i.e. “Template expression” dans la documentation): il s’agit d’une expression Javascript pouvant être une opération, par exemple 3+4, 4>5, 'Message ' + 'content' etc…
    • La déclaration d’une variable locale de la directive: let content permet de déclarer la variable locale et cette opération génère la valeur 'undefined'. Ainsi si on écrit: *testDirective="let content", la valeur fournit par la microsyntaxe est undefined.
    • Une propriété existante dans la classe du composant: si on écrit *testDirective="parameter", parameter est une propriété du composant et sa valeur est utilisée dans le template par property binding.
  • Règle 3: les caractères ';', ':' ou retour à la ligne sont facultatifs et ne servent pas dans l’implémentation de la directive: ils sont utilisés seulement pour faciliter la compréhension de la microsyntaxe, ainsi:
    *testDirective="3+4; let content" 
    

    Est équivalent à:

    *testDirective="3+4 let content" 
    

Les opérations de la microsyntaxe permettent de:

  • Déclarer des variables locales du modèle avec let ou as,
  • Affecter des valeurs en paramètres d’entrée de la directive,
  • Exécuter des expressions.

Déclarer des variables locales du modèle avec let
Pour déclarer des variables locales, il faut utiliser la syntaxe:

  • En utilisant la propriété $implicit du contexte: let <nom de la variable>. Cette déclaration est équivalente à let-<nom de la variable> si on utilise la syntaxe <ng-template>.
  • En utilisant une propriété nommée du contexte: let <nom de la variable> = <propriété dans le contexte>. Cette déclaration est équivalente à let-<nom de la variable>="<propriété dans le contexte>" si on utilise la syntaxe <ng-template>.

Par exemple:

<p *testDirective="let contentPrefix; let contentSuffix = messageContentSuffix">
  Message is:{{contentPrefix}} and {{contentSuffix}}
</p> 

Est équivalent à:

<ng-template [testDirective] let-contentPrefix let-contentSuffix="messageContentSuffix"> 
  <p>Message is:{{contentPrefix}} and {{contentSuffix}}</p> 
</ng-template> 

Dans cet exemple, on déclare 2 variables locales contentPrefix et contentSuffix. Les valeurs de ces variables sont assignées en fonction du contexte à partir des propriétés $implicit et messageContentSuffix.

Affecter des valeurs en paramètres d’entrée de la directive
La syntaxe pour affecter des valeurs aux paramètres d’entrée de la directive est:

<clé du paramètre> <expression retournant une valeur> 

Souvent la notation peut inclure ':' toutefois ce caractère est optionnel (cf. Règle 3):

<clé du paramètre>: <expression retournant une valeur> 

Cette notation ne fournit pas de valeurs dans le cas de l’application de la règle 1 et 2.

La clé de la variable est utilisée pour déterminer le nom de la propriété de la directive ou de son alias:

<selector de la directive><clé avec la 1ère lettre en majuscule> 

Si le nom de la propriété n’est pas respecté, le binding ne sera pas effectué et on aura un warning d’erreur dans la console du browser avec le message:

Can't bind to '<nom attendu de la propriété>' si ce it isn't a known property of 
  '<nom de l'élément HTML>'.

Ainsi si on écrit:

<p *testDirective="inputParameter 'This is the value of the parameter'">
  Template content
</p> 

Dans la microsyntaxe, inputParameter 'This is the value of the parameter' permet de renseigner la propriété de la directive avec le nom testDirectiveInputParameter ou l’alias 'testDirectiveInputParameter'.

La notation précédente est équivalente à:

<ng-template [testDirective] [testDirectiveInputParameter]="'This is the value of the parameter'"> 
  <p>Template content</p> 
</ng-template> 

L’implémentation de la directive est:

@Directive({ 
  selector: '[testDirective]' 
}) 
export class TestDirective implements OnInit { 
  @Input('testDirectiveInputParameter') inputParameter: string; 

  constructor(private parentViewRef: ViewContainerRef, 
    private templateRef: TemplateRef<any>) {} 

  ngOnInit(): void { 
    console.log(this.inputParameter); 
  } 
} 

Dans la console de développement du browser, on peut voir:

This is the directive input content.

Déclarer des variables locales du modèle avec as
On peut déclarer des variables locales dans le modèle avec le mot clé as. La syntaxe générale est:

<nom de la propriété dans le contexte> as <nom variable locale> 

Cette notation est équivalente à l’utilisation de let:

let <nom variable locale> = <nom de la propriété dans le contexte> 

La différence avec let est que as retourne une valeur dans le cas de la règle 1 et 2. let retourne undefined. La valeur est le résultat de l’expression.

Par exemple, les 2 notations suivantes sont équivalentes toutefois l’utilisation de as retourne une valeur:

<p *testDirective="let content = inputParameter">Template content: {{content}}</p> 
<p *testDirective="inputParameter as content">Template content: {{content}}</p> 

Dans les 2 cas, on utilise la propriété inputParameter du contexte pour affecter une valeur à la variable locale du modèle content. Dans le dernier cas de cet exemple, la microsyntaxe retournera la valeur de inputParameter.

Un grand intérêt de as par rapport à let est de pouvoir utiliser des expressions:

<expression> as <nom variable locale> 

Ainsi as s’utilise de 3 façons:

Ou

Ou

Par exemple, si on écrit:

<p *testDirective="inputParameter 'This is the value of the parameter' as content">
  Template content: {{content}}
</p> 

La microsyntaxe permet d’effectuer 2 opérations:

  • Affecter une valeur à un paramètre d’entrée de la directive avec inputParameter 'This is the value of the parameter' et
  • Utiliser la propriété inputParameter du contexte pour affecter une valeur à la variable locale du modèle content.

La notation précédente est équivalente à:

<ng-template [testDirective] 
  [testDirectiveInputParameter]="'This is the value of the parameter'" 
  let-content="testDirectiveInputParameter"> 
  <p>Template content: {{content}}</p> 
<ng-template> 

L’implémentation de la directive est:

@Directive({ 
  selector: '[testDirective]' 
}) 
export class TestDirective implements OnInit { 
  @Input('testDirectiveInputParameter') inputParameter: string; 
 
  constructor(private parentViewRef: ViewContainerRef, 
    private templateRef: TemplateRef<any>) {} 

  ngOnInit(): void { 
    this.parentViewRef.createEmbeddedView(this.templateRef, 
    { testDirectiveInputParameter: this.inputParameter }) 
  } 
} 

Le résultat est:

Template content: This is the directive input content. 

Affecter des valeurs en paramètre d’entrée
Pour affecter des valeurs en paramètre d’entrée de la directive, comme pour une directive attribut (i.e. attribute directive), on peut utiliser des attributs dans le modèle, par exemple:

<ng-template testDirective [inputParameter]="'This is a directive input message'"> 
  <p>Template content.</p> 
</ng-template> 

La syntaxe [<clé de l'attribut>] rend obligatoire l’implémentation d’un paramètre d’entrée dans la directive sinon une erreur se produit:

@Directive({ 
  selector: '[testDirective]' 
}) 
export class TestDirective implements OnInit { 
  @Input('inputParameter') templateContent: string; 
 
  constructor(private parentViewRef: ViewContainerRef, 
    private TemplateRef: TemplateRef<any>) {} 

  ngOnInt(): void { 
    console.log(this.templateContent); 
    this.parentViewRef.createEmbeddedView(this.templateRef); 
  } 
} 

Dans cet exemple, le résultat est le même que précédemment.

ViewContainerRef

Le type ViewContainerRef comporte quelques fonctions pour modifier le contenu du view container:

  • Propriété ViewContainerRef.element permet d’accéder à l’élément se trouvant dans le view container. Il ne s’agit pas d’un élément HTML mais d’un objet Angular permettant de s’intercaler avec les objets du DOM. Il n’y a qu’un seul élément par view Container. Si on crée plusieurs vues intégrées (i.e. embedded view) dans le même view container, elles seront incluses dans le même élément.
  • clear() supprime toutes les vues intégrées (i.e. embedded view) rajoutées à l’élément se trouvant dans le view Container.
  • createEmbeddedView() crée et ajoute une vue intégrée (i.e. embedded view) à l’élément se trouvant dans le view container. Les paramètres de cette fonction correspondent au modèle de la directive structurelle et éventuellement au contexte. La fonction retourne une instance de la vue intégrée.
  • insert() permet d’insérer une vue dans le view container à un index particulier dans la liste des vues intégrées. Dans le cas où on utilise ViewContainerRef.createEmbeddedView(), il est inutile d’appeler ViewContainerRef.insert() car createEmbeddedView() ajoute la vue après l’avoir créé. Par contre, on peut aussi créer une vue avec le modèle de la directive structurelle, par exemple:
    @Directive({ ... })
    export class ExampleDirective implments OnInit {
      constructor(private parentViewRef: ViewContainerRef, 
        private templateRef: TemplateRef) {}
    
      ngOnInit(): void {
        const viewRef = this.templateRef.createEmbeddedView();
        this.parentViewRef.insert(viewRef);
      }
    }
    
  • move() déplace une vue intégrée (i.e. embedded view) en précisant son index dans la liste des vues intégrées du view container.
  • indexOf() renvoie l’index d’une vue intégrée (i.e. embedded view) parmi la liste des vues du view container. Cette fonction renvoie -1 si la vue ne se trouve pas dans la liste de vues du view container.
  • remove() supprime une vue de la liste des vues se trouvant dans un view container en fonction de son index.
  • detach() supprime une vue de la liste des vues se trouvant dans un view container sans la détruire.
  • createComponent() instance un composant et l’ajoute dans la vue hôte du view container. Par exemple:
    import { ComponentFactoryResolver, ComponentFactory } from '@angular/core';
    
    @Directive({ ... })
      export class ExampleDirective implments OnInit {
    
      constructor(private parentViewRef: ViewContainerRef, private templateRef: TemplateRef, 
        private resolver:  ComponentFactoryResolver) {}
      
      ngOnInit(): void {
        const componentFactory: ComponentFactory = 
          this.resolver.resolveComponentFactory(TestComponent);
        this.parentViewRef.createComponent(componentFactory);
      }
    }
    

Exemple de directive structurelle

L’exemple est le même que pour les directives attributs, la directive renseigne les différents éléments li (i.e. list item) d’une liste ordonnée ol (i.e. ordered list). La liste d’éléments correspond à une liste d’utilisateurs. L’utilisateur est affiché si la propriété User.canBeDisplayed est égale à true.

L’implémentation de la directive est:

export class User {
  constructor(public firstName: string, public lastName: string, 
    public canBeDisplayed: boolean) {}
}

export interface ICanDisplayedFunction { 
  (user: User): boolean; 
} 

@Directive({ 
  selector: '[ngDisplayUser]' 
}) 
export class NgDisplayUser { 
  private userList: User[]; 
  private userCanBeDisplayed: ICanBeDisplayedFunction; 

  @Input('ngDisplayUser') 
  set ngDisplayUser(users: Observable) { 
    if (users) { 
        users.subscribe(value => { 
        this.parentViewRef.clear(); 
        this.userList = value; 
        this.generateUsers(); 
      }) 
    } 
  }

  @Input('ngDisplayUserWhen') 
  set whenParameter(condition: ICanBeDisplayedFunction) { 
    if (condition) { 
      this.userCanBeDisplayed = condition; 
      this.generateUsers(); 
    } 
  } 

  constructor(private parentViewRef: ViewContainerRef, 
    private templateRef: TemplateRef) {} 


  private generateUsers(): void { 
    if (this.userList && this.userCanBeDisplayed) { 
        this.userList.forEach(user => { 
          if (this.userCanBeDisplayed(user)) { 
            this.parentViewRef.createEmbeddedView(this.templateRef, user); 
          } 
        }) 
    } 
  } 
} 

L’implémentation du composant hôte est:

Template Classe du composant
<p>Users are:</p>
<ol>
  <li> *ngDisplayUser="users 
    when canUserBeDisplayed 
    let userLastName=lastName 
    let userFirstName=firstName">
    {{userFirstName}} {{userLastName}}
  </li>
</ol>
@Component({ ... }) 
export class HostComponent {
  users: User[];

  constructor() {
    this.users = [
      new User('Beatrix', 'Kiddo', true),
      new User('Bill', 'Bill', true),
      new User('Elle', 'Driver', false),
      new User('O-Ren', 'Ishii', true)
    ]
  }

  canUserBeDisplayed(user: User): void {
    return user.canBeDisplayed;
  }
}

Ainsi, dans le template du composant hôte:

  • l’attribut *ngDisplayUser permet par résolution de faire appel à la directive ngDisplayUser à cause de la valeur du paramètre selector.
  • On utilise la microsyntaxe: users when canUserBeDisplayed let userLastName=lastName let userFirstName=firstName:
    • users affecte la valeur de la propriété du même nom au paramètre d’entrée de la directive avec l’alias 'ngDisplayUser'. L’affectation de la propriété du composant est faite par property binding. Le paramètre d’entrée doit se nommer de la même façon que la directive (cf. règle 1 et 2).
    • when canUserBeDisplayed est une expression permettant d’affecter une fonction au paramètre d’entrée avec l’alias 'ngDisplayUserWhen' dans la directive. La signature de cette fonction est définie par l’interface ICanDisplayedFunction.
    • let userLastName=lastName déclare la variable locale userLastName du modèle. Sa valeur contiendra la valeur de la propriété lastName de l’instance de user passée dans le contexte.
    • let userFirstName=firstName déclare la variable locale userFirstName du modèle. Sa valeur contiendra la valeur de la propriété firstName de l’instance de user passée dans le contexte.
    • Dans le corps du modèle {{userFirstName}} {{userLastName}} permet d’utiliser les variables locales déclarées par la microsyntaxe pour afficher le nom de l’utilisateur.
  • Dans la directive dans la méthode generateUsers(), this.parentViewRef.createEmbeddedView(this.templateRef, user) permet de créer une vue intégrée (i.e. embedded view) pour chaque utilisation et la placer dans le view container. En créant la vue, le contexte contient une instance d’un utilisateur.

Le résultat est le même que précédemment:

Pour résumer…

Les directives permettent de modifier ou d’enrichir des éléments du DOM généralement par programmation. Il exite 3 types de directives:

  • Les composants: même si on considère qu’ils sont des directives, ils se distinguent des directives à proprement parlé car ils possèdent une vue implémentée en utilisant un template. La vue rend le composant autonome car il ne nécessite pas d’autres éléments pour s’afficher.
  • Les autres types de directives ne possèdent pas directement de template et donc de vue, elles modifient le DOM par programmation en utilisant la vue d’un composant hôte:
    • Les directives attribut (i.e. attribute directives) modifient des éléments du DOM entièrement par programmation.
    • Les directives structurelles (i.e. structural directives) permettent d’enrichir le DOM en y ajoutant des éléments par l’intérmédiaire de vues qui peuvent être intégrées à la vue du composant hôte. Ces directives utilisent un modèle implémenté dans la vue du composant hôte.

On peut ajouter une directive en utilisant le CLI Angular en exécutant:

ng g c <nom de la directive>

Cette instruction crée les fichiers correspondant à la directive et ajoute la directive dans le module dans lequel l’instruction est exécutée.

Pour déclarer une directive, il faut utiliser le décorateur @Directive() en utilisant au moins le paramètre selector. Ce paramètre permet d’indiquer comment utiliser la directive dans le template du composant hôte.

Ainsi une directive sera utilisée par résolution dans un composant hôte suivant la valeur du paramètre selector:

Type d’élément participant à la résolution Valeur de selector Exemple
Elément HTML
selector: 'custom-directive'
<custom-directive></custom-directive>
Attribut d’un élément HTML
selector: '[custom-directive]'
<span custom-directive></span>

ou

<span custom-directive="attribute value">
</span>

ou

<span [custom-directive]="bindedProperty">
</span>
Classe CSS
selector: '.custom-directive' 
<span class="custom-directive">
</span>
Valeur de l’attribut d’un élément HTML
selector: '[title=custom-directive]'
<span title="custom-directive">
</span> 
Type d’un élément et valeur d’un attribut
selector: 'p[title=custom-directive]'
<p title="custom-directive"></p>
Opérateur logique NOT
selector: ':not([custom-directive])'
<span custom-directive>With attribute</span>

ou

<span>Without attribute</span>
Opérateur logique OR
selector: '[custom-directive], 
[title=custom-directive]'
<span custom-directive></span>

ou

<span custom-directive="attribute value">
</span>

ou

<span [custom-directive]="bindedProperty">
</span>

ou

<span title="custom-directive">
</span>

L’implémentation d’une directive varie en fonction du type de directive:

  • Directive attribut: on peut injecter les objets:
    • ElementRef: wrapper de l’objet du DOM comportant la vue hôte,
    • Renderer2 permettant de modifier la vue hôte.

    Par exemple:

    @Directive({ 
      selector: 'custom-directive' 
    }) 
    export class CustomDirective { 
      constructor(private elem: ElementRef, private renderer: Renderer2) {  
        let newText = renderer.createText('Text from directive');  
        renderer.appendChild(elem.nativeElement, newText); 
      }
    }
    

    Renderer2.appendChild() permet de rajouter un élément à un élément HTML hôte.

  • Directive structurelle: on peut injecter les objets:
    • ViewContainerRef: conteneur de vues (i.e. view container) dans lequel on pourra ajouter des vues par programmation.
    • TemplateRef: modèle utilisé pour créer des vues à rajouter dans le view container.

    Par exemple:

    @Directive({ 
      selector: '[customDirective]' 
    }) 
    export class CustomDirective implements OnInit { 
      constructor(private parentViewRef: ViewContainerRef, 
        private templateRef: TemplateRef<any>) {} 
    
      ngOnInit(): void { 
        this.parentViewRef.createEmbeddedView(this.templateRef); 
      } 
    }
    

    ViewContainerRef.createEmbeddedView() permet de créer une vue et de la rajouter au view container.

On peut rajouter des paramètres en entrée des directives avec le décorateur @Input(). Le nom de ces paramètres doit correspondre au nom de la propriété dans la directive, par exemple:

@Directive({ ... }) 
export class TestDirective { 
  @Input() textToDisplay: string;
} 

Ce paramètre est affecté si l’élément HTML hôte comporte un attribut du même nom:

<custom-directive textToDisplay='Value defined from component'></custom-directive>

On peut utiliser un raccourci pour indiquer la directive à utiliser et affecter une valeur dans un paramètre d’entrée de cette directive de cette façon:

<span textToDisplay="Value defined from component"></span> 

La directive sera exécutée si la valeur de son paramètre selector est '[textToDisplay]'. D’autre part, la directive doit comporter un paramètre d’entrée avec le même nom que son paramètre selector pour que la valeur "Value defined from component" y soit affectée, par exemple:

@Directive({ 
  selector: '[textToDisplay]'
}) 
export class CustomDirective { 
  @Input() textToDisplay: string; 

  constructor(private elem: ElementRef, private renderer: Renderer2) {} 
}

Les directives structurelles sont plus complexes à implémenter que les directives attributs car elles nécessitent plus de conditions pour s’exécuter:

  • L’attribut permettant de les utilisant dans le template du composant hôte doit être préfixé avec *, par exemple:
     <p *templateDirective>Template content.</p>
    
  • Les directives structurelles nécessitent l’utilisation d’un modèle pour créer les vues. Ce modèle peut être implémenté dans le template du composant hôte en utilisant une microsyntaxe.

Pour construire la ou les vues, on peut utiliser un contexte qui contiendra les valeurs à utiliser. Ce contexte est transmis à la vue au moment de la créer, par exemple:

@Directive({ ... }) 
export class CustomDirective implements OnInit { 
  constructor(private parentViewRef: ViewContainerRef, 
    private templateRef: TemplateRef<any>) {} 

  ngOnInit(): void { 
    this.parentViewRef.createEmbeddedView(this.templateRef, <contexte>); 
  } 
}

Dans ce cas, le type de templateRef est TemplateRef<any>, ainsi any permet de renseigner n’importe quel type y compris des object literals, par exemple:

{ 
  Property1: <valeur de la propriété 1>, 
  Property2: <valeur de la propriété 2>, 
  // ...
  PropertyN: <valeur de la propriété N>, 
}

Ce contexte pourra être utilisé dans le modèle de la directive en utilisant la microsyntaxe.

La microsyntaxe des directives structurelles s’utilise de cette façon:

*<nom paramètre selector de la directive>="<suite d'opérations en microsyntaxe>"

Par exemple, si on considère une directive structurelle avec le paramètre selector '[textToDisplay]' alors on peut appeler la directive dans un composant hôte de cette façon:

<p *textToDisplay="<suite opération microsyntaxe>">
  Modèle de la directive
</p>

La microsyntaxe doit satisfaire certaines règles:

  • Règle 1: si la microsyntaxe renvoie une valeur alors un paramètre d’entrée doit être implémenté dans la directive avec le même nom que le paramètre selector.
  • Règle 2: la suite d’opérations en microsyntaxe renvoie une valeur si la 1ère opération renvoie une valeur.
  • Règle 3: les opérations peuvent être séparées par des caractères d’espacement. Les caractères ';', ':' ou retour à la ligne sont facultatifs.

Les opérations en microsyntaxe permettent d’effectuer les opérations suivantes:

  • Exécuter une expression Javascript.
  • Déclarer une variable locale au modèle de la directive avec la syntaxe let <nom de la variable>.
  • Déclarer et initialiser une variable locale avec la syntaxe let <nom de la variable> = <propriété du contexte>
  • Affecter un paramètre d’entrée de la directive avec la syntaxe <clé du paramètre> <expression retournant une valeur>.
    Dans ce cas la propriété du paramètre d’entrée doit s’appeler <nom de la directive><clé du paramètre avec la 1ère lettre en majuscule>.
  • Déclarer une variable locale et l’initialiser avec une expression en utilisant la syntaxe <expression> as <nom de la variable>.
  • Affecter un paramètre d’entrée de la directive avec une expression, déclarer une variable locale et l’initialiser avec cette expression en utilisant la syntaxe <clé du paramètre>: <expression retournant une valeur> as <nom de la variable>.
    Dans ce cas la propriété du paramètre d’entrée doit s’appeler <nom de la directive><clé du paramètre avec la 1ère lettre en majuscule>.

Le modèle de la directive peut être implémenté en utilisant les variables locales déclarées et initialisées à l’aide de la microsyntaxe, par exemple:

<p *ngDisplayText="let templateContent = textToDisplay">
  The content is: {{templateContent}}.
</p> 

Ainsi:
ngDisplayText est l’attribut permettant de trouver la directive, il correspond au selector '[ngDisplayText]':

  • templateContent correspond à une variable locale et
  • textToDisplay est la propriété du contexte transmis avec ViewContainerRef.createEmbeddedView().
  • Le modèle The content is: {{templateContent}} est renseigné en utilisant une variable locale par interpolation.
Références

Documentation Angular:

Code source d’Angular:

Autres:

Leave a Reply