Les composants Angular

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

@brodanoel

Les composants font partie des objets les plus importants d’Angular car ils permettent d’afficher des vues. Chaque vue correspond à une unité capable d’afficher une partie d’une application Angular.

Pour ordonner cette unité d’implémentation, un composant est formé de différents éléments:

  • La vue du composant appelée template: cette partie comporte du code HTML correspondant aux objets statiques. Ce squelette statique est enrichi par du code interprété par Angular pour permettre des interactions dynamiques entre les objets statiques et du code métier se trouvant dans le reste du composant.
  • La classe du composant: c’est une classe Typescript dans laquelle on peut définir:
    • des membres contenant les données affichées par la vue,
    • des méthodes et fonctions pour exécuter des traitements déclenchés par des actions sur la vue ou par des évènements divers.
  • Des métadonnées: il s’agit d’informations supplémentaires d’implémentation qui permettront à Angular d’interfacer le template avec la classe du composant.

Cet article permet de présenter les fonctionnalités les plus importantes des composants de façon à pouvoir en créer et à en comprendre les principales caractéristiques. Les détails de chaque fonctionnalité seront présentés dans d’autres articles.

Fonctionnement général d’un composant

Une vue Angular est composée d’éléments HTML avec une hiérarchie comme une page HTML classique. Angular s’interface avec le DOM correspondant à ces éléments de façon à en modifier les caractéristiques de façon dynamique. Ainsi, la vue comporte des éléments qui vont rester statiques durant l’exécution de l’application et d’autres éléments dont l’affichage pourrait être modifié suivant différents évènements. L’implémentation de ces éléments se fait dans la classe du composant et dans le fichier template.

Création d’un composant

Par exemple, après avoir créé une application Angular avec le CLI Angular en utilisant ng build (voir Créer une application Angular “from scratch” pour plus de détails), on peut créer un composant nommé Example en exécutant:

ng generate component example 

Ou de façon plus condensée:

ng g c example 

Cette étape effectue plusieurs opérations:

~% ng g c example 
CREATE src/app/example/example.component.css (0 bytes) 
CREATE src/app/example/example.component.html (22 bytes) 
CREATE src/app/example/example.component.spec.ts (635 bytes) 
CREATE src/app/example/example.component.ts (279 bytes) 
UPDATE src/app/app.module.ts (815 bytes) 

Dans un premier temps, 4 fichiers sont créés:

  • example.component.html qui est le fichier template dans lequel on peut implémenter les éléments graphiques de la vue.
  • example.component.ts qui est la classe Typescript du composant.
  • example.component.css qui contient de façon facultative le style CSS utilisé par la vue du composant.
  • example.component.spec.ts contenant les tests unitaires à implémenter pour le composant.

Dans un 2e temps, le composant sera rajouté dans le Root Module de l’application dans app.module.ts:

@NgModule({ 
  declarations: [ 
    AppComponent, 
    ExampleComponent
  ], 
  imports: [ 
    BrowserModule, 
    AppRoutingModule 
  ], 
  providers: [], 
  bootstrap: [AppComponent] 
}) 
export class AppModule { } 

Affichage d’un composant avec le paramètre selector

L’ajout du composant au module de l’application permet à Angular de configurer la factory de composants de façon à instancier le composant dans le cas où il faudrait l’afficher. Toutefois à ce stade, il manque un élément important: il n’y a pas d’éléments dans le code permettant d’indiquer où la vue du composant sera affichée.

On peut indiquer où le composant sera affiché en utilisant le paramètre selector dans les metadatas du composant. Ce paramètre est indiqué dans la classe du composant (cf. example.component.ts):

@Component({ 
  selector: 'app-example',
  templateUrl: './example.component.html', 
  styleUrls: ['./example.component.css'] 
}) 
export class ExampleComponent implements OnInit { 
  constructor() { } 

  ngOnInit(): void { 
  } 
} 

La valeur 'app-example' du paramètre selector indique que le composant Example sera affiché si le template d’un autre composant contient:

<app-example></app-example> 

Ainsi, si on indique ce code dans le template du composant principal (dans le fichier src/app/app.component.html) après en avoir supprimé tout le contenu:

<app-example></app-example> 

Si on lance l’application en exécutant la commande ng server --watch, la vue est directement affichée de cette façon:

example work! 

Afficher plusieurs vues

On peut afficher plusieurs vues en même temps suivant les besoins de l’application. Par exemple si on considère les fichiers index.html et styles.css suivants de façon à afficher 4 zones: header, content, sidebar et footer:

index.html styles.css
<html> 
  <head> 
    <title>Test</title> 
    <link rel="stylesheet" 
      type="text/css" 
      href="styles.css" /> 
  </head> 
  <body> 
    <div id="header"> 
      Header 
    </div> 
    <div id="sidebar"> 
      Sidebar 
    </div> 
    <div id="content"> 
      Content 
    </div> 
    <div id="footer"> 
      Footer 
    </div> 
  </body> 
</html> 
html, body { 
  background: lightgreen; 
  margin: 0; 
  padding: 0; 
  height: 100%; 
} 

div#header { 
  padding: 10px; 
  height: 90px; 
  background: yellow; 
} 

div#footer { 
  position: fixed; 
  height: 30px; 
  bottom: 0; 
  width: 100%; 
  background: lightsalmon; 
  padding-left: 10px; 
} 

div#content { 
  position: relative; 
  margin: 0 210 0 0; 
  bottom: 0; 
  padding-left: 10px; 
  padding-right: 10px; 
} 

div#sidebar { 
  background: lightblue; 
  float: right; 
  width: 190px; 
  position: fixed; 
  right: 0; 
  height: inherit; 
} 

Ces fichiers permettant d’afficher une page comportant 4 zones:

Dans le cadre d’une application Angular, on peut utiliser un composant pour chaque zone et ainsi afficher plusieurs vues. Par exemple, si on reprend l’exemple précédent en créant 4 nouveaux composants:

  1. On exécute les commandes suivantes avec le CLI Angular pour créer les composant header, footer, content et sidebar:
    ~% ng g c header 
    ~% ng g c footer  
    ~% ng g c content 
    ~% ng g c sidebar 
    
  2. On modifie les fichiers templates des composants créés pour que le contenu soit similaire au fichier d’exemple d’origine:
    • app/src/header/header.component.html:
      Header 
      
    • app/src/footer/footer.component.html:
      Footer 
      
    • app/src/sidebar/sidebar.component.html:
      Sidebar 
      
    • app/src/content/content.component.html:
      Content
      
  3. On copie les styles CSS de le fichier src/styles.css de façon à ce que les styles CSS soient disponibles dans toute l’application.
  4. On modifie le template du composant principal dans src/app/app.component.html pour afficher tous les composants en utilisant les paramètres selector de ces composants:
    <div id="header"> 
      <app-header></app-header> 
    </div> 
    <div id="sidebar"> 
      <app-sidebar></app-sidebar> 
    </div> 
    <div id="content"> 
      <app-content></app-content> 
    </div> 
    <div id="footer"> 
      <app-footer></app-footer> 
    </div> 
    

L’affichage obtenu est similaire à l’exemple de départ. On peut voir que chaque partie correspond à un composant différent et à une vue différente.

Liens entre le template et la classe du composant

Pour rendre dynamique l’affichage de la vue d’une composant, Angular permet d’implémenter des interactions entre un template et la classe d’un composant. Ces interactions se font par l’intermédiaire de bindings. L’implémentation de ces bindings permet d’enrichir la vue avec des données ou de déclencher l’exécution de code dans la classe du composant.

Sur le schéma suivant provenant de la documentation Angular, on peut voir 2 types de bindings:

  • Property binding permettant d’échanger des données de la classe du composant vers le template. Ce type de binding permet de modifier une propriété d’un objet dans le DOM à partir d’un membre de la classe du composant.
  • Event binding pour exécuter du code dans la classe du composant à partir d’évènements déclenchés dans le template.

Il existe d’autres types de bindings qui sont décrits de façon plus détaillée dans l’article Les vues des composants Angular:

Interpolation

Le binding le plus simple est l’interpolation. Il permet d’exécuter directement une expression Typescript contenant des membres ou des fonctions publiques dans la classe du composant, par exemple:

Template
<div>{{textToDisplay}}</div>
Classe du composant
@Component({ ... })
export class ExampleComponent {  
  textToDisplay = 'Texte à afficher'; 
} 

Dans cet exemple, à l’exécution {{textToDisplay}} dans le template sera évalué et remplacé par la valeur du membre textToDisplay de la classe.

Property binding

Ce type de binding permet de mettre à jour le contenu d’une propriété DOM d’un élément affiché avec la valeur d’un membre dans la classe du composant, par exemple:

Template
<div [innerText]="textToDisplay"></div> 
Classe du composant
@Component({ ... })  
export class ExampleComponent {  
  textToDisplay = 'Texte à afficher'; 
} 

Dans cet exemple, la propriété innerText de l’objet du DOM sera liée au membre textToDisplay de la classe.

Event binding

Ce binding déclenche l’exécution d’une fonction dans le classe du composant à partir du déclenchement d’un évènement dans un objet du DOM, par exemple:

Template
<p>{{valueToDisplay}}</p> 
<button (click)="incrementValue()">Increment</button> 
Classe du composant
@Component({ ... })
export class ExampleComponent {  
  valueToDisplay = 0; 
 
  incrementValue(): void { 
    this.valueToDisplay++; 
  } 
} 

Dans cet exemple, l’exécution de la fonction incrementValue() dans la classe du composant est lancée quand l’évènement click survient dans l’objet button.

Les autres types de bindings sont décrits dans Les vues des composants Angular.

Composants enfants

Une fonctionnalité importante des composants est qu’ils peuvent contenir d’autres composants. Ainsi, 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ètre selector du composant est:

    @Component({ 
      selector: 'app-child',
      templateUrl: './child.component.html' 
    }) 
    export class ChildComponent {} 
    
  • Le template du composant doit contenir:
    <app-child></app-child> 
    

Les interactions entre le composant parent et un composant enfant sont possibles en utilisant:

  • Des paramètres d’entrée du composant enfant avec @Input().
  • Des évènements de sortie du composant enfant avec @Output().
  • Injecter du contenu dans le composant enfant avec la fonctionnalité content projection.

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 attributs select.

Paramètre d’entrée

Le composant parent peut injecter un paramètre dans le composant enfant en utilisant un property binding. Ainsi 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:

@Component({ 
  selector: 'app-child', 
  templateUrl: './child.component.html' 
}) 
export class ChildComponent { 
  @Input() identifier: number; 
} 

Dans cet exemple, le paramètre d’entrée est identifier.

Le composant parent peut effectuer un property binding:

Template
<h1>Parent component</h1> 
<app-child [identifier]='childIdentifier'></app-child>
Classe du composant
import { Component } from '@angular/core'; 
 
@Component({ 
  templateUrl: './parent.component.html' 
}) 
export class ParentComponent { 
  childIdentifier = 1; 
} 

Le membre childIdentifier du composant parent est injecté par property binding dans le paramètre identifier du composant enfant.

Evènement de sortie

Le composant enfant peut déclencher un évènement qui exécutera une fonction dans le composant parent par event binding.

La déclaration de l’évènement dans le composant enfant se fait en utilisant @Output() et l’objet EventEmitter. EventEmitter permettra d’émettre l’évènement, par exemple:

Template
<p>Child component with identifier: {{identifier}}</p> 
<p> 
    <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; 

  @Ouput() countUpdated: EventEmitter<number>= new EventEmitter<number>();
 
  incrementValue(): void { 
    this.internalCount++; 
    this.countUpdated.emit(this.internalCount); 
  } 
}

Le composant parent peut être notifié du déclenchement de l’évènement en utilisant un event binding sur l’évènement du composant enfant, par exemple:

Template
<h1>Parent component</h1> 
<app-child (countUpdated)='updateTotalCount($event)'></app-child> 
Classe du composant
import { Component } from '@angular/core'; 
     
@Component({ 
  templateUrl: './parent.component.html' 
}) 
export class AppComponent { 
  totalCount = 0; 

  updateTotalCount(count: number): void { 
    this.totalCount = count; 
  } 
}

La fonction updateTotalCount() dans le composant parent est exécutée à chaque déclenchement de l’évènement countUpdated dans le composant enfant.

Il est possible d’implémenter d’autres types d’interactions entre un composant parent et un composant enfant, pour plus de détails voir Les composants enfant.

Cycle de vie d’un composant

Les bindings entre la vue et la classe d’un composant peuvent provoquer des changements dans l’affichage des objets dans la vue d’un composant. La répercussion de ces changements dans la vue se fait par Angular de façon transparente. Par exemple, dans le cas d’une interpolation avec le membre d’une classe, chaque nouvelle valeur du membre provoquera un changement de la valeur affichée:

Template
<div>{{textToDisplay}}</div>
Classe du composant
@Component({ ... })   

export class ExampleComponent {
  
textToDisplay = 'Texte à afficher';  

}

Pour mettre à jour un élément graphique, Angular doit s’interfacer avec le DOM pour créer ou supprimer des objets ou en modifier des propriétés.

Ces accès multiples au DOM sont coûteux en performance. Ainsi, afin d’en limiter au maximum les accès et de modifier les propriétés au minimum, la solution d’Angular est de détecter automatiquement les changements nécessitant une modification dans le DOM. Cette détection s’exécute dans la version des objets maintenue par Angular. Si Angular détecte un changement, il répercute ce changement dans le DOM de façon à ce que les éléments graphiques correspondant puissent être modifiés.

L’algorithme de détection de changements effectue les mises à jour des éléments graphiques suivant un ordre précis. Tout au long de ces mises à jour et suivant les éléments qui sont mis à jour, il va aussi exécuter les callbacks du cycle de vie des composants. La détection de changements est donc très liée au cycle de vie des composants.

Les callbacks peuvent être implémentées au niveau de la classe d’un composant en héritant de l’interface correspondante, par exemple pour OnInit():

import { OnInit } from '@angular/core';  

@Component({ ... })  
export class ExampleComponent implements OnInit {  

  ngOnInit() {  
  }  
}  

Pour implémenter plusieurs callbacks, il suffit de satisfaire plusieurs interfaces. Par exemple pour OnInit() et DoCheck():

import { OnInit, DoCheck } from '@angular/core';  

@Component({ ... })  
export class ExampleComponent implements OnInit, DoCheck {  

  ngOnInit() {  
  }  

  ngDoCheck() {  
  }  
}  

Le but des callbacks est de pouvoir interagir finement avec le framework durant les différentes phases de création d’un composant et de sa vue dans un premier temps ou lors de la détection de changement dans un 2e temps. La succession de ces différentes phases permet d’intervenir:

  • Dans le cas de l’initialisation d’un composant: avant ou après l’initialisation d’un élément particulier de ce composant (paramètres d’entrée, contenu projeté, requête sur la vue etc…)
  • Dans le cas de la détection de changement: avant ou après la vérification d’un changement sur un élément particulier de ce composant.

Ainsi, à l’initialisation d’un composant, les callbacks de ce cycle de vie (i.e. lifecycle hooks) sont, dans l’ordre de déclenchement:

  1. ngOnChanges(): cette callback est exécutée si le composant contient des propriétés en entrée (notamment avec le décorateur @Input()). Si cette callback est implémentée sans paramètre, elle sera déclenchée autant de fois qu’il y a de propriétés en entrée du composant.

    Si la callback est implémentée avec un argument de type SimpleChanges:

    void ngOnChanges(changes: SimpleChanges): void {}
    

    Elle sera déclenchée une seule fois.

  2. ngOnInit(): déclenchée après l’exécution du constructeur. Elle 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. Cette callback est déclenchée une seule fois à l’initialisation du composant même si ngOnChanges() n’est pas déclenchée.
  3. ngDoCheck() permet d’indiquer des changements si Angular ne les a pas détecté.
  4. 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.
  5. 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.
  6. ngAfterViewInit(): déclenchée après l’initialisation de la vue du composant et après l’initialisation de la vue des composants enfant.
  7. ngAfterViewChecked() est déclenchée après détection d’un changement dans la vue du composant et dans la vue des composants enfant.
  8. 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:

  1. ngOnChanges() si les paramètres en entrée du composant sont modifiés.
  2. ngDoCheck()
  3. ngAfterContentChecked() est déclenchée même s’il n’y a pas de contenu projeté.
  4. ngAfterViewChecked().

Il faut avoir en tête l’ordre d’appels des callbacks du cycle de vie du composants puisque chaque appel survient avant ou après une opération particulière d’Angular concernant l’initialisation d’objets de la vue, la détection des changements sur ces objets et leur destruction.

Pour plus de détails sur le cycle de vie des composants et sur la détection de changements voir les articles:

Requêter les éléments d’un vue d’un composant

Les bindings permettent d’interfacer des objets de la classe du composant vers le template. A l’opposé si on souhaite effectuer un traitement sur un objet de la vue à partir de la classe du composant, il faut effectuer une requête sur la vue. Cette requête permet de récupérer l’instance d’un composant enfant, d’une directive ou d’un objet du DOM.

Pour effectuer une requête sur la vue, on peut s’aider des décorateurs @ViewChild(), @ViewChildren(), @ContentChild() ou @ContentCildren(). Ces décorateurs se placent devant la propriété ou le membre de la classe du composant dans lesquels l’instance doit être renseignée. Le choix du décorateur à utiliser dépend du type d’objet à requêter:

  • @ViewChild() pour effectuer une requête sur un seul objet directement dans la vue du composant. Seul le 1er objet de la vue satisfaisant la requête est renvoyé.
  • @ViewChildren() pour effectuer une requête sur une liste d’objets directement dans la vue du composant. Tous les objets satisfaisant la requête sont renvoyés.
  • @ContentChild() pour effectuer une requête sur un seul objet se trouvant dans du contenu projeté (i.e. content projection) sur la vue du composant. Seul le 1er objet de la vue satisfaisant la requête est renvoyé.
  • @ContentChildren() pour effectuer une requête sur une liste d’objets se trouvant dans du contenu projeté (i.e. content projection) sur la vue du composant. Tous les objets satisfaisant la requête sont renvoyés.

Par exemple, pour requêter un élément dans la vue d’un composant:

Template
<p>Example component</p>
<span #spanElement>Span content</span>
Classe du composant
import { Component, ViewChild, OnInit, ElementRef } from '@angular/core';

@Component({
  templateUrl: './example.component.html'
})
export class ExampleComponent implements OnInit {
  @ViewChild('spanElement', { static: true }) spanReference: ElementRef;

  ngOnInit() {
    console.log(this.spanReference.nativeElement);
  }
}

Ainsi, avec une variable référence pour désigner l’objet span, on peut utiliser le décorateur @ViewChild() pour binder l’objet du DOM avec le membre spanReference. Le binding sera effectué quand la callback ngOnInit() est déclenchée.

L’objet du DOM est wrappé dans un objet de type ElementRef. Cet objet permet de récupérer un élément du DOM en utilisant la propriété nativeElement.

Pour requêter des objets d’une vue, les critères utilisés peuvent être de nature différente.

Requêter suivant un type

2 méthodes sont possibles pour spécifier le type de l’objet à requêter:

  • Spécifier directement le type en tant qu’argument du décorateur utilisé, par exemple pour requêter une directive dont le type est TypedDirective:
    @ViewChild(TypedDirective) requestedDirective: TypedDirective;

    Dans cet exemple, on a utilisé @ViewChild() toutefois les autres décorateurs utilisent la même syntaxe.

  • Dans le cas où on effectue une requête avec le nom d’une variable référence, on peut préciser le type attendu de l’objet en l’indiquant en utilisant l’option read.

    Par exemple, pour requêter une directive avec une variable référence 'directiveRef' et dont le type est TypedDirective:

    @ViewChild('directiveRef', { read: TypedDirective }) requestedDirective: TypedDirective;

Requêter suivant une variable référence

Si l’objet est identifié dans la vue en utilisant une variable référence, on peut requêter en utilisant le nom de cette variable.

Par exemple, pour requêter une directive avec une variable référence 'directiveRef':

@ViewChild('directiveRef') requestedDirective: TypedDirective;

Indiquer si l’objet fait partie du contenu statique de la vue

Le contenu de la vue d’un composant est séparé en 2 parties:

  • Un contenu statique: ce contenu permet de mettre à jour le DOM seulement à l’initialisation de la vue. Ce contenu est initialisé juste avant le déclenchement des callbacks ngOnInit() et/ou ngDoCheck() du cycle de vie du composant.
  • Un contenu dynamique: cette partie de la vue est mise à jour à chaque détection de changement. Ce contenu est initialisé et mis à jour à des périodes différentes suivant l’objet requêté:
    • Si l’objet se trouve directement dans la vue du composant: il est requêté avec @ViewChild() ou @ViewChildren(). Le contenu dynamique de cet objet est initialisé et mis à jour juste avant le déclenchement des callbacks ngAfterViewInit() et/ou ngAfterViewChecked().
    • Si l’objet se trouve dans du contenu projeté: il est requêté avec @ContentChild() ou @ContentChildren(). Le contenu dynamique de cet objet est initialisé et mis à jour avant le déclenchement des callbacks ngAfterContentInit() et/ou ngAfterContentChecked().

A l’initialisation d’un composant, le DOM est mis à jour à 2 reprises: lors de la création du contenu statique de la vue et lors de la mise à jour du contenu dynamique. On peut résumer cette mise à jour, l’exécution des callbacks du cycle de vie et l’exécution des requêtes sur les vues dans le schéma suivant:

Légende du schéma
  1. A l’initialisation, le DOM est mis à jour avec le contenu statique de la vue.
  2. Les requêtes avec le paramètre { static: true } sur le contenu projeté (avec @ContentChild() ou @ContentChildren()) et sur la vue (avec @ViewChild() ou @ViewChildren()) sont exécutées sur le contenu statique de la vue.
  3. A l’exécution de la callback ngOnInit(), les requêtes sur le contenu statique ont été exécutées.
  4. Les requêtes avec le paramètre { static: false } sur le contenu projeté (avec @ContentChild() ou @ContentChildren()) sont exécutées. Le lien avec le composant parent n’apparaît pas sur ce schéma toutefois à ce state, le contenu dynamique du DOM du composant parent a été mise à jour.
  5. A chaque détection de changements, le contenu dynamique de la vue est mis à jour.
  6. Les requêtes avec le paramètre { static: false } sur la vue (avec @ViewChild() ou @ViewChildren()) sont exécutées sur le contenu dynamique de la vue.
  7. A l’exécution de la callback ngAfterViewInit(), les requêtes sur le contenu dynamique ont été exécutées.

Un objet peut être requêté dans le contenu statique ou dynamique de la vue suivant la valeur de l’option static:

  • { static: false }: valeur par défaut, elle permet de requêter le contenu statique et dynamique d’une vue. Le résultat de cette requête est disponible dans la propriété au déclenchement des callbacks:
    • ngAfterContentInit() et/ou ngAfterContentChecked() pour du contenu projeté.
    • ngAfterViewInit() et/ou ngAfterViewChecked() pour une requête directement sur la vue.
  • { static: true }: permet de requêter seulement le contenu statique d’une vue. Le résultat de cette requête est disponible dans la propriété au déclenchement des callbacks ngOnInit() et/ou ngDoCheck().

    Par exemple, pour requêter un élément HTML p nommé 'content' dans le contenu statique de la vue:

    @ViewChild('content', { static: true }) contentRef: ElementRef<HTMLParagraphElement>;
    

QueryList

Si on utilise @ViewChildren() ou @ContentChildren() pour requêter une liste d’objets, on peut utiliser la liste QueryList pour stocker la liste des objets.

Par exemple, pour requêter des directives de type TypedDirective directement sur la vue d’un composant:

@ViewChildren(TypedDirective) requestedDirectives: QueryList<TypedDirective>;

Injection de dépendances

Angular permet d’effectuer de l’injection des dépendances d’un composant à son instanciation. Suivant la façon dont les dépendances sont déclarées, le framework d’injection de dépendances pourra instancier un nouvel objet ou utiliser une instance existante sous forme de singleton. Les objets injectés dans un composant peuvent être des classes ou des services.

Par exemple, si on considère un service à injecter dans un composant. Ce composant est déclaré dans un module:

@NgModule({  
  declaration: [ ExampleComponent ],  
  ...  
})  

Le service est déclaré avec le décorateur @Injectable() pour indiquer qu’il s’agit d’une classe injectable:

import { Injectable } from '@angular/core';  

@Injectable({  
  providedIn: 'root'  
})  
export class InjectedService {  
}  

Un singleton de ce service peut être injecté dans le composant ExampleComponent simplement en ajoutant la paramètre du service en tant qu’argument du constructeur:

import { Component } from '@angular/core';  
import { InjectedService } from '../InjectedService';  

@Component({  
  ...  
})  
export class ExampleComponent {  
  constructor(private InjectedService: InjectedService) {}  
}  

Avec cette implémentation, le service est un singleton dont l’instance est accessible dans toute l’application à cause de la déclaration:

@Injectable({  
  providedIn: 'root'  
})  

Il existe d’autres configurations possible pour déclarer un objet à injecter. il possible de configurer l’injection pour qu’une instance soit créée au chargement d’un module ou qu’une nouvelle instance soit créée à chaque injection. Pour voir plus en détails tous ces éléments de configuration voir l’article Injection de dépendances dans une application Angular.

Méthodes pour afficher un composant

La vue d’un composant peut être affichée suivant différentes méthodes:

  • En utilisant le paramètre selector comme on a pu le voir plus haut.
  • Avec le module de routing.
  • Par programmation.

Utiliser un module de routing

Un module de routing permet de configurer l’affichage d’un composant en fonction d’indication dans l’URL. Avec un module de routing, il suffit de configurer le composant devant être affiché en fonction d’un chemin indiqué dans l’URL.

En créant une application Angular avec l’option --routing:

ng new <nom application> —-routing  

Avec cette configuration, le module de routing est créé dans le fichier src/app/app-routing.module.ts. Ce module est importé dans le Root Module dans src/app/app.module.ts:

@NgModule({  
  declarations: [ ... ],  
  imports: [  
    AppRoutingModule,  
    ...  
  ]  
})  
export class AppModule { }  

Le module de routing (dans notre exemple ce module se trouve dans src/app/app-routing.module.ts) contient des routes indiquant le chemin de l’URL et le composant à afficher suivant ce chemin, par exemple:

import { NgModule } from '@angular/core';  
import { Routes, RouterModule } from '@angular/router';  
import { AppComponent } from '../app.component';  
import { ExampleComponent } from './example/example.component';  

const routes: Routes = {  
  { path: 'example', component: ExampleComponent }  
  { path: '', component: AppComponent }  
}  

@NgModule({  
  imports: [RouterModule.forRoot(routes)],  
  exports: [RouterModule]  
})  

