Cet article fait partie de la série d’articles Angular from Scratch.
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.
Configuration des directives
Paramètre selector
Paramètre inputs
Utilisation d’un alias
@Input()
Utilisation d’un alias
Paramètre outputs
Utilisation d’un alias
@Output()
Utilisation d’un alias
Paramètre providers
@HostBinding()
@HostListener()
Attribute directives
ElementRef et Renderer2
Exemple de directive attribut
Structural directives
<ng-template>
View container
Injecter des données dans le modèle avec le contexte
Détection des changements et cycle de vie d’une vue
$implicit
ngTemplateOulet et ngTemplateOutletContext
Utiliser ngTemplate avec une directive
Implémentation d’une directive structurelle
Configurer une directive structurelle par microsyntaxe
ViewContainerRef
Exemple de directive structurelle
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:
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’attributtitle="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:
custom-directive
,- L’attribut peut comporter une valeur:
custom-directive="attribute value"
, - Dans le cas d’un property binding:
[custom-directive]="bindedProperty"
(un paramètre d’entrée doit être présent dans la directive dans ce cas), title="custom-directive"
<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>
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>
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 |
---|---|
|
|
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
etRenderer2
corrrespondant, respectivement, à l’objet dans lequel on va rajouter les élémentsli
et l’objet qui va permettre d’effectuer les modifications dans l’objetElementRef
. - Le paramètre
selector
de la directive'[displayUsers]'
permet d’effectuer la résolution de la directive si un élément HTML contient un attributdisplayUsers
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émentsli
à 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 |
---|---|
|
|
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éthodengAfterViewInit()
est déclenchée, on appelle explicitementViewContainerRef.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:
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.ngDoCheck()
: cette méthode est déclenchée à chaque fois qu’Angular détecte un changement nécessitant de mettre à jour une vue.ngAfterContentInit()
est déclenchée après projection de contenu externe dans la vue d’un composant.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ènementngAfterViewInit()
.
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 |
---|---|
|
|
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 directiveTemplateDirective
soit utilisée.let-content
pour déclarer la variablecontent
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 |
---|---|
|
|
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 |
---|---|
|
|
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:
ViewContainerRef
contenant le view container qu’Angular va injecter en fonction de la vue hôte du modèle (obtenue suivant l’arbre hiérarchique des vues).TemplateRef<any>
contenant le modèle.
- 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ètreselector
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); } }
- Que le paramètre d’entrée avec un nom ou un alias
- 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 estundefined
. - 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.
- Une valeur simple comme une chaîne de caractères (comme
- 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
ouas
, - 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èlecontent
.
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 utiliseViewContainerRef.createEmbeddedView()
, il est inutile d’appelerViewContainerRef.insert()
carcreateEmbeddedView()
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 |
---|---|
|
|
Ainsi, dans le template du composant hôte:
- l’attribut
*ngDisplayUser
permet par résolution de faire appel à la directivengDisplayUser
à cause de la valeur du paramètreselector
. - 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’interfaceICanDisplayedFunction
.let userLastName=lastName
déclare la variable localeuserLastName
du modèle. Sa valeur contiendra la valeur de la propriétélastName
de l’instance deuser
passée dans le contexte.let userFirstName=firstName
déclare la variable localeuserFirstName
du modèle. Sa valeur contiendra la valeur de la propriétéfirstName
de l’instance deuser
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 |
|
|
Attribut d’un élément HTML |
|
ou
ou
|
Classe CSS |
|
|
Valeur de l’attribut d’un élément HTML |
|
|
Type d’un élément et valeur d’un attribut |
|
|
Opérateur logique NOT |
|
ou
|
Opérateur logique OR |
|
ou
ou
ou
|
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 ettextToDisplay
est la propriété du contexte transmis avecViewContainerRef.createEmbeddedView()
.- Le modèle
The content is: {{templateContent}}
est renseigné en utilisant une variable locale par interpolation.
Documentation Angular:
- Lifecycle hooks: https://angular.io/guide/lifecycle-hooks
- TemplateRef: https://angular.io/api/core/TemplateRef
- ViewChild:
https://angular.io/api/core/ViewChild - Directive: https://angular.io/api/core/Directive
- Attribute directives: https://angular.io/guide/attribute-directives
- Structural directives: https://angular.io/guide/structural-directives
- Renderer2: https://angular.io/api/core/Renderer2
Code source d’Angular:
- Code source ngIf: https://github.com/angular/angular/blob/master/packages/common/src/directives/ng_if.ts
- Code source ngFor: https://github.com/angular/angular/blob/master/packages/common/src/directives/ng_for_of.ts
- Code source ngSwitch: https://github.com/angular/angular/blob/master/packages/common/src/directives/ng_switch.ts
Autres:
- Angular Templates — From Start to Source: https://unicorn-utterances.com/posts/angular-templates-start-to-source/
- Angular 4 Renderer2 Example: https://www.concretepage.com/angular-2/angular-4-renderer2-example
- Angular 9 Renderer2 with Directives Tutorial by Example: https://www.techiediaries.com/angular/angular-9-renderer2-directives-tutorial-example/
- Using @HostBinding and @HostListener in Custom Angular Directives: https://alligator.io/angular/hostbinding-hostlistener/
- HostBinding: https://angular.io/api/core/HostBinding
- Handling Observables with Structural Directives in Angular: https://dev.to/angular/handling-observables-with-structural-directives-in-angular-112j
- TypeScript Function as a parameter: https://riptutorial.com/typescript/example/8099/function-as-parameter
- The Power of Structural Directives in Angular: https://netbasal.com/the-power-of-structural-directives-in-angular-bfe4d8c44fb1
- Use <ng-template>: https://medium.com/angular-in-depth/use-ng-template-c72852c37fba
- Understanding Angular Structural Directives: https://netbasal.com/understanding-angular-structural-directives-659acd0f67e
- Angular ng-template, ng-container and ngTemplateOutlet – The Complete Guide To Angular Templates: https://blog.angular-university.io/angular-ng-template-ng-container-ngtemplateoutlet/
- Dynamically Creating Components with Angular: https://netbasal.com/dynamically-creating-components-with-angular-a7346f4a982d