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

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

Pour récupérer l’instance d’un objet se trouvant sur la vue dans la classe d’un composant, il est possible d’effectuer des requêtes auprès de cette vue et renseigner un membre ou une propriété de la classe avec l’instance de cet objet. L’objet requêté peut être un composant enfant, une directive ou un objet du DOM.

@juanster

Pour effectuer un requêtage sur la vue, on peut s’aider de plusieurs décorateurs: @ViewChild(), @ViewChildren(), @ContentChild() ou @ContentChildren().

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.

En Javascript, l’équivalent de ces décorateurs pourrait être:

document.getElementById('id-element'); 

Le choix du décorateur à utiliser dépend du type d’objet à requêter:

  • @ViewChild() et @ViewChildren() permettent de requêter un objet de la vue. Cet objet peut être un objet Angular ou un objet du DOM. @ViewChild() retourne le 1er objet correspondant aux identifiants indiqués en argument; @ViewChildren() renvoie une liste d’objets correspondants.
  • @ContentChild() et @ContentChildren() retournent un ou plusieurs objets se trouvant dans le composant par projection de contenu. A la différence de @ViewChild(), le contenu par projection n’est pas initialisé au même moment que les autres éléments de la vue d’un composant. @ContentChild() retourne le 1er objet correspondant aux identifiants; @ContentChildren() renvoie une liste d’objets correspondants.

Requêter un élément d’une vue

Pour requêter un élément de la vue d’un composant, il faut utiliser @ViewChild() ou @ViewChildren().

@ViewChild()

Le décorateur @ViewChild() permet d’accéder à un élément implémenté dans le template d’un composant:

  • Si l’élément est un composant enfant alors @ViewChild() permettra d’accéder à l’instance de ce composant.
  • Si l’élément est un objet du DOM alors @ViewChild() permettra d’accéder à cet objet par l’intermédiaire d’un objet de type ElementRef.

Pour comprendre l’intérêt de @ViewChild(), dans un premier temps imaginons que l’on souhaite imbriquer un composant dans un autre. Plusieurs syntaxes sont possibles:

Ces 2 méthodes permettent d’effectuer l’imbrication directement à partir des fichiers templates sans effectuer d’implémentation particulière du coté des classes des composants.

Par exemple, si on considère 2 composants ChildComponent et ParentComponent utilisés respectivement pour être le composant enfant et le composant parent. Pour imbriquer le composant ChildComponent dans le composant ParentComponent en utilisant le paramètre selector, l’implémentation pourrait être:

  • Pour le composant enfant:
    Template
    <p>Child component</p>
    Classe du composant
    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-child',
      templateUrl: './child.component'
    })
    export class ChildComponent() {}
  • Pour le composant parent:
    Template
    <p>Parent component</p>
    <app-child></app-child>
    Classe du composant
    import { Component } from '@angular/core';
    
    @Component({
      templateUrl: './parent.component'
    })
    export class ParentComponent() {}

Avec cette implémentation, on peut faire référence au composant enfant à partir du template du composant parent en utilisant une variable référence. Si on souhaite accéder à une propriété du composant enfant pour l’afficher, on peut utiliser l’implémentation suivante:

  • Pour le composant enfant:
    Template
    <p>Child component</p>
    Classe du composant
    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-child',
      templateUrl: './child.component'
    })
    export class ChildComponent() {
      internalValue = 'Value to display';
    }
  • Pour le composant parent:
    Template
    <p>Parent component</p>
    <app-child #child></app-child>
    <p>Child internal value: {{child.internalValue}}</p>
    Classe du composant
    import { Component } from '@angular/core';
    
    @Component({
      templateUrl: '/parent.component'
    })
    export class ParentComponent() {}

Si on souhaite accéder au membre internalValue du composant enfant à partir de la classe du composant parent, il n’y a pas de méthode directe.

Le but du décorateur @ViewChild() est de donner une méthode pour accéder à un composant utilisé dans le template.

Plus généralement @ViewChild() permet d’accéder à un composant, une directive ou un objet du DOM implémenté dans le template à partir de la classe du composant. Ainsi en préfixant une propriété avec le décorateur, la propriété sera automatiquement bindée avec l’objet se trouvant dans le template.

Requêter un composant enfant

En reprenant l’exemple précédent, on ajoute dans la classe du composant parent le membre childReference avec le décorateur @ViewChild():

Template
<p>Parent component</p>
<app-child #child></app-child>
<p>Child internal value: {{child.internalValue}}</p>
Classe du composant
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ChildComponent } from '../child/child.component';

@Component({
  templateUrl: './parent.component'
})
export class ParentComponent implements AfterViewInit {
  @ViewChild(ChildComponent, { static: false }) childReference: ChildComponent;

  ngAfterViewInit() {
    console.log(this.childReference.internalValue);
  }
}

Ainsi la propriété childReference est automatiquement bindée avec l’instance du composant enfant seulement quand l’évènement AfterViewInit ou OnInit est déclenché suivant la valeur de static.

Callbacks ngAfterViewInit() ou ngOnInit()

Quand on utilise le décorateur @ViewChild(), le binding de l’élément n’est pas effectué dès la construction de la classe mais après le déclenchement des callbacks ngAfterViewInit() ou ngOnInit() suivant la valeur du paramètre static (pour plus de détails voir Paramètre static):

  • @ViewChild(<type de l'objet>, { static: false }) le binding sera effectué quand la callback ngAfterViewInit() est déclenchée. La classe du composant doit dériver de AfterViewInit:
    export class ParentComponent implements AfterViewInit {
      ngAfterViewInit(): void {}
    }
    
  • @ViewChild(<type de l'objet>, { static: true }) le binding sera effectué quand la callback ngOnInit() est déclenchée. La classe du composant doit dériver de OnInit:
    export class ParentComponent implements OnInit {
      ngOnInit(): void {}
    }
    

Requêter une directive

La syntaxe est indentique à celle utilisée avec les composants. Par exemple si on considère la directive suivante:

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

@Directive({
  selector: '[contentFiller]'
})
export class SimpleDirective {
  constructor(private elem: ElementRef, private renderer: Renderer2) { 
    let newText = renderer.createText('Content from directive');  
    renderer.appendChild(elem.nativeElement, newText); 
  }
}

Il s’agit d’une directive attribut (i.e. attribute directive) rajoutant le texte 'Create from directive' dans son élément hôte.

Pour appeler la directive à partir du composant hôte, le template du composant est:

<p>Parent component</p>
<p contentFiller #directiveHost></p>

En utilisant @ViewChild() avec la variable référence #directiveHost dans la classe du composant, on obtient l’implémentation suivante:

import { Component, AfterViewInit, ViewChild } from '@angular/core';
import { SimpleDirective } from '../simple.directive';

@Component({
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
export class ParentComponent implements AfterViewInit {
  @ViewChild('directiveHost', { read: SimpleDirective }) innerDirective: SimpleDirective;

  constructor() { }

  ngAfterViewInit(): void {
    console.log(this.innerDirective);
  }
}

On remarque quand dans cet exemple, on utilise l’option { read: SimpleDirective } dans la directive @ViewChild() de façon à effectuer la résolution avec le directive. Si on ne précise pas cette option, la résolution se fera sur l’élément hôte de la directive.
Dans cet exemple, l’élément hôte de la directive est une élément HTML <p></p>.

Requêter un objet du DOM

@ViewChild() permet de binder un objet du DOM avec un membre du composant.

Par exemple, si on considère le code suivant pour la classe parente:

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

@Component({
  templateUrl: './parent.component'
})
export class ParentComponent implements AfterViewInit {
  @ViewChild('spanElement', { static: false }) spanReference: ElementRef;

  ngAfterViewInit() {
    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 ngAfterViewInit() ou ngOnInit() est déclenchée suivant la valeur de static.

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.

@ViewChildren()

Le décorateur @ViewChildren() permet de requêter la vue pour retourner les instances d’objets s’y trouvant. La différence avec ViewChild() est que @ViewChildren() renvoie tous les objets satisfaisants les conditions de la requête (@ViewChild() ne retourne que le 1er objet).

@ViewChildren() peut être utilisé pour retourner un composant ou une directive en précisant le type de l’objet dans le paramètre selector. Il est possible de requêter plusieurs objets en précisant plusieurs noms.

Requêter les objets suivant leur type

Si le paramètre selector est un type alors tous les objets correspondant à ce type seront retournés.

Par exemple, si on considère l’exemple suivant:

  • Le composant ChildComponent:
    Template
    <p>Child Component</p>
    Classe du composant
    @Component({ 
      selector: 'child',
      templateUrl: './child.component' 
    }) 
    export class ChildComponent {}
  • Le composant ParentComponent:
    Template
    <p>Parent Component</p> 
    <child></child> 
    <child></child> 
    <child></child>
    Classe du composant
    import { Component, AfterViewInit, ViewChildren, QueryList } from '@angular/core'; 
    import { ChildComponent } from '../child/child.component';
    
    @Component({ 
      templateUrl: './parent.component.html' 
    }) 
    export class ParentComponent implements AfterViewInit { 
      @ViewChildren(ChildComponent) childReferences: QueryList<ChildComponent>;
    
      ngAfterViewInit(): void { 
        console.log(this.childReferences); 
      } 
    }

Dans ce cas, childReferences contient les 3 références de composant enfant ChildComponent.

Le résultat est:

Parent Component
Child Component
Child Component
Child Component

Requêter les objets suivant leur nom

Il est possible de requêter les objets en précisant les noms de ces objets en utilisant la syntaxe:

@ViewChildren('<nom variable ref 1>, <nom variable ref 2>, ..., <nom variable ref N>') references: QueryList<T>; 

Par exemple:

Template
<p>Parent Component</p> 
<child #child1></child> 
<child #child2></child> 
<child #child3></child>
Classe du composant
import { Component, AfterViewInit, ViewChildren, QueryList } from '@angular/core'; 
import { ChildComponent } from '../child/child.component'; 

@Component({ 
  templateUrl: './parent.component.html' 
}) 
export class ParentComponent implements AfterViewInit { 
  @ViewChildren('child1, child2, child3') childReferences: QueryList<ChildComponent>;
 
  ngAfterViewInit(): void { 
    console.log(this.childReferences); 
  } 
}

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

Requêter un contenu projeté

Pour requêter un contenu projeté dans la vue d’un composant, il faut utiliser @ContentChild() ou @ContentChildren() (voir Les composants enfant pour plus de détails sur la projection de contenu).

@ContentChild()

Le décorateur @ContentChild() permet de requêter la vue d’un composant dans le cadre d’une projection de contenu. Comme le contenu projeté provient de l’extérieur du composant, utiliser @ViewChild() ne permettra pas de requêter le contenu projeté car @ViewChild() effectue la recherche parmi les éléments du composant définis directement dans sa vue.

Si on prend l’exemple suivant:

  • Le composant ParentComponent projette un contenu dans le composant ChildComponent.
  • Le composant ChildComponent affiche le contenu projeté en utilisant <ng-content>.
  • Le code de ChildComponent est:
    Template
    <p>Child Component</p> 
    <ng-content></ng-content>
    Classe du composant
    @Component({ 
      selector: 'child', 
      templateUrl: './child.component.html' 
    }) 
    export class ChildComponent {}
  • Le code de ParentComponent est:
    Template
    <p>Parent Component</p> 
    <child> 
      <p #projectedContent>Projected content</p> 
    </child>
    Classe du composant
    @Component({ 
      templateUrl: './parent.component.html' 
    }) 
    export class ParentComponent {}

L’affichage de cet exemple est:

Parent Component
Child Component
Projected Content

Dans ChildComponent, on pourra requêter le contenu projeté grâce à <ng-content></ng-content> avec le décorateur @ContentChild():

Template
<p>Child Component</p> 
<ng-content></ng-content>
Classe du composant
import { Component, AfterContentInit, ContentChild, ElementRef } 
  from '@angular/core'; 

@Component({ 
  selector: 'child', 
  templateUrl: './child.component.html' 
}) 
export class ChildComponent implements AfterContentInit { 
  @ContentChild('projectedContent') projectedContent: ElementRef; 
 
  ngAfterContentInit(): void { 
    console.log(this.projectedContent); 
  } 
}

Le résultat sera affiché dans la console du browser.

Le requêtage est effectué juste avant ngAfterContentInit()

Quand on utilise @ContentChild(), le membre ou la propriété sera affecté juste avant le déclenchement de la callback ngAfterContentInit().

Requêter les objets suivant leur type

L’exemple précédent permettait d’effectuer une requête en utilisant le nom d’une variable référence toutefois, comme pour @ViewChild(), il est possible d’utiliser un type dans le paramètre selector de @ContentChild().

Par exemple, si on ajoute le composant OtherComponent:

Template
<p>Other Component</p>
Classe du composant
@Component({ 
  selector: 'other', 
  templateUrl: './other.component.html' 
}) 
export class OtherComponent {}

On peut effectuer une requête pour récupérer l’instance du composant OtherComponent se trouvant dans le contenu projeté:

Template
<p>Child Component</p> 
<ng-content></ng-content>
Classe du composant
import { Component, AfterContentInit, ContentChild, ElementRef } from '@angular/core'; 
import { OtherComponent } from '../other/other.component'; 

@Component({ 
  selector: 'child', 
  templateUrl: './child.component.html' 
}) 
export class ChildComponent implements AfterContentInit { 
  @ContentChild(OtherComponent) projectedContent: OtherComponent;

  ngAfterContentInit(): void { 
    console.log(this.projectedContent); 
  } 
}

Le code de ParentComponent est:

Template
<p>Parent Component</p> 
<child> 
  <other></other> 
</child>
Classe du composant
@Component({ 
  templateUrl: './parent.component.html' 
}) 
export class ParentComponent {}
Le requêtage par type s’applique aussi sur des directives

Comme pour @ViewChild(), le requêtage en utilisant un type s’applique plus généralement aux directives et pas seulement sur les composants. Dans l’exemple précédent, on aurait pû utiliser une directive au lieu d’utiliser le composant OtherComponent.

@ContentChildren()

Le décorateur @ContentChildren() permet de requêter le contenu projeté d’un composant pour retourner toutes les instances des objets satisfaisants aux conditions de la requête. La différence avec @ContentChild() est que @ContentChildren() renvoie toutes les instances satisfaisants aux conditions et pas seulement le 1er objet trouvé comme pour @ContentChild().

@ContentChildren() peut être utilisé pour retourner un composant ou une directive en précisant le type de l’objet dans le paramètre selector. Il est possible de requêter plusieurs objets en précisant plusieurs noms.

Requêter les objets suivant leur type

L’exemple suivant permet de montrer comment requêter des objets suivant leur type. Dans cet exemple, plusieurs instances de OtherComponent sont projetés dans le composant ChildComponent. En utilisant le décorateur @ContentChildren(), on peut retourner une liste de toutes les instances de OtherComponent qui ont été projetées:

Template
<p>Other Component</p>
Classe du composant
@Component({ 
  selector: 'other', 
  templateUrl: './other.component.html' 
}) 
export class OtherComponent {}

On peut effectuer une requête pour récupérer l’instance du composant OtherComponent se trouvant dans le contenu projeté:

  • Le code du composant ChildComponent:
    Template
    <p>Child Component</p> 
    <ng-content></ng-content>
    Classe du composant
    import { Component, AfterContentInit, ContentChildren, 
      ElementRef, QueryList } from '@angular/core'; 
    import { OtherComponent } from '../other/other.component'; 
    
    @Component({ 
      selector: 'child', 
      templateUrl: './child.component.html' 
    }) 
    export class ChildComponent implements AfterContentInit { 
      @ContentChildren(OtherComponent) projectedContent: QueryList<OtherComponent>;
    
      ngAfterContentInit(): void { 
        console.log(this.projectedContent); 
      } 
    }
  • Le code de ParentComponent est:
    Template
    <p>Parent Component</p> 
    <child> 
      <!-- Instance 1 --> 
      <other></other>  
      <!-- Instance 2 --> 
      <other></other> 
      <!-- Instance 3 --> 
      <other></other> 
    </child>
    Classe du composant
    @Component({ 
      templateUrl: './parent.component.html' 
    }) 
    export class ParentComponent {}

Le résultat est:

Parent Component
Child Component
Other Component
Other Component
Other Component

Requêter les objets suivant leur nom

Il est possible de requêter les objets en précisant les noms de ces objets en utilisant la syntaxe:

@ContentChildren('<nom variable ref 1>, <nom variable ref 2>, .., <nom variable ref N>') references: QueryList<T>; 

Par exemple:

Template
<p>Other Component</p>
Classe du composant
@Component({ 
  selector: 'other', 
  templateUrl: './other.component.html' 
}) 
export class OtherComponent {}

On peut effectuer une requête pour récupérer l’instance du composant OtherComponent se trouvant dans le contenu projeté:

  • Le code de ChildComponent:
    Template
    <p>Child Component</p> 
    <ng-content></ng-content>
    Classe du composant
    import { Component, AfterContentInit, ContentChildren, 
      ElementRef, QueryList } from '@angular/core'; 
    import { OtherComponent } from '../other/other.component'; 
    
    @Component({ 
      selector: 'child', 
      templateUrl: './child.component.html' 
    }) 
    export class ChildComponent implements AfterContentInit { 
      @ContentChildren('instance1, instance2, instance3') 
        projectedContent: QueryList<OtherComponent>;
    
      ngAfterContentInit(): void { 
        console.log(this.projectedContent); 
      } 
    }
  • Le code de ParentComponent est:
    Template
    <p>Parent Component</p> 
    <child> 
      <other #instance1></other>  
      <other #instance2></other> 
      <other #instance3></other> 
    </child>
    Classe du composant
    @Component({ 
      templateUrl: './parent.component.html' 
    }) 
    export class ParentComponent {}

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

@ContentChild() et @ContentChildren() effectuent une recherche dans le DOM

Les décorateurs @ContentChild() et @ContentChildren() permettent d’effectuer une requête dans le contenu projeté dans le DOM. Cela ne veut pas dire que le contenu doit être affiché dans la vue correspondante ou que <ng-content></ng-content> doit être présent.

Par exemple, si on écrit le code suivant:

  • Pour un composant enfant:
    Template
    <p>Child component</p>
    Classe du composant
    import { Component, ElementRef, ContentChild, AfterContentInit } from '@angular/core'; 
    
    @Component({ 
      selector: 'child', 
      templateUrl: './child.component.html' 
    }) 
    export class ChildComponent implements AfterContentInit { 
      @ContentChild('content') content: ElementRef; 
    
      ngAfterContentInit(): void { 
        console.log(this.content); 
      } 
    }
  • Pour le composant parent:
    Template
    <p>Parent component</p> 
    <child> 
      <p #content>Content</p> 
    </child>
    Classe du composant
    import { Component } from '@angular/core'; 
    
    @Component({ 
      selector: 'parent', 
      templateUrl: './parent.component.html' 
    }) 
    export class ParentComponent {}

On peut voir qu’il n’y a pas <ng-content></ng-content> et que le contenu projeté n’est pas visible:

Parent Component
Child Component

Pourtant le contenu projeté se trouve dans le DOM et @ContentChild() permet de récupérer l’objet correspondant se trouvant dans le DOM.

L’objet du DOM se trouve dans la propriété this.content.nativeElement. On peut voir que la valeur de la propriété this.content.nativeElement.isConnected est false et que this.content.nativeElement.parentNode est undefined expliquant pourquoi cet objet n’est pas visible.

Paramètre descendants dans @ContentChildren()

Le paramètre descendants permet d’indiquer si la requête porte sur les éléments se trouvant directement dans le contenu projeté ou s’il faut descendre parmi les descendants dans la hiérarchie HTML des éléments.

Par exemple si on considère la directive suivante:

import { Directive, ElementRef, ContentChildren, AfterContentInit, QueryList } from '@angular/core'; 

@Directive({ 
  selector: 'custom-directive', 
  templateUrl: './custom.component.html' 
}) 
export class CustomDirective implements AfterContentInit { 
  @ContentChildren('content') content: QueryList<ElementRef> 

  ngAfterContentInit(): void { 
    console.log(this.content); 
  } 
} 

Et le composant suivant:

Template
<p>Parent component</p> 
<custom-directive> 
  <div> 
    <p #content>Content</p> 
  </div> 
</custom-directive>
Classe du composant
import { Component } from '@angular/core'; 

@Component({ 
  selector: 'parent', 
  templateUrl: './parent.component.html' 
}) 
export class ParentComponent {}

Dans ce cas, @ContentChildren() ne permettra pas de récupérer l’élément HTML p qui est projeté car il se trouve dans un élément HTML div:

<div> 
  <p #content>Content</p> 
</div> 

Par défaut, @ContentChildren() ne descend pas dans l’arbre hiérarchique des éléments HTML et la valeur du paramètre descendants est false: { descendants: false }. Pour que @ContentChildren() puisse effectuer la requête en considérant des éléments HTML plus bas dans la hiérarchie, il faut rajouter l’option { descendants: true } dans @ContentChildren():

@Directive({ 
  ... 
}) 
export class CustomDirective implements AfterContentInit { 
  @ContentChildren('content', { descendants: true }) content: QueryList<ElementRef>  

  ngAfterContentInit(): void { 
    console.log(this.content); 
  } 
} 

Avec l’option { descendants: true }, l’élément p sera récupéré par @ContentChildren().

Paramètre read

Ce paramètre s’utilise avec les décorateurs @ViewChild(), @ViewChildren(), @ContentChild() ou @ContentChildren(). Il permet d’ajouter un critère à la requête faite pour retourner l’objet de la vue. Le paramètre read permet d’indiquer un type de l’objet qui sera retourné. Ainsi si plusieurs objets existent avec le même nom de variable référence, l’objet retourné correspondra au type précisé par read.

Pour chaque élément se trouvant dans la vue, il existe un objet Angular correspondant de type ElementRef<any> ou ViewContainerRef. Avec le paramètre read, on peut alors préciser le type ElementRef<any> ou ViewContainerRef et obtenir l’instance de l’objet correspondant.

Si cet élément est un composant, en précisant le type du composant avec read, il est possible de retourner directement l’instance du composant.

L’exemple suivant permet de montrer que des éléments avec les mêmes identifiants dans la vue peuvent être utilisés de façon différente suivant la valeur du paramètre read.

On considère le composant suivant qui servira de composant enfant:

Template
<p>Child component</p>
Classe du composant
import { Component } from '@angular/core'; 

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

On implémente un 2e composant qui servira de parent:

  • Le fichier template est:
    <p>Read example component</p> 
      <p #content>Element content</p> 
    <child-component #child></child-component> 
    
  • La classe du composant est:
    import { Component, ViewChild, ElementRef, ViewContainerRef, AfterViewInit, ViewContainerRef } 
      from '@angular/core'; 
    import { ChildComponent } from '../child/child.component'; 
    
    @Component({ 
      templateUrl: './readexample.component.html' 
    }) 
    export class ReadExampleComponent implements AfterViewInit { 
      @ViewChild('content', { read: ElementRef }) contentElementRef: ElementRef; 
      @ViewChild('content', { read: ViewContainerRef }) contentViewContainerRef: ViewContainerRef; 
    
      @ViewChild('child', { read: ChildComponent }) child: ChildComponent; 
      @ViewChild('child', { read: ElementRef }) childElementRef: ElementRef; 
      @ViewChild('child', { read: ViewContainerRef }) childViewContainerRef: ViewContainerRef;
    
    
      ngAfterViewInit(): void { 
        console.log(this.contentElementRef); 
        console.log(this.contentViewContainerRef); 
    
        console.log(this.child); 
        console.log(this.childElementRef); 
        console.log(this.childViewContainerRef); 
      } 
    } 
    

A l’affichage du composant ReadExampleComponent, on peut voir dans la console du browser les différentes formes des objets affichés:

  • Dans le cas de l’élément p avec la variable référence content:
    • La propriété contentElementRef de type ElementRef qui est un objet Angular wrappant l’objet du DOM correspondant.
    • La propriété contentViewContainerRef de type ViewContainerRef qui est un objet Angular wrappant la vue correspondant à l’élément p.
  • Dans le cas du composant enfant ChildComponent:
    • Les objets équivalents de type ElementRef et ViewContainerRef.
    • child contenant l’instance du composant ChildComponent. La ligne @ViewChild() permettant d’effectuer le binding avec child peut être simplifié en:
      @ViewChild(ChildComponent) child: ChildComponent;
      

Paramètre static

Le paramètre static peut être utilisé avec les décorateurs @ViewChild(), @ViewChildren(), @ContentChild() ou @ContentChildren(). Il permet d’indiquer quand doit être effectué la requête sur la vue. Pour comprendre pleinement le sens de ce paramètre, il faut avoir en tête le cycle de vie d’un composant et le fonctionnement de la détection de changements d’Angular. Le choix de la valeur de ce paramètre n’est pas anodin, un mauvais choix de sa valeur peut entraîner un échec de la requête sur la vue.

Pour résumer, Angular construit la vue d’un composant suivant 2 étapes:

  • Création du contenu statique de la vue: cette étape n’est effectuée qu’à l’initialisation d’un composant. Elle permet de créer les éléments statiques de la vue c’est-à-dire les éléments qui ne seront pas modifiés par la détection de changements. Cette étape n’est exécutée qu’une seule fois de façon à optimiser le traitement, étant donné que les éléments sont statiques, il n’est pas nécessaire de les mettre à jour.
  • Mise à jour du contenu dynamique: cette étape est répétée à chaque exécution de la détection de changements dans le cas où un changement a été détecté. Elle permet de mettre à jour les éléments d’une vue pouvant être affectée après la mise à jour d’une propriété du composant.

Le cycle de vie d’un composant découle directement du mécanisme de détection de changements. 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 (i.e. Lifecycle hooks). L’ordre de déclenchement de ces callbacks est le suivant:

  • A l’initialisation du composant:
    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()).
    2. 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 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()

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.

Ce schéma permet de se rendre compte à quelle stade les requêtes sont exécutées suivant la valeur du paramètre static.

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

Template
<p>Example component:</p> 
<p #numberElement>{{miscNumber}}</p> 
Classe du composant
import { Component, OnInit, AfterContentInit, AfterViewInit, ViewChild, ElementRef }  
  from '@angular/core';  

@Component({  
  selector: 'example',  
  templateUrl: './example.component.html'  
})  
export class ExampleComponent implements OnInit, AfterContentInit, AfterViewInit {  
  miscNumber = 5; 

  @ViewChild('numberElement') numberElementRef: ElementRef<HTMLParagraphElement>;

  ngOnInit(): void {      
    if (this.numberElementRef)  
      console.log('From ngOnInit():', 
        this.numberElementRef.nativeElement.textContent); 
  }  

  ngAfterContentInit(): void {  
    if (this.numberElementRef)  
      console.log('From ngAfterContentInit():', 
        this.numberElementRef.nativeElement.textContent); 
  }  
 
  ngAfterViewInit(): void {      
    if (this.numberElementRef)  
      console.log('From ngAfterViewInit():', 
        this.numberElementRef.nativeElement.textContent); 
  }  
} 

Dans le template, on affiche la propriété miscNumber du composant. Dans le classe du composant, on effectue une requête sur la vue en utilisant la variable référence #numberElement. On implémente les callbacks ngOnInit(), ngAfterContentInit() et ngAfterViewInit() pour afficher le contenu de l’élément p identifié par la variable #numberElement.

Après exécution, seule la ligne suivante apparaît dans la console du browser:

From ngAfterViewInit(): 5 

Par défaut, si le paramètre static n’est pas renseigné dans la requête @ViewChild(), sa valeur est false. Cela signifie que la requête sera exécutée peu avant le déclenchement de la callback ngAgterViewInit(). Ainsi, le résutat de l’exécution s’explique par le fait que pour les autres callbacks, la requêtes @ViewChild() n’a pas été exécutée et la propriété numberElementRef est encore indéfinie. Le contenu de l’élément #numberElement ne peut donc pas être affiché.

Si on modifie la valeur du paramètre static telle que:

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

Le résultat de l’exécution devient:

From ngOnInit():  
From ngAfterContentInit():  
From ngAfterViewInit(): 5 

Ainsi, avec { static: true }, la requête @ViewChild() est exécutée juste avant le déclenchement de la callback ngOnInit() sur le contenu statique de la vue. Ce contenu contient l’élément p toutefois le résultat de l’interpolation {{miscNumber}} ne fait pas partie du contenu statique de la vue. La priopriété this.numberElementRef est définie mais this.numberElementRef.nativeElement.textContent est vide car le DOM n’a pas été mis à jour avec le contenu dynamique de la vue.

Une fois que le DOM a été mis à jour avec le contenu dynamique de la vue juste avant le déclenchement de la callback ngAfterViewInit(), this.numberElementRef.nativeElement.textContent contient le résultat de l’interpolation. Ainsi ngAfterViewInit() permet d’afficher le résultat de l’interpolation.

Cet exemple permet de montrer que suivant la valeur du paramètre static, l’exécution des requêtes sur les vues n’est pas effectuée au même stade:

  • Si static est true alors la requête est effectuée sur le contenu statique. Le résultat de la requête est disponible en amont du cycle de vie du composant.
  • Si static est false alors la requête est effectuée sur le contenu dynamique (elle peut aussi être exécutée sur le contenu statique). Le résultat de la requête est disponible plus tard dans le cycle de vue du composant.

Paramètre queries de @Directive()

Le paramètre queries est utilisable dans les décorateurs @Directive() et @Component() (puisque @Component() hérite de @Directive()). Ce paramètre permet d’effectuer les mêmes traitements qu’avec les décorateurs @ViewChild(), @ViewChildren(), @ContentChild() et @ContentChildren(), toutefois la syntaxe rend la lecture moins facile.

Au lieu d’utiliser les décorateurs devant les membres ou propriétés, on instancie les objets qui vont renseigner les valeurs des membres ou des propriétés. Les objets instanciés sont équivalents aux décorateurs: @ViewChild(), @ViewChildren(), @ContentChild() ou @ContentChildren().

Par exemple, si considère 2 composants ChildComponent et ParentComponent tels que:

  • ChildComponent:
    Template
    <p>Child component</p>
    Classe du composant
    import { Component, ElementRef, ContentChild, AfterContentInit } from '@angular/core'; 
    
    @Component({ 
      selector: 'child', 
      templateUrl: './child.component.html', 
      queries: { 
        content: new ContentChild('content') 
      } 
    }) 
    export class ChildComponent implements AfterContentInit { 
      content: ElementRef; 
    
      ngAfterContentInit(): void { 
        console.log(this.content); 
      } 
    }
  • Pour le code du composant parent:
    Template
    <p>Parent component</p> 
    <child #child> 
      <p #content>Content</p> 
    </child>
    Classe du composant
    import { Component, ViewChild, AfterViewInit } from '@angular/core'; 
    import { ChildComponent } from '../child/child.component'; 
    
    @Component({ 
      selector: 'parent', 
      templateUrl: './parent.component.html' 
      queries: { 
        child: new ViewChild(ChildComponent) 
      }
    }) 
    export class ParentComponent implements AfterViewInit { 
      child: ChildComponent; 
    
      ngAfterViewInit(): void { 
        console.log(this.child); 
      } 
    }

Dans cet exemple, on utlise le paramètre queries dans des décorateurs @Component() pour instancier:

  • ContentChild() dans ChildComponent pour renseigner le membre content de type ElementRef contenant un objet Angular correspondant à l’élément HTML <p #content></p> projeté à partir de ParentComponent.
  • @ViewChild() dans ParentComponent pour renseigner le membre child de type ChildComponent contenant l’instance du composant enfant se trouvant dans le template de ParentComponent.

Pour résumer…

Requêter la vue d’un composant permet de récupérer l’instance d’un objet de façon à l’exploiter dans la classe du composant. Les objets requêtés peuvent être un composant enfant, une directive ou objet quelconque du DOM.

Le résultat des requêtes permet d’affecter des propriétés dans la classe du composant. Pour indiquer qu’une propriété doit être initialisée avec le résultat d’une requête sur la vue, il faut utiliser des décorateurs particuliers:

  • @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.

Les critères de la requête 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 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().

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>;

Option descendants

L’option { descendants: true } utilisable avec @ContentChildren() permet d’indiquer si la requête doit porter sur tous les descendants d’un élément dans la hiérarchie HTML. Par défaut, la requête est effectuée seulement sur les enfants directs.

Références

Leave a Reply