Ainsi cette configuration permet d’afficher:

  • Le composant ExampleComponent si l’URL est http://local host:4200/#example.
  • Le composant AppComponent si l’URL est http://local host:4200/#.

Ces composants seront affichés au niveau du composant principal (i.e. src/app/app.component.html) car le template du fichier principal app.component.html contient:

<router-outlet></router-outlet>  

Plus concrètement, à l’exécution, <router-outlet></router-outlet> sera remplacé par la vue du composant à afficher.

Afficher un composant par programmation

On peut afficher des composants par programmation en utilisant une factory pour instancier un composant à afficher.

Par exemple si on considère 2 composants Parent et Child. On souhaite afficher Child par programmation dans la vue de Parent.

  1. Pour créer les 2 composants, on exécute les instructions:
    ~% ng g c Parent  
    ~% ng g c Child  
    

    Ces instructions vont créer les composants et les rajouter au module principal dans src/app/app.module.ts.

  2. On indique l’emplacement dans la vue du composant Parent dans lequel on placera la vue du composant Child. Cet emplacement est de type <ng-template> qui est un élément Angular dans lequel on peut placer une vue. On modifie le template du comparent Parent (dans src/app/parent/parent.component.html) de cette façon:
    <p>Composant Parent</p>  
    <ng-template #childComponentPlace></ng-template>  
    

    #childComponentPlace correspond à une variable référence qui permet de nommer l’élément <ng-template>.

  3. Dans la classe du composant Parent (dans src/app/parent/parent.component.ts), on effectue une requête dans la vue de ce composant pour récupérer l’emplacement dans lequel on va placer la vue du composant Child. On effectue cette requête en utilisant @ViewChild:
    import { Component, ViewContainerRef, ViewChild } from '@angular/core';  
    
    @Component({  
      selector: 'app-parent',  
      templateUrl: './parent.component.html'  
    })  
    export class ParentComponent {  
      @ViewChild('childComponentPlace', { read: ViewContainerRef }) childComponentRef: ViewContainerRef;  
    }
    
  4. On modifie le template du composant principal (dans src/app/app.component.html) pour afficher le composant Parent (on peut supprimer tout ce qui se trouve dans ce fichier):
    <app-parent></app-parent>
    
  5. On injecte l’objet ComponentFactoryResolver dans le composant Parent par injection de dépendances et on implémente la callback ngAfterViewInit() pour instancier le composant Child et placer la vue de ce composant dans l’emplacement childComponentPlace:
    import { Component, ViewContainerRef, ViewChild, AfterViewInit, ComponentFactoryResolver } 
      from '@angular/core';  
    import { ChildComponent } from '../child/child.component';  
    
    @Component({  
      selector: 'app-parent',  
      templateUrl: './parent.component.html'  
    })  
    export class ParentComponent implements AfterViewInit {  
      @ViewChild('childComponentPlace', { read: ViewContainerRef }) childComponentRef: ViewContainerRef;  
    
      constructor(private componentFactoryResolver: ComponentFactoryResolver) {}  
    
      ngAfterViewInit() {  
        const childComponentFactory = this.componentFactoryResolver.resolveComponentFactory(ChildComponent);  
        const containerRef = this.childComponentRef;  
        containerRef.clear();  
        containerRef.createComponent(childComponentFactory);  
      }  
    }  
    

    On place le code qui permet l’instanciation du composant Child en utilisant la callback ngAfterViewInit() de façon à ce que le membre childComponentRef soit instancié. En effet si on considère le cycle de vie d’un composant, les requêtes sur la vue avec @ViewChild() sont exécutées juste avant que la callback ngAfterViewInit() soit appelée. Si on place le code avant dans le cycle de vie du composant, childComponentRef sera undefined.

En exécutant ce code on obtient:

Composant Parent
child work!

On peut voir que le composant Child est présent dans la vue du composant Parent.

Pour aller plus loin…

Les fonctionnalités indiquées dans cet article sont détaillées dans d’autres articles:

Leave a Reply