Cet article fait partie de la série d’articles Angular from Scratch.
Une fonctionnalité importante des composants est qu’ils peuvent contenir des directives. Par suite sachant que les composants sont des directives, il est possible d’imbriquer des composants les uns dans les autres. Un composant se trouvant dans un autre composant est appelé composant enfant (i.e. child component).
Dans la suite de cet article, on appellera:
- “Composant parent”: le composant contenant d’autres composants,
- “Composant enfant”: le composant imbriqué dans un autre composant.
Dans cet article, on ne parlera que de la composition des composants pour faciliter les explications toutefois il faut garder en tête que dans le cas général, les composants peuvent contenir des directives (pour davantage de détails, voir “Les directives Angular”).
Dans un premier temps, on indiquera comment placer un composant enfant dans un composant parent. Puis on explicitera la méthode pour injecter des paramètres dans un composant et comment être notifié par le déclenchement d’un évènement dans un composant enfant. Enfin, on expliquera la fonctionnalité de projection de contenu.
Interactions avec les composants enfant
Injection du composant parent
Exposer le composant parent avec une classe abstraite
Trouver un parent sur plusieurs niveaux
@Optional()
@SkipSelf()
@ViewChild() et @ViewChildren()
@Input()
Utiliser un alias pour identifier le paramètre injecté
Property binding
@Output() + EventEmitter
Utiliser un alias pour identifier l’évènement
Paramètres inputs et outputs de @Component()
Content projection
Projection de contenu sur plusieurs niveaux
Multi-content projection
Paramètre providers vs viewProviders de @Component()
Contenu projeté vs contenu non projeté
Paramètre viewProviders de @Component()
Paramètre providers au niveau du module uniquement
Paramètre providers au niveau du composant
Paramètre viewProviders au niveau de LibraryComponent
Ajout d’un contenu directement dans LibraryComponent
Ordre d’exécution des callbacks du cycle de vie d’un composant
Pour imbriquer un composant dans un autre, il suffit d’indiquer le paramètre selector
dans le template du composant parent. Pour que la résolution du template puisse réussir dans le composant parent, il faut que le composant enfant soit dans le même module que le composant parent.
Pour illustrer l’exemple d’un composant enfant, on va créer 2 composants:
ChildComponent
qui servira de composant enfant,ParentComponent
qui servira de composant parent et qui contiendraChildComponent
.
Dans le répertoire du module dans lequel on veut créer les 2 composants, on exécute la commande suivante en utilisant le CLI Angular:
ng generate component Child
ng generate component Parent
Ou
ng g c Child
ng g c Parent
On modifie le paramètre selector
du composant enfant:
Template |
|
Classe du composant |
|
Dans le composant parent, on modifie l’implémentation du template pour afficher le contenu du composant enfant:
<h1>Parent component</h1>
<app-child></app-child>
En affichant le composant parent, on verra aussi le composant enfant:
De façon très facultative, on peut rajouter un style CSS pour que l’affichage permette de faire la différence entre le composant parent et le composant enfant.
On modifie le style du composant parent dans le fichier CSS correspondant (i.e. parent.component.css
):
:host {
border: 1px solid black;
display: block;
background-color: lightgrey;
width: 500px;
}
On modifie le style du composant enfant dans le fichier CSS child.component.css
:
:host {
border: 1px solid black;
display: block;
background-color: lightgreen;
margin: 5px;
}
L’affichage devient:
Interactions avec les composants enfant
Les interactions possibles entre le composant parent et les composants enfant peuvent être de différente nature suivant l’origine de l’interaction, par exemple:
- Du composant parent vers l’enfant:
- En injectant le composant parent dans le composant enfant: utiliser l’injection de dépendances d’Angular pour injecter le composant parent dans le composant enfant.
- En utilisant des paramètres d’entrée dans le composant enfant avec le décorateur
@Input()
: un property binding peut lier ce paramètre d’entrée avec une propriété du composant parent.
- Du composant enfant vers le parent:
- En utilisant le décorateur
@ViewChild()
pour accéder à l’instance du composant enfant dans la classe du composant parent. - En utilisant le décorateur
@Output()
pour émettre un évènement dans le composant enfant et notifier le composant parent.
- En utilisant le décorateur
On va expliciter chacune de ces interactions par la suite.
Injection du composant parent
L’interaction la plus directe entre un composant parent et un enfant est d’injecter le composant parent directement dans le composant enfant en utilisant l’injection de dépendances. Angular comporte un moteur d’injection de dépendances que l’on peut utiliser pour injecter dans un composant enfant une instance d’un de ses parents. L’injection ne se limite pas au composant parent direct, il est possible d’effectuer une injection de n’importe quel parent dans l’arbre de dépendances.
Par exemple, si l’implémentation du composant parent est:
Template |
|
Classe du composant |
|
Et si l’implémentation du composant enfant est:
Template |
|
Classe du composant |
|
Dans le composant enfant, on peut ainsi accèder à l’instance de son parent.
Même si l’injection du composant parent est une solution facile à implémenter, d’un point de vue de l’architecture, elle introduit une double dépendance entre le composant parent et le composant enfant puisque:
- Le composant parent référence le composant enfant dans l’implémentation du template: c’est le sens conventionnel de dépendance puisque le template doit indiquer quel est le composant enfant à afficher.
- Le composant enfant référence le composant parent avec:
import { ParentComponent } from '../parent/parent.component';
Il y a, ainsi, une double dépendance puisque le composant parent doit avoir une connaissance de son enfant et inversement. Cette double dépendance amène le composant enfant à avoir une implémentation dépendante de celle du composant parent, ce qui renforce le couplage entre ces 2 composants.
Pour éviter cette double dépendance, il est préférable d’exposer des propriétés dans le composant enfant par l’intermédiaire des décorateurs @Input()
et @Output()
de façon à ce que la dépendance se fasse dans un seul sens, du parent vers l’enfant. Ainsi seul le composant parent a une connaissance du composant enfant. L’implémentation du composant enfant reste, alors, indépendante de celle du composant parent.
Exposer le composant parent avec une classe abstraite
Une méthode pour éviter la double dépendance entre le composant parent et le composant enfant est de passer par une classe abstraite pour limiter les éléments exposés par le composant parent au composant enfant.
Ainsi:
- La classe abstraite définit les propriétés ou fonctions utilisables par le composant enfant,
- Le composant parent implémente la classe abstraite et
- L’instance du composant parent est injectée dans le composant enfant sous la forme de la classe abstraite. Le couplage entre le composant parent et enfant est, ainsi, davantage limité.
Par exemple, si on considère la classe abstraite suivante implémentée dans un fichier séparée ValueHandler.ts
:
export abstract class ValueHandler {
abstract get innerValue(): string;
}
Pour injecter ValueHandler
dans le composant enfant plutôt que ParentComponent
, on configure l’injecteur de cette façon au niveau du composant parent:
- Avec l’option
provide
pour indiquer le type de la classe abstraite - L’option
useExisting
pour indiquer une instance particulière deParentComponent
et forwardRef()
pour faire suivre la référence vers le type du composant parent.
L’implémentation du composant parent devient:
Template |
|
Classe du composant |
|
L’implémentation du composant enfant devient:
Template |
|
Classe du composant |
|
Dans le composant enfant, la référence vers le composant parent a disparu.
Il n’est pas possible d’utiliser une interface à la place de la classe abstraite pour exposer le composant parent.
Trouver un parent sur plusieurs niveaux
L’injection du composant parent dans le composant enfant n’est pas limitée au parent direct. Le parent peut se trouver à plusieurs niveaux du composant enfant.
Par exemple, si on considère 3 composants tels que:
ChildComponent
est un composant enfant deMiddleComponent
etMiddleComponent
est un composant enfant deParentComponent
.
Les implémentations des 3 composants sont:
- Pour
ParentComponent
:
Template <h1>Parent component</h1> <app-middle></app-middle>
Classe du composant import { Component } from '@angular/core'; @Component({ templateUrl: './parent.component.html' }) export class ParentComponent { innerValue = 'Value defined in parent'; }
- Pour
MiddleComponent
:
Template <h2>Middle component</h2> <app-child></app-child>
Classe du composant import { Component } from '@angular/core'; @Component({ selector: 'app-middle', templateUrl: './middle.component.html' }) export class MiddleComponent {}
- Et pour
ChildComponent
:
Template <h3>Child component</h3>
Classe du composant import { Component} from '@angular/core'; import { ParentComponent } from '../parent/parent.component'; @Component({ selector: 'app-child', templateUrl: './child.component.html' }) export class ChildComponent { constructor(parent: ParentComponent) { console.log(parent.innerValue); } }
L’injection réussit même si ParentComponent
n’est pas le parent direct de ChildComponent
.
@Optional()
On peut utiliser le décorateur @Optional()
au niveau du paramètre du constructeur du composant enfant pour indiquer au moteur d’injection de dépendances de fournir null
si la dépendance n’est pas trouvée.
Par exemple:
import { Component, Optional } from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html'
})
export class ChildComponent {
constructor(@Optional() parent: ParentComponent) {
console.log(parent.innerValue);
}
}
On peut rajouter ?
dans le paramètre parent du constructeur pour indiquer que le paramètre est optionel:
constructor(@Optional() parent?: ParentComponent) {
console.log(parent.innerValue);
}
On peut, ainsi, appelé le constructeur de ChildComponent
sans préciser de paramètre:
let child = new ChildComponent();
@SkipSelf()
Dans le cas où un composant configure l’injecteur de dépendances en faisant suivre la référence d’un type vers lui-même avec forwardRef()
et qu’il injecte ce même type, @SkipSelf()
permet d’éviter que l’instance injectée soit celle de ce composant. @SkipSelf()
force le moteur d’injection de dépendances à aller chercher une autre instance à injecter.
Par exemple, si on considère 2 composants dans lesquels on fait suivre la référence d’un type vers eux-même avec forwardRef()
:
ParentComponent
est le composant parent etChildComponent
est le composant enfant deParentComponent
.
Les implémentations sont:
- Pour
ParentComponent
:
Template <h1>Parent component</h1> <app-child></app-child>
Classe du composant import { Component, forwardRef } from '@angular/core'; import { ValueHandler } from '../ValueHandler'; @Component({ templateUrl: './parent.component.html', providers: [{ provide: ValueHandler, useExisting: forwardRef(() => ParentComponent) }] }) export class ParentComponent extends ValueHandler { get innerValue(): string { return 'Value defined in Parent'; } constructor() { super(); } }
- Pour
ChildComponent
:
Template <h3>Child component</h3>
Classe du composant import { Component, forwardRef } from '@angular/core'; import { ValueHandler } from '../ValueHandler'; @Component({ selector: 'app-child', templateUrl: './child.component.html', providers: [{ provide: ValueHandler, useExisting: forwardRef(() => ChildComponent) }] }) export class ChildComponent extends ValueHandler { get innerValue(): string { return 'Value defined in Child'; } constructor(valueHandler: ValueHandler) { super(); console.log(valueHandler.innerValue); } }
L’implémentation de ValueHandler
est la même que précédemment:
export abstract class ValueHandler {
abstract get innerValue(): string;
}
Si on essaie d’exécuter ce code, une erreur apparaîtra dans la console du browser indiquant qu’une référence circulaire existe dans le composant ChildComponent
:
ERROR error: "Uncaught (in promise): Error: Circular dep for ChildComponent
[...]"
Cette erreur survient car l’injecteur de dépendance essaie d’injecter ChildComponent
dans lui-même pour le paramètre valueHandler
à cause de la configuration:
providers: [{ provide: ValueHandler, useExisting: forwardRef(() => ChildComponent) }]
Pour injecter dans le paramètre valueHandler
une instance de ParentComponent
au lieu de ChildComponent
, il faut utiliser le décorateur @SkipSelf()
dans le constructeur de ChildComponent
:
import { Component, SkipSelf } from '@angular/core';
import { ValueHandler } from '../ValueHandler';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
providers: [{ provide: ValueHandler, useExisting: forwardRef(() => ChildComponent) }]
})
export class ChildComponent extends ValueHandler {
get innerValue(): string {
return 'Value defined in Child
}
constructor(@SkipSelf() valueHandler: ValueHandler) {
super();
console.log(valueHandler.innerValue);
}
}
A l’exécution, le résultat affiché dans la console du browser sera 'Value defined in Parent'
puisque l’instance injectée pour le paramètre valueHandler
est celle de ParentComponent
.
@ViewChild() et @ViewChildren()
Une autre méthode d’interaction entre un composant parent et un composant enfant est d’utiliser les décorateurs @ViewChild()
ou @ViewChildren()
. Dans la classe du composant parent, ils permettent d’effectuer une requête sur la vue pour récupérer l’instance du ou des composants enfant qui y sont déclarées.
Ces décorateurs seront abordés plus en détails ultérieurement dans un article consacré aux requêtes effectuées dans la vue d’un composant.
@Input()
Le décorateur @Input()
permet d’implémenter des paramètres d’entrée dans le composant enfant qu’on pourra initialiser à partir du composant parent.
Par exemple, pour implémenter le paramètre identifier
dans le composant enfant, on modifie l’implémentation de la façon suivante:
Template |
|
Classe du composant |
|
On peut injecter le paramètre identifier
à partir du composant parent en modifiant l’implémentation du template:
<h1>Parent component</h1>
<app-child identifier='1'></app-child>
Ainsi la valeur du paramètre identifier
peut être initialisée à partir du composant parent.
Pour afficher plusieurs fois le composant enfant avec un identifiant différent:
<h1>Parent component</h1>
<app-child identifier='1'></app-child>
<app-child identifier='2'></app-child>
L’affichage devient:
Property binding
Pour effectuer un property binding entre une propriété de la classe du composant parent et le paramètre d’entrée du composant enfant, il suffit de modifier l’implémentation du parent:
Template |
|
Classe du composant |
|
Ainsi:
- On rajoute la propriété
childIdentifier
dans le classe du composantParentComponent
et - On effectue un property binding dans le template avec le paramètre d’entrée
identifier
du composant enfant:[identifier]='childIdentifier'
.
Utiliser un alias pour identifier le paramètre injecté
On peut utiliser un alias pour identifier le paramètre injecté dans le template du composant parent. Il suffit d’indiquer l’alias en tant que paramètre du décorateur @Input('')
dans le composant enfant.
Par exemple, si on utilise 'childComponentIdentifier'
à la place de 'identifier'
pour désigner le paramètre injecté dans le composant enfant, l’implémentation du composant enfant devient:
Template |
|
Classe du composant |
|
On utilise l’alias dans le template du composant parent (dans parent.component.ts
):
<h1>Parent component</h1>
<app-child childComponentIdentifier='1'></app-child>
<app-child childComponentIdentifier='2'></app-child>
Ce changement d’implémentation ne modifie pas le comportement.
@Output() + EventEmitter
@Input()
permet d’effectuer un binding de paramètres du composant parent vers le composant enfant. A l’opposé, on peut notifier le composant parent à partir d’évènements survenus dans le composant enfant en utilisant l’event binding. Il n’est pas possible d’effectuer le binding d’une propriété du composant enfant vers le composant parent toutefois, l’event binding permet de déclencher l’exécution d’une méthode à chaque modification de cette propriété.
Dans un composant enfant, pour déclarer un évènement auquel pourra s’abonner le composant parent, il faut utiliser le décorateur @Output()
.
Par exemple, on modifie l’implémentation du composant enfant ChildComponent
:
- Dans le template:
- on ajoute un élément
input
contenant une valeur qu’on pourra incrémenter. La valeur dans l’input
sera bindée avec la propriétéinternalCount
dans la classe du composant enfant. - On ajoute un bouton pour incrémenter la valeur dans l’élément
input
. - On ajoute la méthode
incrementValue()
qui permettra d’incrémenter la valeur deinternalCount
.
- on ajoute un élément
- Dans la classe du composant enfant (
child.component.ts
): on ajoute la propriétéinternalCount
et on déclare l’évènementcountUpdated
avec la décorateur@Output()
pour notifier le composant parent quand le valeur de la propriétéinternalCount
est modifiée.
L’implémentation du composant enfant devient:
- Template:
<p>Child component with identifier: {{identifier}}</p> <p> <input readonly='true' [(ngModel)]='internalCount' /> <button (click)='incrementValue()'>Increment</button> </p>
- Classe du composant:
import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'app-child', templateUrl: './child.component.html' }) export class ChildComponent { internalCount = 0; @Input('childComponentIdentifier') identifier: number; @Ouput() countUpdated: EventEmitter<number>= new EventEmitter<number>(); incrementValue(): void { this.internalCount++; this.countUpdated.emit(this.identifier); } }
Dans le code du composant:
countUpdated: EventEmitter<number> = new EventEmitter<number>()
permet d’initialiser un objet de typeEventEmitter
qui permettra de notifier le composant parent.- Dans
incrementValue()
,this.countUpdated.emit(this.internalIdentifier)
déclenche l’évènement en incluant l’identifiant du composant enfant.
On modifie le composant parent:
- Dans le template:
- On effectue un event binding avec l’évènement
countUpdated
du composant enfant. - On ajoute un total des incrémentations des compteurs des composants enfant.
- On effectue un event binding avec l’évènement
- Dans la classe du composant: on implémente la méthode
updateTotalCount()
qui sera appelée à chaque déclenchement de l’évènement.
L’implémentation du composant parent devient:
- Template:
<h1>Parent component</h1> <app-child childComponentIdentifier='1' (countUpdated)='updateTotalCount($event)'></app-child> <app-child childComponentIdentifier='2' (countUpdated)='updateTotalCount($event)'></app-child> <p>Total: {{totalCount}}</p>
- Classe du composant:
import { Component } from '@angular/core'; @Component({ templateUrl: './parent.component.html' }) export class AppComponent { totalCount = 0; lastUpdateIdentifier: number; updateTotalCount(childIdentifier: number): void { this.totalCount++; this.lastUpdateIdentifier = childIdentifier; } }
Ainsi, l’event binding permet de déclencher l’exécution de la méthode updateTotalCount()
pour incrémenter totalCount
et récupérer l’identifiant du dernier composant enfant pour lequel une incrémentation a été effectuée.
Le résultat devient:
Utiliser un alias pour identifier l’évènement
Comme pour le décorateur @Input()
, on peut utiliser un alias pour identifier l’évènement auquel on s’abonne du coté du composant parent. Il suffit d’indiquer l’alias en tant que paramètre du décorateur @Output('')
dans le composant enfant.
Par exemple, si on utilise 'childCountUpdated'
à la place de 'countUpdated'
pour désigner l’évènement déclaré dans le composant enfant, l’implémentation du composant enfant ChildComponent
(dans child.component.ts
) devient:
import { Component, Input, Output } from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html'
})
export class ChildComponent {
internalCount = 0;
@Input('childComponentIdentifier') identifier: number;
@Ouput('childCountUpdated') countUpdated: EventEmitter = new EventEmitter();
incrementValue(): void {
this.internalCount++;
this.countUpdated.emit(this.internalIdentifier);
}
}
Le template du composant enfant n’est pas modifié.
On utilise l’alias dans le template du composant parent (dans parent.component.ts
):
<h1>Parent component</h1>
<app-child childComponentIdentifier='1' (childCountUpdated)='updateTotalCount($event)'></app-child>
<app-child childComponentIdentifier='2' (childCountUpdated)='updateTotalCount($event)'></app-child>
<p>Total: {{totalCount}}</p>
Ce changement d’implémentation ne modifie pas le comportement.
Paramètres inputs et outputs de @Component()
Au lieu d’utiliser les décorateurs @Input()
et @Output()
, on peut utiliser les paramètres inputs
et outputs
du décorateur @Component()
. A l’origine, ces paramètres sont utilisables dans le décorateur @Directive()
, par suite le décorateur @Component()
en hérite puisque les composants sont des directives. Pour plus de détails, voir le détail de ces paramètres pour les directives:
Content projection
La fonctionnalité content projection (i.e. projection de contenu) permet de réserver dans un composant enfant des espaces dont le contenu sera implémenté dans le template du composant parent. Le contenu est, ainsi, projeté du composant parent vers le composant enfant:
L’intérêt de cette fonctionnalité est de prévoir un composant enfant dont l’implémentation est générique pour permettre d’indiquer des éléments de la vue à partir du composant parent.
Pour indiquer dans le template du composant enfant où doit être placé le contenu projeté, il faut utiliser <ng-content></ng-content>
.
Par exemple, si on considère le composant parent suivant:
Template |
|
Classe du composant |
|
Dans cet exemple, on utilise les mêmes styles CSS que ceux définis plus haut pour avoir un affichage plus clair.
On considère le composant enfant ChildComponent
suivant:
Template |
|
Classe du composant |
|
Si on exécute l’application, l’affichage est:
On modifie le composant enfant ChildComponent
pour permettre la projection de contenu. Il suffit de rajouter ng-content
à l’endroit où on souhaite la projection dans le template:
<h3>Child component</h3>
<ng-content></ng-content>
On modifie ensuite le template du composant parent en indiquant une implémentation du contenu à projeter:
<h1>Parent component</h1>
<app-child identifier='1'>
<span>Content projection from parent component</span>
</app-child>
En exécutant l’application, l’affichage est:
De façon très facultative, on peut modifier le style CSS des composants pour que l’affichage puisse mettre en évidence le contenu projeté provenant du composant parent:
- Dans
child.component.css
::host { border: 1px solid black; display: block; background-color: lightgreen; margin: 5px; padding: 5px; }
- Dans
parent.component.css
::host { border: 1px solid black; display: block; background-color: lightgrey; width: 500px; } span { border: 1px solid black; background-color: lightgrey; }
L’affichage devient:
On peut choisir un contenu projeté différent directement à partir du template du composant parent:
<h1>Parent component</h1>
<app-child identifier='1'>
<span>Content projection from parent component</span>
</app-child>
<app-child identifier='2'>
<span>Different content projection</span>
</app-child>
L’affichage correspondant est:
Projection de contenu sur plusieurs niveaux
Dans tous les exemples précédents, on exploitait la projection de contenu sur un seul niveau c’est-à-dire la projection du contenu d’un parent vers un seul composant enfant.
Il est possible d’effectuer des projections vers des composants enfant se trouvant à plusieurs niveaux de hiérarchie du composant parent.
Par exemple, si on considère 3 composants ParentComponent
, MiddleComponent
et ChildComponent
tels que:
MiddleComponent
est un composant enfant deParentComponent
etChildComponent
est un composant enfant deMiddleComponent
.
A partir de ParentComponent
, il est possible de projeter un contenu dans MiddleComponent
(se trouvant au 1er niveau de ParentComponent
) et dans ChildComponent
(se trouvant au 2e niveau de ParentComponent
).
Au niveau de la syntaxe, si:
- Le
selector
deMiddleComponent
est'app-middle'
et - Le
selector
deChildComponent
est'app-child'
.
Le contenu à projeter peut être implémenté de cette façon dans le template du composant ParentComponent
:
<app-middle>
// Ce contenu sera projeté dans MiddleComponent au 1er niveau
<app-child>
// Ce contenu sera projeté dans ChildComponent au 2e niveau
<p>Projected content</p>
</app-child>
</app-middle>
L’implémentation complète des composants est:
- Pour
ChildComponent
:
Template <h3>Child component</h3> <ng-content></ng-content>
Classe du composant @Component({ selector: 'app-child', templateUrl: './child.component.html' }) export class ChildComponent {}
- Pour
MiddleComponent
:
Template <h2>Middle component</h2> <ng-content></ng-content>
Classe du composant @Component({ selector: 'app-middle', templateUrl: './middle.component.html' }) export class MiddleComponent {}
- Et pour
ParentComponent
:
Template <h1>Parent component</h1> <app-middle> <app-child> <span>Projected content</span> </app-child> </app-middle>
Classe du composant @Component({ templateUrl: './parent.component.html' }) export class ParentComponent {}
Le résultat permet de se rendre compte que le contenu est projeté sur tous les composants enfant sur plusieurs niveaux:
Multi-content projection
Il est possible d’effectuer des projections de plusieurs contenus dans le composant enfant en utilisant la syntaxe suivante quand on utilise <ng-content>
:
<ng-content select='<type de l'élément à projeter>'></ng-content>
Par exemple, si on modifie le template du composant enfant ChildComponent
pour autoriser plusieurs contenus à projeter:
<h3>Child component</h3>
<ng-content select='h4'></ng-content>
<ng-content select='span'></ng-content>
On implémente, ensuite, 2 projections dans le template du composant parent:
<h1>Parent component</h1>
<app-child identifier='1'>
<span>Content projection from parent component</span>
<h4>The content is:</h4>
</app-child>
Ainsi:
- Le contenu de
<span></span>
est projeté dans la partie<ng-content select='span'></ng-content>
du template deChildComponent
. - Le contenu de
<h4></h4>
est projeté dans la partie<ng-content select='h4'></ng-content>
du template deChildComponent
.
L’affichage correspondant est:
Si on utilise plusieurs indications <ng-content>
mais qu’une de ces indications ne possède pas d’attribut select
, alors le contenu projeté sera le contenu qui ne correspond à aucune des autres clauses select
.
Par exemple, si on considère l’implémentation suivante dans le template du composant enfant:
<h3>Child component</h3>
<ng-content select='h4'></ng-content>
<ng-content></ng-content>
<ng-content select='span'></ng-content>
On modifie l’implémentation dans le template du composant parent:
<h1>Parent component</h1>
<app-child [identifier]='1'>
<span>Input value is: {{inputElement.value}}</span>
<h4>The content is:</h4>
<p><input #inputElement ngModel /></p>
</app-child>
Ainsi:
- Le contenu de
<span></span>
est projeté dans la partie<ng-content select='span'></ng-content>
. - Le contenu de
<h4></h4>
est projeté dans la partie<ng-content select='h4'></ng-content>
. - Le contenu de
<p></p>
est projeté dans<ng-content></ng-content>
car il ne correspond à aucunes autres clausesselect
.
L’affichage correspondant est:
Paramètre providers vs viewProviders de @Component()
La paramètre providers
du décorateur @Component()
est utilisé dans le cadre de l’injection de dépendances pour configurer les injecteurs au niveau du composant. Ce paramètre peut être utilisé pour les décorateurs:
@NgModule()
pour indiquer que l’instance injectée sera la même au niveau du module,@Component()
pour indiquer que l’instance injectée sera nouvelle à chaque exécution du constructeur du composant.
Ainsi, si on utilise le paramètre providers
dans le décorateur @Component()
, l’instance injectée sera la même pour le composant et pour tous ses composants enfant.
Contenu projeté vs contenu non projeté
Avant d’expliquer le paramètre viewProviders
, il faut distinguer 2 types de contenu présent dans la vue d’un composant:
- Le contenu non projeté: il contient les éléments implémentés dans la vue de ce composant:
- Directement si ces éléments sont implémentés dans le template ou
- Indirectement si ces éléments sont implémentés dans un composant enfant ou une directive.
- Le contenu projeté: il contient des éléments provenant d’un autre composant et qui sont projetés avec
<ng-content></ng-content>
.
Paramètre viewProviders de @Component()
A l’opposé du paramètre providers
, le paramètre viewProviders
va configurer les injecteurs pour que l’injection soit limitée exclusivement aux composants se trouvant dans le contenu non projeté d’une vue d’un composant.
L’intérêt de viewProviders
par rapport au paramètre providers
est de limiter l’injection d’une classe dans un cadre plus restreint. Par exemple dans le cas d’une bibliothèque à l’intérieur de laquelle on souhaite effectuer l’injection d’une classe. Les injections ne seront limitées aux composants ayant un contenu non projeté c’est-à-dire les contenus se trouvant à l’intérieur de la bibliothèque. Les contenus projetés provenant de l’extérieur de la bibliothèque ne seront pas concernés par ces injections.
Par exemple, si on considère 3 composants:
CallerComponent
qui sera le composant parent se trouvant à l’extérieur de la bibliothèque.LibraryComponent
qui sera le composant se trouvant à l’intérieur de la bibliothèque.ProjectedComponent
qui sera le composant qui sera injecté dans la bibliothèque.
On va effectuer une projection de contenu dans LibraryComponent
et ProjectedComponent
à partir de CallerComponent
sur plusieurs niveaux. On va utiliser successivement le paramètre providers
et viewProviders
de façon à voir les différences.
On considère la classe suivante qui sera injectée dans le composant:
export class Dependency {
message: string;
constructor(caller: string) {
this.message = 'Instance from: ' + caller;
}
}
Voici le code correspondant aux 3 composants:
- Pour
ProjectedComponent
:
Template <p><b>ProjectedComponent</b></p> <p>{{dependency.message}}</p> <ng-content></ng-content>
Classe du composant import { Component } from '@angular/core'; import { Dependency } from '../dependency'; @Component({ selector: 'app-projected', templateUrl: './projected.component.html', styleUrls: ['./projected.component.css'] }) export class ProjectedComponent { constructor(public dependency: Dependency) {} }
- Pour
LibraryComponent
:
Template <p><b>LibraryComponent</b></p> <p>{{dependency.message}}</p> Projected content from outside: <ng-content></ng-content>
Classe du composant import { Component } from '@angular/core'; import { Dependency } from '../dependency'; const dependency = new Dependency('library component'); @Component({ selector: 'lib-library', templateUrl: './library.component.html', styleUrls: ['./library.component.css'] }) export class LibraryComponent { constructor(public dependency: Dependency) {} }
- Et pour
CallerComponent
:
Template <p><b>CallerComponent</b></p> <p>{{dependency.message}}</p> <lib-library> <app-projected> <span>Projected content</span> </app-projected> </lib-library>
Classe du composant import { Component } from '@angular/core'; import { Dependency } from '../dependency'; @Component({ templateUrl: './caller.component.html', styleUrls: ['./caller.component.css'] }) export class CallerComponent { constructor(public dependency: Dependency) {} }
De façon très facultative, on modifie les fichiers de style des composants CallerComponent
et LibraryComponent
pour que l’affichage permette de faire la différence entre les différents composants:
- Dans le fichier CSS de
CallerComponent
(danscaller.component.css
)::host { border: 1px solid black; display: block; background-color: lightgrey; width: 500px; } app-projected { border: 2px dashed black; display: block; background-color: lightgreen; margin: 5px; padding: 5px; } span { border: 2px dashed black; background-color: white; }
- Dans le fichier CSS de
LibraryComponent
(danslibrary.component.css
)::host { border: 1px solid black; display: block; background-color: #bb8fce; margin: 5px; padding: 5px; } app-projected { border: 1px solid black; display: block; background-color: lightblue; margin: 5px; padding: 5px; } span { border: 2px dashed black; background-color: white; }
Paramètre providers au niveau du module uniquement
Si on place le paramètre providers
au niveau du module dans lequel se trouve tous les composants, la même instance sera injectée et utilisée dans tous les composants:
Le code du module devient:
import { Dependency } from '../Dependency';
const dependency = new Dependency('module');
@NgModule({
...
providers: [ { provide: Dependency, useValue: dependency} ]
})
Le paramètre useValue
permet de configurer les injecteurs pour injecter une instance précise de Dependency
.
A l’exécution, le résultat permet de montrer que l’instance provient du module:
Paramètre providers au niveau du composant
Si on ajoute le paramètre providers
au niveau du composant LibraryComponent
, l’instance utilisée sera:
- Dans
CallerComponent
, l’instance provenant du module. - Dans
LibraryComponent
, l’instance provenant dansLibraryComponent
et - Dans
ProjectedComponent
, l’instance provenant deLibraryComponent
puisqueProjectedComponent
est projeté dansLibraryComponent
.
On modifie LibraryComponent
pour instancier une nouvelle classe Dependency
et ajouter le paramètre providers
à ce niveau:
Template |
|
Classe du composant |
|
Le résultat permet de montrer que l’instance Dependency
utilisée dans LibraryComponent
et ProjectedComponent
est celle instanciée dans LibraryComponent
:
Paramètre viewProviders au niveau de LibraryComponent
Si on modifie LibraryComponent
pour utiliser viewProviders
au lieu de providers
, l’instance injectée sera:
- Dans
CallerComponent
, l’instance provenant du module (pas de changement). - Dans
LibraryComponent
, l’instance provenant dansLibraryComponent
(pas de changement) et - Dans
ProjectedComponent
, l’instance provenant du module carviewProviders
permet de limiter l’injection au contenu non projeté.ProjectedComponent
étant projeté dansLibraryComponent
, l’instance deDependency
injectée sera celle du module.
On modifie LibraryComponent
pour utiliser viewProviders
au lieu de providers
:
Template |
|
Classe du composant |
|
Le résultat permet de montrer que l’instance Dependency
utilisée dans ProjectedComponent
est celle instanciée dans le module c’est-à-dire à l’extérieur de la bibliothèque:
Ajout d’un contenu directement dans LibraryComponent
On va rajouter le composant enfant ProjectedComponent
directement dans LibraryComponent
. Cette instance de ProjectedComponent
ne sera pas projetée mais fera partie directement du contenu de LibraryComponent
. Cet exemple permet de montrer que les instances de Dependency
seront différentes dans le cas où ProjectedComponent
fait partie du contenu projeté ou non projeté. Ainsi:
- Dans l’instance de
ProjectedComponent
qui fait partie du contenu projeté, l’instance injectée deDependency
est celle du module à cause du paramètreviewProviders
au niveau deLibraryComponent
. - Dans l’instance de
ProjectedComponent
qui fait partie du contenu non projeté, l’instance injectée deDependency
est celle deLibraryComponent
.
On modifie seulement le fichier template de LibraryComponent
pour ajouter ProjectedComponent
en tant composant enfant:
Template |
|
Classe du composant |
|
Dans le résultat, on peut voir que la 2e instance de ProjectedComponent
faisant partie du contenu non projeté de LibraryComponent
comporte une instance de Dependency
provenant de LibraryComponent
. Le reste des injections n’est pas modifié:
Ordre d’exécution des callbacks du cycle de vie d’un composant
Lors de la création et du rendu de la vue d’un composant, certaines callbacks peuvent être déclenchées de façon successive dans la classe du composant dans le cas où elles sont implémentées. Dans la documentation Angular ces callbacks sont appelées “lifecycle hooks”.
Pour rappel, à l’initialisation d’un composant, les callbacks de ce cycle de vie (i.e. Lifecycle hooks) sont, dans l’ordre de déclenchement:
ngOnChanges()
: cette callback est exécutée si le composant contient des propriétés en entrée (notamment avec le décorateur@Input()
).ngOnInit()
: déclenchée après l’exécution du constructeur. Il permet d’initialiser le composant avec le 1er affichage des données de la vue ayant un binding avec des propriétés de la classe du composant. Le cas échéant, il permet d’affecter les paramètres en entrée du composant. Cette callback est déclenchée une seule fois à l’initialisation du composant même singOnChanges()
n’est pas déclenchée.ngDoCheck()
permet d’indiquer des changements si Angular ne les a pas détecté.ngAfterContentInit()
est déclenchée à l’initialisation après la projection de contenu. Elle est déclenchée même s’il n’y a pas de contenu à projeter.ngAfterContentChecked()
: déclenchée après la détection de changement dans le contenu projeté. Cette callback est déclenchée même s’il n’y a pas de projection de contenu.ngAfterViewInit()
: déclenchée après l’initialisation de la vue du composant et après l’initialisation de la vue des composants enfant.ngAfterViewChecked()
est déclenchée après détection d’un changement dans la vue du composant et dans la vue des composants enfant.ngOnDestroy()
est déclenchée avant la destruction du composant.
A chaque détection de changements, les callbacks déclanchées sont, dans l’ordre:
ngOnChanges()
si les paramètres en entrée du composant sont modifiés.ngDoCheck()
ngAfterContentChecked()
est déclenchée même s’il n’y a pas de contenu projeté.ngAfterViewChecked()
.
Ainsi quand le composant comporte des composants enfant, l’ordre d’exécution est le suivant à l’initialisation:
Exécution du constructeur du composant parent | |
Exécution du constructeur du composant enfant | |
Exécution des callbacks du composant parent jusqu’à ngAfterContentChecked() |
|
Exécution des callbacks du composant enfant y compris ngAfterViewInit() et ngAfterViewChecked() |
|
Exécution des callbacks restantes du parent |
|
Lors de la détection d’un changement, les callbacks sont exécutées dans cet ordre:
Exécution des callbacks du composant parent sauf ngAfterViewChecked() |
|
Exécution des callbacks du composant enfant |
|
Exécution de ParentComponent.ngAfterViewChecked() |
Pour résumer…
Pour imbriquer un composant dans un autre:
- D’abord il faut que le composant enfant soit déclaré dans le module du composant parent pour que la résolution réussisse.
- Ensuite le template du composant parent doit contenir le contenu du paramètre
selector
du composant enfant:
Par exemple si le paramètreselector
du composant est:@Component({ selector: 'app-child', templateUrl: './child.component.html' }) export class ChildComponent {}
- Le template du composant doit contenir:
<app-child></app-child>
Pour injecter un composant parent dans un composant enfant:
Pour injecter un des composants parent dans un composant enfant, il suffit d’indiquer un paramètre du type du parent dans le constructeur du composant enfant:
export class ChildComponent {
constructor(parent: ParentComponent) {
console.log(parent.innerValue);
}
}
Pour éviter un couplage trop fort entre un composant parent et un composant enfant, on peut utiliser une classe abstraite au lieu d’injecter directement le parent. Le composant parent doit satisfaire la classe abstraite et l’injecteur de dépendances doit être configuré avec l’option useExisting
:
@Component({
[...]
providers: [{ provide: AbstractComponent, useExisting: forwardRef(() => ParentComponent) }]
})
export class ParentComponent extends AbstractComponent {
constructor() {
super();
}
}
L’injection est effectuée dans le composant enfant avec la classe abstraite:
export class ChildComponent {
constructor(parent: AbstractComponent) {
}
}
Pour effectuer un binding avec le composant enfant:
- Property binding:
Le composant enfant doit déclarer les propriétés exposées en tant que paramètre d’entrée avec le décorateur@Input()
, par exemple:@Input() identifier: number;
Le composant parent peut effectuer un property binding avec la propriété du composant enfant à partir du template:
<app-child [identifier]='1'></app-child>
Avec un alias, la propriété doit être de cette façon:
@Input('identifier_alias') identifier: number;
On utilise l’alias dans le template du composant parent:
<app-child [identifier_alias]='1'></app-child>
- Event binding:
Le composant enfant doit déclarer l’évènement à exposer avec le décorateur@Output()
etEventEmitter
:@Ouput() updated: EventEmitter<number>= new EventEmitter<number>();
Le composant parent peut effectuer un event binding avec la propriété du composant enfant à partir du template:
<app-child (updated)='triggerFunction($event)'></app-child>
triggerFunction($event)
est la fonction du composant parent qui sera exécutée à chaque déclenchement de l’évènement.
Il est aussi possible d’utiliser un alias avec@Output()
.
Content projection
La fonctionnalité content projection permet de projeter un contenu à partir du composant parent dans un composant enfant.
Si 'app-child'
est le selector
du composant enfant, pour qu’un contenu soit projeté à partir du composant parent la syntaxe dans le template doit être:
<app-child>
<!-- Contenu projeté -->
</app-child>
L’emplacement du contenu à projeter doit être indiqué dans le composant enfant avec:
<ng-content></ng-content>
Si <ng-content>
est omis dans le composant enfant, il n’y aura pas d’erreur.
On peut projeter plusieurs contenus en les nommant avec l’attribut select
. Par exemple:
<ng-content select='h4'></ng-content>
<ng-content></ng-content>
<ng-content select='span'></ng-content>
Si le template du composant parent est:
<app-child [identifier]='1'>
<span>Input value is: {{inputElement.value}}</span>
<h4>The content is:</h4>
<p><input #inputElement ngModel /></p>
</app-child>
Alors:
- Le contenu de
<span></span>
est projeté dans la partie<ng-content select='span'></ng-content>
. - Le contenu de
<h4></h4>
est projeté dans la partie<ng-content select='h4'></ng-content>
. - Le contenu de
<p></p>
est projeté dans<ng-content></ng-content>
car il ne correspond à aucuns autres attributsselect
.
Paramètres providers et viewProviders de @Component()
Les paramètres providers
et viewProviders
du décorateur @Component()
permettent de paramètrer les injecteurs pour effectuer de l’injection de dépendances:
providers
configure les injecteurs d’un composant pour que la même instance d’un objet soit injectée dans ce composant et dans tous les composants et directives dont il est le parent.viewProviders
limite l’injection d’une instance d’objet aux composants et directives faisant partie du contenu non projeté d’un composant.
Lifecycle hooks
Dans le cas où un composant comporte un composant enfant, l’ordre d’exécution des lifecycle hooks est plus complexe:
- A l’initialisation:
- A chaque détection d’un changement: