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

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
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Le scope des variables en Javascript

Le scope (ou la portée) d’un objet est la portion de code dans laquelle une variable peut exister et maintenir une valeur qui lui aura été préalablement affectée. Le scope des objets est loin d’être un sujet trivial en Javascript car suivant la façon dont on les déclare beaucoup de règles ou de comportements du langage ne sont pas forcément intuitifs et surtout sont opposés à ceux d’autres langages plus standards comme le C++, C#, Java etc…

@martinols3n

Le but de cet article est d’essayer d’indiquer tous les cas atypiques de comportements de la gestion des scopes des objets en Javascript pouvant mener à des erreurs d’implémentations.
Dans un premier temps, on va indiquer les règles générales liées au scope suivant l’utilisation de var, let ou const. Ensuite, on montrera quelle syntaxe permet de modifier les règles liées à la gestion du scope par le moteur Javascript. Dans un 3e temps, on développera les différents comportements du mot-clé this. Enfin, on indiquera quelles sont les règles liées à l’hoisting.

Pour déclarer des variables en Javascript de façon à éviter de déclarer dans le scope global, on utilise les mots-clé var ou let (ou const dans le cas d’une variable dont la valeur est constante). Suivant le mot-clé utilisé, le comportement du moteur Javascript sera très différent. Avec let et const, les comportements sont assez standards pour rapport à d’autres langages. En revanche, avec var, les comportements sont spécifiques et peuvent surprendre en particulier si on est habitué à d’autres langages.
On pourrait se demander quelle est la justification qui nécessite de s’intéresser à var, il suffirait d’utiliser uniquement let. La raison est que let est apparu avec ES6 (ES2015) et que beaucoup d’applications fonctionnent avec un code compatible ES5 utilisant var.

Variables globales

Un des plus gros inconvénient de Javascript est de permettre de déclarer des variables globales. Le scope de ces variables étant celui de l’application, elles garderont leur valeur dans toute l’application. Ainsi une même variable peut être valuée à des endroits différents du code et des conflits peuvent apparaître entre ces valuations dans le cas où elles sont incompatibles ce qui peut mener à des erreurs. D’une façon générale, il faut éviter d’utiliser des variables globales.

Les variables globales se déclarent de cette façon:

<nom variable> = <valeur>;

ou

window.<nom variable> = <valeur>;

Par exemple:

variable = 'OK';

ou

window.variable = 'OK';

Lexical scope vs dynamic scope

Quand Kyle Simpson présente le scope des objets en Javascript dans You don’t know JS, il explique le comportement de Javascript en passant par le lexical scope.
Le lexical scope ou static scope s’oppose au dynamic scope. Il s’agit d’un ensemble de règles utilisé par le moteur d’exécution d’un langage pour identifier une variable nommée et pour en déterminer la valeur. Le lexical scope est le comportement utilisé par la plupart des langages y compris C++, C# ou Java. Javascript utilise aussi cet ensemble de règles.

Dans la gestion du scope des variables, tous ces langages ont des comportements similaires:

  • Les scopes s’imbriquent: un scope peut se trouver à l’intérieur d’un autre scope. Les variables définies dans le scope parent sont accessibles dans le scope enfant.
  • Le scope le plus général est le scope global: dans le cas de Javascript, une variable globale est accessible partout.
  • La plupart du temps, un scope ne peut pas appartenir à plus d’un seul scope parent.

En Javascript, les règles qu’imposent le lexical scope sont les suivantes:

  • Règle 1: lorsque le moteur d’exécution exécute la déclaration d’une variable, le scope de la variable sera celui dans lequel se trouve la déclaration.
  • Règle 2: lorsque le moteur d’exécution cherche la valeur d’une variable, il recherche sa déclaration dans le scope courant. Si aucune déclaration n’est trouvée, le moteur cherche dans les scopes parent. Si aucune déclaration n’est trouvée alors la variable appartient au scope global.
  • Règle 3: si une variable est déclarée dans un scope parent et qu’une variable est déclarée avec le même nom dans un scope enfant, la variable dans le scope parent sera occultée par celle du scope enfant. Ce comportement correspond au shadowing.

Pour se rendre compte de la différence entre le lexical scope et le dynamic scope, on peut prendre l’exemple suivant:

var x = 1;

function a() {
  console.log('x in a(): ',x); // x => 1
  x = 2;
}

function b() {
  var x = 3; // x => 3
  a();
  console.log('x in b(): ',x); // x => 3
}

b();
console.log(x); // x => 2

Suivant le scope utilisé, le comportement est le suivant:

Lexical scope

Dynamic scope

Le dynamic scope utilise des piles durant l’exécution pour garder les valeurs des variables de façon dynamique.
Javascript a le comportement correspondant au lexical scope, il n’utilise donc pas le dynamic scope..
  1. Dans le corps principal, var x = 1 permet d’affecter la valeur 1 à la variable x (qu’on peut appeler x1).
  2. La méthode b() est appelée:
    1. Dans le corps de la méthode b(), var x = 3 permet d’affecter 3 à une nouvelle variable nommée x (qu’on peut appeler x2). La première variable x (i.e. x1) déclarée dans le corps principal est occultée par shadowing.
    2. On appelle la méthode a(), dans le corps de la méthode b():
      1. On affiche la valeur de x, pour savoir à quoi correspond cette variable:
        • Dans le scope de la méthode a(), y’a-t-il une déclaration d’une variable x dans le scope correspondant au corps de la méthode a() ? ⇒ non donc on cherche dans le scope parent de la méthode.
        • Dans le corps principal, y’a-t-il une déclaration d’une variable nommée x ? ⇒ oui avec var x = 1 (il s’agit de x1).
        • On peut en déduire la valeur de x qui est 1.
      2. On affecte une nouvelle valeur à la variable x. Par le même principe que précédemment, la variable x (i.e. x1) correspond à la variable du corps principal. La nouvelle valeur de cette variable est donc 2.
    3. Dans le corps de b(), on affiche la valeur de la variable x. Pour savoir à quoi correspond cette variable: y’a-t-il une déclaration d’une variable x dans le scope correspondant au corps de b() ? ⇒ oui à cause de var x = 3. La valeur de cette variable est donc 3 (il s’agit de x2).
  3. Dans le corps principal, on affiche la valeur de la variable x. Pour savoir à quoi correspond cette variable x: y’a-t-il une déclaration d’une variable x dans le scope du corps principal ? ⇒ oui à cause de var x = 1 (il s’agit de x1). Toutefois la valeur de cette variable a été modifiée lors de l’appel à la méthode a(). La valeur de x est 2.
  1. Dans le corps principal, var x = 1 permet d’ajouter à la pile la valeur 1 pour la variable x (qu’on peut appeler x1).
  2. La méthode b() est appelée:
    1. Dans le corps de la méthode b(), var x = 3 ajoute à la pile la valeur 3 pour une nouvelle variable x (qu’on peut appeler x2).
    2. On appelle la méthode a(), dans le corps de la méthode b():
      • On affiche la valeur de x. En regardant au sommet de la pile, on cherche une variable nommée x (il s’agit de x2) donc la valeur affichée est 3.
      • On affecte une nouvelle valeur à la variable x ⇒ en cherchant au sommet de la pile on trouve une variable nommée x (i.e. x2) donc la valeur est 3. On modifie cette valeur pour affecter 2.
    3. Dans le corps de b(), on affiche la valeur de la variable x. Pour savoir à quoi correspond cette variable, on cherche au sommet de la pile une variable nommée x ⇒ la variable trouvée est x2, la valeur affichée est donc 2. Quand on sort de la méthode b(), la variable nommée x au sommet de la pile est supprimée (x2 est donc supprimée de la pile).
  3. Dans le corps principal, on affiche la valeur d’une variable nommée x. Pour savoir à quoi correspond cette variable x: on regarde au sommet de la pile, la variable nommée x correspond à x1. La valeur de cette variable n’a jamais été modifiée, sa valeur est 1.

Les 2 comportements sont donc différents et impliquent des valeurs différentes suivants les scopes dans lesquels les affectations et les récupérations de valeurs sont faites.

var, let et const

Les mots-clé var, let et const permettent de déclarer une variable en limitant son scope: suivant le mot-clé utilisé, le scope de la variable sera très différent:

  • var permet de déclarer une variable dans le scope global ou dans le scope d’une fonction.
  • const et let permettent de déclarer une variable dans le scope d’un bloc de code.

Déclarer une variable avec ces mots-clé est possible avec la syntaxe suivante:

var <nom variable>;
let <nom variable>;

Avec const, une initialisation est obligatoire.

Pour initialiser une variable en même temps que sa déclaration, la syntaxe est:

var <nom variable> = <valeur d'initialisation>
let <nom variable> = <valeur d'initialisation>
const <nom variable> = <valeur d'initialisation>

Scope avec let et const

La gestion du scope est similaire entre let et const. La différence entre ces mots-clé est que:

  • let autorise des affectations d’une nouvelle valeur à une variable.
  • const interdit de nouvelles affectations.

Si on exécute:

const a = 3;
a = 5; // SyntaxError

On obtient une erreur de type 'SyntaxError: redeclaration of const a'.

let et const sont apparus à partir de ES6 et ont pour but d’avoir un comportement similaire à la plupart des autres langages: ils limitent le scope d’une variable à un bloc de code. Un bloc de code étant la portion de code délimitée par { ... }, par exemple:

  • Une fonction:
    function add(a, b) {
      // ...
    }
    
  • Une clause if...else, for etc:
    for (let i = 0; i < 10; i++) {
      // ...
    }
    
  • Le contenu des blocs dans un try...catch:
    try {
      // ...
    }
    catch (err) {
      // err est limité au bloc catch
    }
    
  • Une bloc simple:
    {
      // ...
    }
    
  • Une bloc nommé:
    namedBlock: {
      // ...
    }
    
  • Un objet:
    var obj = {
      // ...
    };
    

Ainsi avec let ou const, le scope d’une variable est limité au bloc de code dans lequel elle est définie et dans les blocs enfant.

Le comportement est similaire à la plupart des autres langages, une variable déclarée dans un bloc de code ne sera pas accessible en dehors de ce bloc et de ses blocs enfant (sauf en cas de shadowing).

Par exemple:

const a = 'OK';
if (a === 'OK') {
  let b = 5;
  console.log('Inside if: a=',a,' ;b=',b); // 'Inside if: a=OK ;b=5'
}

console.log('Outside if: a=',a); // le résultat est 'Outside if: a=OK'
console.log('Outside if: b=',b): // Reference Error

Dans le code plus haut, on peut voir 2 blocs de code: celui en dehors de la clause if et celui en dehors. Ainsi:

  • La variable a a été définie dans le bloc en dehors de la clause if, elle est donc disponible dans les 2 blocs.
  • La variable b a été définie dans le bloc du if, elle n’est accessible que dans ce bloc.

Scope avec var

Contrairement à let et const, le scope des objets avec var correspond aux fonctions. Les blocs de code ne limitent pas les scopes quand on déclare une variable avec var.

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

var a = 'OK';
if (a === 'OK') {
  var b = 5;
  console.log('Inside if: a=',a,' ;b=',b);
}

console.log('Outside if: a=',a,' ;b=',b);

Le scope est le même pour les variables a et b même si b est déclaré dans le corps de la clause if. A l’intérieur ou à l’extérieur de la clause if, le résultat est le même:

Inside if: a=OK ;b=5
Outside if: a=OK ;b=5
Fonction, fonction anonyme, arrow function et IIFE

La notion de scope n’est pas modifiée suivant le type de fonction. Dans le cas de var, le scope des variables correspond aux fonctions.

Si on considère le code suivant:

var x = 3;
function namedFunction() {
  var x = 2;
  console.log(x); // 2
}

namedFunction();

console.log(x); // 3

La règle du lexical scope s’applique dans le cadre de cet exemple.

  • Fonction anonyme (i.e. function expression): le comportement est le même pour une fonction anonyme:
    var x = 3;
    var anonymousFunction = function() {
      var x = 2;
      console.log(x); // 2
    }
    
    anonymousFunction();
    
    console.log(x); // 3
    
  • IIFE (i.e. Immediatly Invoked Function Expression) qui sont exécutées au même moment que leur déclaration:
    var x = 3;
    (function() {
      var x = 2;
      console.log(x); // 2
    })();
    
    console.log(x); // 3
    
  • Arrow function: à partir de ES6:
    var x = 3;
    var arrowFunction = () => {
      var x = 2;
      console.log(x); // 2
    })();
    
    arrowFunction();
    console.log(x); // 3
    

Comment modifier le comportement du lexical scope ?

Il est possible de modifier le comportement du lexical scope en utilisant eval() ou le constructeur Function().

eval()

eval() permet d’évaluer une expression sous forme de chaîne de caractères.

Par exemple:

let result = eval('1 + 3');
console.log(result); // 4

Ainsi eval() exécute l’expression sous forme de chaîne de caractères: '1 + 3'.

Le scope d’une variable utilisée dans une expression évaluée par eval() dépend du scope dans lequel l’expression est évaluée et non en fonction du scope dans lequel la chaîne évaluée est construite.

Par exemple, si on exécute:

function a(expr) {
  eval(expr);
  console.log('x=',x); // 4
}

var x = 3;
let expr = 'var x = 4';
a(expr);

console.log('x=',x); // 3

Le scope utilisé pour évaluer x avec eval() est celui de la fonction a(). Même si l’expression expr a été construite à l’extérieure de a(), c’est le scope dans lequel eval() est exécuté qui sera utilisé.
Dans cet exemple, quand expr est évaluée dans le corps de la méthode a(), var x = 4 occulte la première déclaration de x pour définir une 2e variable nommée x dont la valeur est 4. À l’extérieur de a(), la variable x correspond à la variable déclarée avec var x = 3.

Comportement de eval() avec le mode strict

Si on utilise le mode strict, l’expression évaluée dans eval() possède un scope spécifique qui ne déborde pas du scope dans lequel eval() est exécuté.

Par exemple si on exécute le code suivant:

function a(expr) {
  "use strict"; 
  eval(expr); // Inside eval: 4
  console.log('x=',x); // 3
}

var x = 3;
let expr = "var x = 4; console.log('Inside eval: ',x)";
a(expr);

console.log('x=',x); // 3

Toutes les évaluation de x en dehors de eval() possède la valeur 3 car la variable utilisée est celle déclarée par var x = 3. Toutefois avec le mode strict, le scope de l’expression évaluée par eval() est limité, ainsi x possède la valeur 4 seulement dans l’expression évaluée.

Comportement de eval() avec const et let

Si on utilise const ou let pour déclarer une variable dans une expression évaluée par eval(), le scope est spécifique à l’expression évaluée, il ne déborde pas dans le scope dans lequel eval() est exécuté.

Par exemple, si on exécute le code suivant:

function a(expr) {
  eval(expr); // Inside eval: 4
  console.log('x=',x); // 3
}

let x = 3;
let expr = "let x = 4; console.log('Inside eval: ',x)";
a(expr);

console.log('x=',x); // 3

Toutes les évaluation de x en dehors de eval() possède la valeur 3 car la variable utilisée est celle déclarée par var x = 3. Le scope de l’expression évaluée par eval() est limité, x possède la valeur 4 seulement dans l’expression évaluée.

new Function()

La constructeur Function() permet de déclarer un objet de type Function. Utiliser ce constructeur permet d’évaluer une expression de la même façon qu’avec eval(). La syntaxe à utiliser est:

var objectFunction = new Function([arg1, arg2, ..., argN], expression);

Dans cet appel, expression contient la chaîne de caractères à évaluer.

On peut exécuter la fonction de cette façon:

objectFunction(arg1, arg1, ..., argN, '....');

Du point de vue de scope, cet objet ne se comporte ni comme une fonction normale, ni comme eval(): le scope parent d’un objet de type Function est le scope global et non le scope de la fonction parente.

Par exemple dans le cadre général, si une fonction est définie dans une autre fonction dite parente, le scope parent de la fonction est le scope de la fonction parente. C’est la règle du lexical scope.

Ainsi, si on exécute le code suivant:

function parent() {
  function a() {
    x = 2;
    console.log('x=',x); // x=2
  }

  var x = 3;
  a();

  console.log('x=',x,'window.x=',window.x); // x=2 window.x=undefined
}

parent();

La règle du lexical scope s’applique pour déterminer qu’elle est la déclaration d’une variable:

  • Dans le corps de la fonction a(): x désigne la variable déclarée avec var x = 3 donc partout x désigne la même variable.
  • Dans le corps de parent(): window.x est indéfinie puisqu’on a jamais affecté de valeur à la variable globale x.

Si on considère un code semblable utilisant un objet de type Function:

function parent() {
  var a = new Function("x = 2; console.log('x=',x);");
  var x = 3;

  a(); // x= 2

  console.log('x=',x,'window.x=',window.x); // x=3 window.x=2
}

parent();

Le scope parent de l’objet Function n’est pas la fonction parent() mais le scope global, ainsi:

  • Quand l’expression est évaluée x = 2 fera référence à x dans le scope parent c’est-à-dire le scope global. C’est la raison pour laquelle window.x renvoie la valeur 2.
  • L’expression évaluée dans le constructeur de Function() modifie x en tant que variable globale donc la variable déclarée par var x = 3 n’est pas modifiée et console.log('x=',x) dans le corps de parent() renvoie 3.

De même, le scope parent de eval() correpond, par défaut, au scope dans lequel eval() est exécuté. Ce qui est contraire au comportement de l’objet Function puisque son scope parent est le scope global.

this

this est un mot-clé très courant dans beaucoup de langage pour désigner l’instance courante d’une classe.

En Javascript, c’est aussi le sens de ce mot-clé dans le cadre des classes introduites à partir de ES6.

Par exemple:

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  logPerson() {
    console.log(this.firstName, this.lastName);
  }
}

var person = new Person('Uncle', 'Bob');
person.logPerson(); // Uncle Bob

Dans cet exemple, this renvoie bien à l’instance courante de la classe Person.

En Javascript, le plus souvent this est utilisé dans un cadre différent des classes. Au lieu de représenter l’instance d’une classe, il désigne un contexte dans lequel le code est exécuté. Suivant les cas d’usage de this, sa valeur peut être plus difficile à prévoir que dans le cas d’une classe.

Parmi les cas les plus courants d’utilisation de this, il y a celui du corps d’une fonction (cette fonction n’appartient pas à une classe ou à un objet). Dans le cas d’une fonction:

  • En mode non strict: this désigne le contexte global.
  • En mode strict: this est indéfini sauf si on indique explicitement sa valeur.

Par exemple:

function a() {
  return this;
}

console.log(a() === window); // true

Dans ce cas, this désigne le contexte global.

Dans la cas du mode strict, this est indéfini car sa valeur n’a pas été indiquée explicitement:

function a() {
  "use strict";
  return this;
}

console.log(a()); // undefined

Dans le cas d’un objet, this dans une fonction désigne implicitement cet objet:

var obj = {
  x: 5,
  a: function() {
    return this;
  }
};

console.log(obj.a().x === 5); // true

Dans ce cas, this désigne l’objet obj.

Arrow function

D’une façon générale, les arrow functions ont les mêmes comportements que les fonctions normales c’est-à-dire que this désigne le contexte global ou l’objet englobant.

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

var arrowFunction = () => this; // Référence d'une arrow function

console.log(arrowFunction() === window); // true

Dans le cas de l’exemple précédent, on exécute dans le contexte global donc this dans l’arrow function désigne le contexte global.

Dans le cas d’une arrow function déclarée dans un objet, il faut distinguer l’appel à une fonction et le cas d’une référence vers une fonction.

Par exemple, si on place cette fonction dans un objet:

var obj = {
  x: 5,
  a: function() {
    var arrowFunction = () => this;
    return arrowFunction; // On retourne la référence de la fonction
  }
};

var fn = obj.a(); // On récupère la référence de l'arrow function dans le contexte de l'objet obj.
console.log(fn().x === 5); // True
console.log(obj.a().x === 5); // False
console.log(obj.a()().x === 5); // True

Ainsi:

  • console.log(fn().x === 5) retourne true car fn est une référence vers l’arrow function dans le contexte de l’objet obj. Quand on exécuté fn(), on exécute l’arrow function et this désigne l’objet obj. Par suite obj.x contient la valeur 5.
  • console.log(obj.a().x === 5) retourne false car obj.a() contient la référence de l’arrow function, il faut exécuter obj.a()() pour exécuter réellement l’arrow function.

Comment affecter une valeur à this ?

On peut affecter une valeur à this avec les fonctions call(), apply() ou bind().

call() et apply()

call() et apply() permettent de contrôler la valeur de this en indiquant quel est le contexte d’exécution d’une fonction.

La syntaxe de ces méthodes est:

  • call():
    <fonction>.call(thisVar, [arg1, arg2,..., argN]);
    

    Avec <fonction> la fonction à exécuter; thisVar, la valeur de this et [arg1, arg2,..., ArgN], les arguments de la fonction à exécuter.
    En mode non strict, si thisVar est nul, alors this contient le contexte global.

  • apply():
    <fonction>.apply(thisVar, [tableau d'arguments]);
    

    Avec <fonction> la fonction à exécuter; thisVar, la valeur de this et [tableau d'arguments], les arguments de la fonction à exécuter sous forme d’un tableau.
    En mode non strict, si thisVar est nul, alors this contient le contexte global.

Si on considère l’exemple suivant:

function a(z, w) {
  return this.x + this.y + z +w;
}

var obj = {
  x: 5,
  y: 6
};

x = 1;
y = 2;

console.log(a.call(null, 3, 4)); // 10
console.log(a.apply(null, [3, 4])); // 10

Les 2 appels précédents permettent d’exécuter a() dans le contexte global puisque la valeur indiquée de this est nulle. Dans le contexte global, x et y contiennent respectivement les valeurs 1 et 2.

Si on exécute:

console.log(a.call(obj, 3, 4)); // 18
console.log(a.apply(obj, [3, 4])); // 18

this contient obj donc a() est exécuté dans le contexte de obj. x et y contiennent respectivement les valeurs 5 et 6.

Si on modifie le code pour activer le mode strict:

function a(z, w) {
  "use strict";
  return this.x + this.y + z +w;
}

var obj = {
  x: 5,
  y: 6
};

x = 1;
y = 2;

console.log(a.call(null, 3, 4)); // TypeError: this is null
console.log(a.apply(null, [3, 4])); // TypeError: this is null

Si le mode strict est activé et que this contient null, sa valeur n’est pas remplacée par le contexte global. a() ne peut pas effectuer l’addition car this est nul.

bind()

A partir de ES5, bind() permet aussi de préciser une valeur de this en générant une référence vers une nouvelle fonction. Cette nouvelle fonction sera liée au contexte indiqué lors de l’exécution de bind().
La syntaxe de bind() est:

let <référence fonction> = <fonction>.bind(thisVar, [arg1, arg2,..., argN]);

Avec <fonction> la fonction à exécuter; thisVar, la valeur de this et [arg1, arg2,..., ArgN], les arguments de la fonction à exécuter.

Avec bind(), le cas d’usage est un peu différent de call() et apply(). Si on reprend l’exemple précédent:

function a(z, w) {
  return this.x + this.y + z +w;
}

var obj = {
  x: 5,
  y: 6
};

x = 1;
y = 2;

var boundA = a.bind(null, 3, 4); // On définit une nouvelle référence sans indiquer de valeur pour this
console.log(boundA()); // 10

Si on ne précise pas de valeur pour this, c’est le contexte global qui est utilisé. Dans cet exemple, dans le corps de a(), this fait référence au contexte global, x et y ont pour valeur, respectivement, 1 et 2.

Si on indique une autre valeur pour this:

var boundA = a.bind(obj, 3, 4);
console.log(boundA()); // 18

Dans cet appel, this fait référence à l’objet obj donc x et y ont pour valeur, respectivement 5 et 6.

Enfin dans le cas du mode strict, si on ne précise pas de valeur pour this, sa valeur restera nulle. Si on reprend l’exemple précédent:

function a(z, w) {
  "use strict";
  return this.x + this.y + z +w;
}

var obj = {
  x: 5,
  y: 6
};

x = 1;
y = 2;

var boundA = a.bind(null, 3, 4);
console.log(boundA()); // TypeError: this is null

Avec le mode strict, this est nul dans le corps de la fonction a() d’où l’erreur.

setTimeout()

Avec setTimeout(), la valeur par défaut de this est le contexte global. Ce comportement peut prêter à confusion quand on utilise this dans une fonction exécutée par setTimeout().

Par exemple:

var obj = {
  x: 5,
  a: function() {
    console.log(this.x);
  }
};

var x = 3;

obj.a(); // 5

Dans ce dernier appel, this fait référence à l’objet obj puisque a() se trouve dans cet objet.

En revanche si on exécute le code suivant, this fait référence au contexte global:

setTimeout(obj.a, 100); //3

C’est la 3 qui s’affiche car c’est la variable x déclarée dans le contexte global qui est utilisée.

La solution peut consister à utiliser une arrow function (disponible à partir de ES6):

setTimeout(() => obj.a(), 100); // 5

Cet appel permet de forcer l’appel dans le contexte de l’objet obj.

On peut aussi utiliser bind() (à partir de ES5) pour indiquer explicitement la valeur de this:

var boundA = obj.a.bind(obj);
setTimeout(boundA, 100); // 5

Enfin, on peut utiliser une variable locale et encapsuler l’appel à setTimeout dans la fonction déclarée dans l’objet obj:

var obj = {
  x: 5,
  a: function() {
    var self = this;
    setTimeout(function() {
      console.log(self.x);
    }, 100);
  }
};

var x = 3;

obj.a(); // 5

Closure

Dans le cadre des closures (i.e. fermetures), le comportement de this peut prêter à confusion car Javascript perd le scope de this quand il est utilisé dans une fonction qui se trouve dans une autre fonction.

Qu’est ce qu’une closure ?

Avant de rentrer dans les détails de this dans le cadre d’une closure, on peut rappeler la définition d’une closure (les closures ne sont pas spécifiques à Javascript):

D’après MDN web docs:

“Une fermeture correspond à la combinaison entre une fonction et l’environnement lexical au sein duquel elle a été déclarée. En d’autres termes, une fermeture donne accès à la portée d’une fonction externe à partir d’une fonction interne. En Javascript, une fermeture est créée chaque fois qu’une fonction est créée.

L’exemple le plus courant d’une closure est celui intervenant dans le cadre d’une boucle:

for (var i = 1; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 100);
}

A l’exécution, on s’attendrait à voir s’afficher 1, 2, 3, etc… toutefois on voit 5 fois s’afficher 6. Ce comportement s’explique par le fait que la fonction anonyme se trouvant dans setTimeout() utilise le scope de la fonction externe, or dans le scope externe i varie de 1 à 6. Étant donné que setTimeout() retarde l’exécution de la fonction anonyme pendant 100 ms, les valeurs affichées correspondent à la dernier valeur de i.

Pour empêcher ce comportement, il faudrait que la fonction anonyme utilise une variable dont le scope ne dépend pas de la fonction externe. Par exemple, si on utilise une IIFE exécutée à chaque boucle et si on utilise une variable locale à l’IIFE, on pourra utiliser une variable dont la valeur est spécifique à chaque boucle:

for (var i = 1; i < 5; i++) {
  // IIFE exécutée à chaque boucle
  (function() {
    var j = i; // Variable locale
    setTimeout(function() {
      console.log(j); // On utilise la variable locale
    }, 100);
  })();
}

A l’exécution, on affiche les différentes valeurs: 1, 2, 3, etc…

Conséquences avec this

Comme on l’a indiqué plus haut:

En Javascript, une fermeture est créée chaque fois qu’une fonction est créée“.

Ainsi dans le cadre de l’exemple suivant, la closure (i.e. fermeture) entraîne le comportement du lexical scope: pour connaître la valeur d’une variable, le moteur vérifie le scope courant puis les scopes parent jusqu’à ce qu’il trouve la déclaration.

Par exemple:

function a() {
  var x = 5;
  var nestedA = function() {
    console.log(x); // 5
  }
  
  nestedA();
}

a();

La valeur affichée sera 5 dans le corps de nestedA() car cette fonction a accès au scope de la fonction externe c’est-à-dire celui de a(). x étant déclarée dans le corps de a() alors la valeur affichée sera 5.

On pourrait s’attendre à ce que le comportement de this soit identique, par exemple:

var obj = {
  x: 5,
  a: function() {
    console.log(this.x); // 5

    var nestedA = function() {
      console.log(this.x); // undefined
    };
    nestedA();
  }
};

obj.a();

On pourrait s’attendre à ce que this.x dans nestedA() contienne la valeur 5. Ce n’est pas le cas car le scope de this est perdu quand une fonction se trouve dans une autre fonction. Dans nestedA(), this ne correspond pas à l’objet obj (comme c’est la cas dans le corps de a()) mais au scope global. Ainsi, this.x dans nestedA() est indéfinie.

Le comportement est le même si on utilise une IIFE:

var obj = {
  x: 5,
  a: function() {
    console.log(this.x); // 5

    (function() {
      console.log(this.x); // undefined
    })();
  }
};

obj.a();

Par contre avec les arrow functions implémentées à partir de ES6, le comportement est plus standard:

var obj = {
  x: 5,
  a: function() {
    console.log(this.x); // 5

    var nestedA = () => {
      console.log(this.x); // 5
    };
    nestedA();
  }
};

obj.a();

Dans le cadre des arrow functions, this correspond bien à l’objet obj.

Hoisting avec var

L’utilisation de var pour déclarer des variables entraîne un comportement qui peut prêter à confusion: l’hoisting (i.e. hisser). Le principe de ce comportement est le suivant:

  1. Le moteur Javascript effectue 2 passes sur le code avant de l’exécuter:
    • Une 1ère passe pour référence toutes les déclarations de variables, de fonctions et les définitions des fonctions.
    • Une 2e passe permet d’affecter à ces objets leur valeur d’initialisation (s’il y en a une).
  2. Ces 2 passes entraînent que l’ordre de traitement des lignes de code par le moteur Javascript n’est pas toute à fait le même que celui du script:
    • Les déclarations sont traitées avant les affectations (même si dans le script l’affectation d’une variable est placée avant la déclaration).
    • Les déplacements des lignes traitées sont effectués à l’intérieur d’un même scope et se limite au scope d’une fonction.
    • Les déclarations de fonction sont déplacées avant les autres objets.

Par exemple, si on exécute le code suivant:

function a() {
  x = 5;
  var x;
  console.log(x); // 5
  console.log(window.x); // undefined
}

a();

On pourrait penser qu’étant donné l’ordre des lignes dans a(), x = 5 correspondrait à l’affectation de 5 à la variable globale x. Mais le comportement de hoisting entraîne la modification de l’ordre de traitement des lignes par le moteur Javascript. Dans la réalité, les lignes sont exécutées de cette façon:

function a() {
  var x;
  x = 5;
  console.log(x); // 5
  console.log(window.x); // undefined
}

a();

A l’intérieur du scope de la fonction a(), var x est traité avant x = 5.

Pas d’hoisting avec let ou const

Si on exécute:

function a() {
  x = 5; // ReferenceError: can't access lexical declaration 'x' before initialisation
  let x;
  console.log(x);
  console.log(window.x);
}

a();

Une erreur explicite indique qu’on peut pas inverser l’ordre des lignes, la déclaration doit se trouver avant l’initialisation.

Hoisting valable pour les fonctions anonymes et les arrow functions
Le même comportement d’hoisting est valable pour les fonctions anonymes (i.e. function expressions), par exemple:

a = function() {
  return 'from function expression';
}

var a;

console.log(a()); // from function expression
console.log(window.a()); // window.a is not a function

Même comportement que précédemment, la déclaration de la fonction a() est déplacée par hoisting.

Le comportement est le même pour les arrow functions:

a = () => {
  return 'from arrow function';
}

var a;

console.log(a()); // from arrow function
console.log(window.a()); // window.a is not a function

Les déclarations de fonctions sont déplacées avant les autres objets
Les déclarations des fonctions sont traitées prioritairement par rapport aux déclarations de variables.
Si on exécute le code suivant:

console.log(a()); // from a()

a = function() {
  return 'from function expression';
}

var a;

function a() {
  return 'from a()';
}

L’hoisting déplace le code de cette façon:

function a() {
  return 'from a()';
}

console.log(a()); // from a()

a = function() {
  return 'from function expression';
}

En résumé…

D’une façon générale, il faut éviter de déclarer des variables dans le scope global en javascript. Pour que la portée d’une variable soit limitée, on utilise les mots-clé var, let ou const:

  • var est compatible avec toutes les versions de Javascript toutefois il entraîne certains comportements qui ne sont pas standards.
  • let est apparu à partir de ES6. La portée d’une variable déclarée avec ce mot-clé est standard par rapport aux autres langages.
  • const est similaire à let à la différence qu’il n’autorise pas de nouvelles affectations à une variable.

Le scope d’une variable se limite à un bloc de code pour let et const. Avec var, le scope correspond à une fonction, un bloc de code ne limite pas la portée.

Lexical scope
Le moteur Javascript utilise le lexical scope pour déterminer la déclaration d’une variable. Suivant les différents mots-clé utilisés la notion de scope ne sera pas la même toutefois la recherche de la déclaration d’une variable se fait de la même façon:

  • Lorsque le moteur d’exécution exécute la déclaration d’une variable, le scope de la variable sera celui dans lequel se trouve la déclaration.
  • Lorsque le moteur d’exécution cherche la valeur d’une variable, il recherche sa déclaration dans le scope courant. Si aucune déclaration n’est trouvée, le moteur cherche dans les scopes parent. Si aucune déclaration n’est trouvée alors la variable appartient au scope global.
  • Si une variable est déclarée dans un scope parent et qu’une variable est déclarée avec le même nom dans un scope enfant, la variable dans le scope parent sera occultée par celle du scope enfant. Ce comportement correspond au shadowing.

eval() et new Function()
On peut modifier le comportement du lexical scope avec eval() qui permet d’évaluer une expression sous forme de chaîne de caractères:

  • Le scope de l’expression évaluée par eval() est celui dans lequel eval() est exécuté. L’expression peut être construite dans un scope différent, toutefois c’est au moment de l’exécution que le scope de l’expression sera confondu avec le scope dans lequel eval() est exécuté.
  • En mode strict, le scope de l’expression évaluée par eval() est limité à eval(), il n’est pas confondu avec le scope dans lequel eval() est exécuté.

Le constructeur new Function() permet aussi d’évaluer une expression sous forme de chaîne de caractères. La différence avec eval() est que le scope parent d’un objet de type Function est le scope global et non le scope dans lequel new Function() est exécuté.

this
La valeur de this dépend du contexte dans lequel il est utilisé:

  • Dans une classe, this est l’instance de cette classe.
  • Dans un objet, this est l’instance de cet objet.
  • Dans une fonction n’appartenant pas à un objet:
    • En mode non strict, this est le contexte global
    • En mode strict, this est indéfini.

On peut affecter explicitement une valeur à this quand on exécute une fonction en utilisant:

  • call(): <fonction>.call(thisVar, [arg1, arg2,..., argN]);
  • apply(): <fonction>.apply(thisVar, [tableau d'arguments]);
  • bind() qui permet de créer une nouvelle référence vers une fonction: let <référence fonction> = <fonction>.bind(thisVar, [arg1, arg2,..., argN]);

Si on exécute une fonction avec setTimeout(), par défaut, this fait référence au contexte global.

Hoisting

  • Si on déclare une variable avec var, le comportement d’hoisting du moteur Javascript peut entraîner un déplacement des lignes de code d’un script pour prendre en compte la déclaration d’une variable avant son initialisation. Même si dans le script initial, l’initialisation se trouve avant la déclaration, le moteur prendra en compte d’abord la déclaration.
  • Il n’y a pas d’hoisting si on utilise let ou const.
  • L’hoisting est valable aussi pour les fonctions anonymes ou les arrow functions.
  • Les déclarations de fonctions sont déplacées avant les autres objets.
Références
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Personnaliser la détection de changements dans une application Angular

@raychelsnr

Le but de cet article est d’expliquer quelques caractéristiques de la détection automatique de changements dans une application Angular. Ensuite, on indiquera comment on peut personnaliser cette détection pour améliorer les performances ou pour la solliciter de façon explicite.
Un autre article permet de rentrer dans les détails du fonctionnement de cette détection de changements: Fonctionnement de la détection de changement.

Une application Angular peut être impactée par des évènements provenant du browser comme:

  • Les évènements Javascript comme les clicks, keydown, submit, input etc… (Référence des événements),
  • Les évènements XHR provenant de l’objet XmlHttpRequest,
  • Les évènements provenant de Timers comme setTimeout() ou setInterval().

Certains de ces évènements peuvent entraîner des modifications sur des éléments de l’application et nécessiter une mise à jour des vues de certains composant. La détection automatique des changements provoqués par ces évènements permet de mettre à jour les éléments dans le DOM quand cela est nécessaire.

Ainsi, les changements pouvant impacter la vue d’un composant peuvent provenir de:

  • La mise à jour d’un binding dans un composant,
  • La mise à jour d’une expression dans une interpolation d’un template.

D’autres changements peuvent impacter des bindings se trouvant dans la classe du composant comme:

  • Les requêtes de vue avec @ViewChild() ou @ViewChildren(),
  • Les requêtes sur du contenu projeté avec @ContentChild() ou @ContentChildren().
  • Des bindings avec un élément natif hôte concernant des propriétés @HostBinding() ou des évènements avec @HostListener().

Lorsqu’au moins une des sources impacte un élément d’une vue, la détection de changements est exécutée en comparant les références des anciens bindings et les nouveaux de façon à détecter un changement. Dans le cas où un changement est détecté, les éléments natifs correspondant dans le DOM sont modifiés (pour plus de détails, voir le fonctionnement de la détection de changements).

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

Template
(example.component.html)
<p>Example composant</p>
<p>Click count: {{clickCount}}</p>
Classe du composant
(example.component.ts)
import { Component, HostListener } from '@angular/core';

@Component({
    selector: 'app-example',
    templateUrl: './example.component.html',
    styleUrls: ['./example.component.css']
})
export class ExampleComponent {
    clickCount = 0;

    @HostListener('click') onHostClicked(triggeredEvent): void {
        this.clickCount++;
    }
}
Style CSS
(example.component.css)
:host {
    width: 300px;
    height: 150px;
    background-color: lightblue;
    display: block;
    padding: 10px;
}

Dans cet exemple, on utilise @HostListener() pour s’abonner à l’évènement click de l’élément natif de la vue du composant de façon à effectuer un traitement pour chaque click sur cet élément.

A l’exécution, la vue de ce composant se présente de cette façon:

Exemple de détection de changements avec click

Si on clique sur la zone en bleu, le compteur s’incrémente. Pour que la propriété clickCount soit incrémentée, l’évènement est propagé de cette façon dans Angular:

  1. Un click sur un élément natif déclenche un évènement click.
  2. Cet évènement se propage dans Zone.js.
  3. Zone.js déclenche une callback correspondant à du code Angular qui lancera la méthode tick().
  4. La méthode tick() permet de déclencher la détection de changements dans l’application en commençant par le composant Root.
  5. La détection de changements est déclenchée dans le composant Example ce qui va entraîner la mise à jour du binding initié par @HostListener(). Cette mise à jour met à jour la propriété clickCount.
  6. En parallèle, le template est mis à jour et l’expression d’interpolation {{clickCount}} est évaluée ce qui modifie la valeur (car la propriété clickCount a changé de valeur).
  7. Une comparaison entre la nouvelle valeur de l’expression d’interpolation et l’ancienne permet d’indiquer qu’un changement a été détecté.
  8. Le changement permet d’effectuer la mise à jour de l’objet natif dans le DOM.

Détection des changements et cycle de vie des composants

La détection des changements est liée à l’exécution des callbacks du cycle de vie des composants (i.e. lifecycle hooks). Ainsi, la vérification des changements et la mise à jour éventuelle des éléments du DOM sont effectuées dans un certain ordre. Modifier un binding de façon trop tardive par rapport à la détection de changements dans le cycle de vie d’un composant peut ne pas entraîner de modifications dans la vue.

Le cycle de vie d’un composant est, dans l’ordre de déclenchement:

  • 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()). 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:
    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().

Lors de l’exécution de la détection de changements, la mise à jour des éléments est effectuée de la façon suivante par rapport aux callbacks du cycle de vie:

  1. Exécution des callbacks ngOnChanges(), ngOnInit() et ngDoCheck(). La callback ngOnInit() est exécutée seulement à l’initialisation du composant.
  2. Exécution de la fonction contentQueries: cette fonction permet de mettre à jour les requêtes effectuées sur le contenu projeté de la vue avec @ContentChild() ou @ContentChildren().
  3. Exécution des callbacks ngAfterContentInit() et ngAfterContentChecked(): l’exécution de ces callbacks vient clôturer la mise à jour des requêtes effectuées sur le contenu projeté. La callback ngAfterContentInit() est exécutée seulement à l’initialisation du composant.
  4. Exécution de la fonction hostBindings: cette fonction permet d’affecter les bindings provenant de l’élement HTML hôte avec @HostBinding().
  5. Exécution de la fonction template: elle servira à mettre à jour le DOM avec tous les éléments dynamiques de la vue.
  6. Exécution de la fonction viewQuery: cette étape permet de mettre à jour les requêtes effectuées sur la vue du composant avec @ViewChild() et @ViewChildren().
  7. Exécution des callbacks ngAfterViewInit() et ngAfterContentChecked(), la callback ngAfterViewInit() est exécutée seulement à l’initialisation du composant.

Erreur “ExpressionChangedAfterItHasBeenCheckedError”

Dans le cas où une modification est effectuée trop tardivement, une erreur du type ExpressionChangedAfterItHasBeenCheckedError peut se produire en mode développement.
Pour davantage de détails, voir cdiese.fr/angular-change-detection.

Au cours de son exécution, l’algorithme de détection de changements ne s’applique pas qu’à un composant, il parcourt aussi toutes les directives enfant de ce composant. Un composant parent peut être lié aux directives enfants de multiples façons comme par exemple:

  • Un binding entre une propriété et un paramètre d’entrée du composant enfant,
  • Une requête sur un contenu projeté dans le composant enfant avec @ContentChild(),
  • Une requête sur le composant natif hôte du composant enfant avec @HostListener().
  • Etc…

Toutes ces fontionnalités entre un composant parent et un composant enfant imposent à l’algorithme de détection de prendre en compte ce lien et surtout un ordre dans la mise à jour des objets dans le DOM.

Au niveau d’un composant, du point de vue de la détection de changements on pourrait résumer le lien avec son parent avec ce schéma (pour davantages de détails, voir le fonctionnement de la détection de changements):

Ce schéma permet de montrer que certains éléments d’un composant sont mis à jour de façon concomitante avec des traitements du composant parent.

Ordre d’exécution de la détection de changements dans l’arbre des composants

Etant donné les liens entre un composant parent et ses enfants, à l’échelle de plusieurs composants, l’ordre d’exécution des traitements de la détection de changements dans l’arbre des composants n’est pas forcément trivial.

Si on reprend le schéma plus haut permettant de schematiser l’exécution de l’algotihme de détection de changements entre un composant parent et un composant enfant, on peut le séparer en 2 parties: avant et après la boucle récursive de parcours des composants enfant:

Ces 2 parties amènent 2 comportements différents qui entraînent un parcours différents de l’arbre des composants:

  • Avant la boucle récursive: les traitements et exécution des callbacks du cycle de vie sont exécutés dans l’arbre des composants parent vers les composants enfant:

    Ainsi les callbacks ngOnChanges(), ngOnInit(), ngDoCheck(), ngAfterContentInit(), ngAfterContentChecked() et la fonction template permettant de mettre à jour le DOM sont exécutées dans cette partie. Dans le cadre de l’exemple, elles seront déclenchées dans cet ordre: d’abord les composants parent et ensuite les composants enfant.

  • Après la boucle récursive: les traitements et exécution des callbacks sont exécutés des composants enfant vers les composants parents en allant d’abord vers le composant le plus profond dans l’arbre:

    Les callbacks ngAfterViewInit() et ngAfterViewChecked() sont exécutées dans cette partie. Dans le cadre de l’exemple, elles seront déclenchées dans cet ordre: d’abord les composants enfant et ensuite les composants parent.

Contourner la détection de changements

Dès qu’un évènement notifie Zone.js, il déclenche la détection de changements dans Angular. Si on souhaite exécuter du code sans que la détection de changements ne soit sollicitée, on peut utiliser l’objet NgZone.

En injectant un objet de type NgZone dans le constructeur d’un composant, on obtient un service permettant d’effectuer des traitements sur la zone parente de la vue du composant:

import { Component, NgZone } from '@angular/core';

@Component({
    // ...
})
export class ExampleComponent {
    constructor(private zone: NgZone) {}
}

Le service NgZone permet d’exécuter des lambdas avec les fonctions:

  • runOutsideAngular() pour exécuter du code en dehors de la détection de changements Angular.
  • run() exécute du code dans une zone Angular. La détection de changements sera exécutée.
  • runTask() permet d’exécuter du code dans une zone Angular de façon asynchrone. La détection de changements sera exécutée.

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

Template
<p>Example composant</p>
<p>Random number: {{randomNumber}}</p>
Classe du composant
import { Component, NgZone, OnInit } from '@angular/core';

@Component({
    selector: 'app-example',
    templateUrl: './example.component.html'
})
export class ExampleComponent implements OnInit {
    randomNumber: number;

    constructor(private zone: NgZone) {}

    ngOnInit(): void {
        setInterval(() => {
            this.randomNumber = Math.random();
        }, 1000);
    }
}

Au moyen de setInterval(), on peut mettre à jour la propriété randomNumber toutes les secondes avec un nombre aléatoire. Une interpolation dans le template du composant permet d’afficher la propriété randomNumber dans la vue.

A l’exécution, on peut voir que le nombre est mis à jour toutes les secondes:

Exemple de détection de changements avec setInterval()

La propagation de l’évènement est effectuée de cette façon:

  1. La callback de la méthode setInterval() est déclenchée toutes les secondes.
  2. L’exécution de la callback permet de mettre à jour la propriété randomNumber.
  3. Cet évènement se propage dans Zone.js.
  4. La suite de la détection de changements s’exécute le long de l’arbre des composants.

Si on modifie la méthode ngOnInit() dans la classe du composant de cette façon:

ngOnInit(): void {
    this.zone.runOutsideAngular(
        setInterval(() => {
            this.randomNumber = Math.random();
        }, 1000);
    );
}

Après cette modification, la mise à jour de la vue n’est plus effectuée. La propriété est bien mise à jour par setInterval() toutefois la détection de changements n’est pas déclenchée:

Mettre à jour la vue directement

Quand on utilise NgZone.runOutsideAngular(), il est possible de mettre à jour la vue sans utiliser la détection de changement en intervenant directement dans l’objet natif de la vue.

Par exemple, si on reprend le composant Example introduit plus haut, on peut utiliser @ViewChild() avec une variable référence dans le template pour effectuer une requête sur la vue de façon à pouvoir accéder à l’objet natif dans lequel on veux mettre à jour l’affichage. L’implémentation deviendrait:

Template
<p>Example composant</p>
<p #random></p>
Classe du composant
import { Component, NgZone, OnInit, ViewChild, ElementRef } 
  from '@angular/core';

@Component({
    selector: 'app-example',
    templateUrl: './example.component.html'
})
export class ExampleComponent implements OnInit {
    randomNumber: number;

    @ViewChild('random', { static: true }) 
        random: ElementRef<HTMLParagraphElement>

    constructor(private zone: NgZone) {}

    ngOnInit(): void {
        this.zone.runOutsideAngular(
            setInterval(() => {
                this.randomNumber = Math.random();
                this.random.nativeElement.textContent = 
                    `Random number: ${this.randomNumber}`;
            }, 1000);
        );
    }
}

Ainsi:

  • Ce code permet d’effectuer une requête sur la vue du composant avec @ViewChild() pour récupérer un objet identifié avec la variable référence #random dans le template. Cet objet permettra d’accéder à l’objet natif correspondant.
  • On utilise l’option { static: true } dans @ViewChild() de façon à ce que la requête soit exécutée lors de la construction des éléments statiques de la vue.
  • Avec la propriété nativeElement, on peut accéder à l’objet natif correspond à l’élément p de façon à modifier son contenu.

A l’exécution, on peut voir que le nombre est mise à jour chaque seconde dans la vue du composant.

Déclencher la détection de changements dans toute l’application

A l’opposé, on peut volontairement lancer l’exécution de la détection de changements de toute l’application en injectant l’objet ApplicationRef et en appelant ApplicationRef.tick().

Par exemple, si on réutilise l’exemple précédent de ce composant:

Template
<p>Example composant</p>
<p>Random number: {{randomNumber}}</p>
Classe du composant
import { Component, NgZone, OnInit } from '@angular/core';

@Component({
    selector: 'app-example',
    templateUrl: './example.component.html'
})
export class ExampleComponent implements OnInit {
    randomNumber: number;

    constructor(private zone: NgZone) {}

    private updateRandomNumber(): void {
        this.randomNumber = Math.random();
    }

    ngOnInit(): void {
        this.zone.runOutsideAngular(
            setInterval(() => {
                this.updateRandomNumber();
            }, 1000);
        );
    }
}

Cet exemple ne permet pas de mettre à jour la vue avec la propriété randomNumber car NgZone.runOutsideAngular() ne déclenche pas la détection de changements.

Pour forcer la détection de changements à chaque exécution de la lambda dans setInternal(), on peut rajouter ApplicationRef.tick(). On modifie la méthode ngOnInit() dans ce sens:

import { Component, NgZone, OnInit, ApplicationRef } from '@angular/core';

@Component({
    selector: 'app-example',
    templateUrl: './example.component.html'
})
export class ExampleComponent implements OnInit {
    randomNumber: number;

    constructor(private zone: NgZone, private ApplicationRef: ApplicationRef) {}

    private updateRandomNumber(): void {
        this.randomNumber = Math.random();	

        this.applicationRef.tick();
    }

    ngOnInit(): void {
        this.zone.runOutsideAngular(
            setInterval(() => {
                this.updateRandomNumber();
            }, 1000);
        );
    }
}

Dans ce code, this.applicationRef.tick() permet de lancer l’exécution de la détection de changements à chaque exécution de la lambda dans setInterval(). A l’exécution, on peut voir que la vue est correctement mise à jour: la valeur du nombre change toutes les secondes.

Personnaliser la détection de changements

2 méthodes permettent de mettre à jour une vue en cas de changements:

  • Détecter les changements de façon automatique: l’algorithme de détection effectue les comparaisons nécessaires pour savoir quand un changement nécessite une mise à jour de propriétés dans le DOM. L’algorithme détecte le changement de valeur sans savoir d’où provient ce changement.
  • Mettre à jour la vue à la demande: en cas de changements, on peut solliciter explicitement la détection de changements pour que la vue soit mise à jour à la demande. L’algorithme de détection de changements et la mise à jour des éléments natifs du DOM sont indissociables. On ne peut pas exécuter l’un sans l’autre. En revanche, il est possible de désactiver la détection automatique pour privilégier une exécution à la demande pour maitriser la mise à jour d’une vue ou pour améliorer les performances d’exécution.

Dans cette partie, on va expliciter des méthodes pour désactiver la détection automatique de changements ou pour lancer explicitement son exécution.

ChangeDetectionStrategy

Modifier le paramètre changeDetection d’un composant (dans le décorateur @Component()) permet de modifier le comportement de la détection:

  • ChangeDetectionStrategy.Default: déclenchement automatique de la détection de changements, c’est le comportement par défaut. La détection est exécutée pour le composant et ses enfants de façon automatique.
  • ChangeDetectionStrategy.OnPush: désactive la détection automatique des changements pour le composant et pour ses enfants. Il faut déclencher la détection de la façon explicite avec ChangeDetectorRef ou ApplicationRef.

Modifier le paramètre changeDetection permet de désactiver la détection pour toutes les instances du composant et pour toute sa durée de vie. Cette implémentation est moins flexible qu’en utilisant ChangeDetectorRef (même si on exécute ChangeDetectorRef.reattach(), la détection automatique restera désactivée).

Par exemple, si on modifie le paramètre changeDetection dans le cas du composant Example, l’implémentation devient:

@Component({
    selector: 'app-example',
    templateUrl: './example.component.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExampleComponent implements OnInit {
    // ...
}

ChangeDetectorRef

En injectant ChangeDetectorRef dans un composant, on peut personnaliser la détection de changements pour:

  • Désactiver son exécution automatique avec ChangeDetectorRef.detach(),
  • Ré-activer la détection automatique avec ChangeDetectorRef.reattach(),
  • Forcer l’exécution de la détection avec ChangeDetectorRef.detectChanges(),
  • Vérifier si des changements sont détectés avec ChangeDetectorRef.checkNoChanges() ou
  • Marquer la vue du composant pour que la détection de changements soit effectuée avec ChangeDetectorRef.markForCheck().

On peut injecter un objet de type ChangeDetectorRef en utilisant le moteur d’injection de dépendances Angular, par exemple:

Template
<p>Example composant</p>
Classe du composant
import { Component, ChangeDetectorRef } from '@angular/core';

@Component({
    selector: 'app-example',
    templateUrl: './example.component.html'
})
export class ExampleComponent {
    constructor(private changeDetectorRef: ChangeDetectorRef) {}
}

L’instance injectée de l’objet de type ChangeDetectorRef est spécifique à la vue du composant.

Désactiver/Activer la détection de changements

La détection de changements peut être désactivée ou activée par programmation de façon à optimiser les performances d’une vue. Lorsque la détection est désactivée pour un composant donné, la détection ne sera plus exécutée de façon automatique sur la vue du composant et sur les vues des composant enfant:

Pour exécuter la détection de changements à la demande, il faudra explicitement exécuter ChangeDetectorRef.detectChanges() ou ChangeDetectorRef.markForCheck() sur le composant pour lequel la vue doit être mise à jour. En cas d’exécution explicite de la détection pour un composant donné, la détection sera effectuée sur le composant et sur toutes ses directives enfants.

Par exemple, si on considère un composant pour lequel on place un Timer qui permet de mettre à jour périodiquement la valeur d’une propriété de type number et qu’on affiche cette propriété sur la vue en utilisant une interpolation:

Template
<p>Example composant</p>
<p>Random number: {{randomNumber}}</p>
Classe du composant
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';

@Component({
    selector: 'app-example',
    templateUrl: './example.component.html'
})
export class ExampleComponent implements OnInit {
    randomNumber: number;

    constructor(private changeDetectorRef: ChangeDetectorRef) {}

    ngOnInit(): void {
        setInterval(() => {
            this.randomNumber = Math.random();
        }, 1000);
    }
}

Si on exécute ce code, on pourra voir que l’affichage est mise à jour toutes les secondes.

On ajoute 2 boutons pour désactiver et activer la détection de changements:

Template
<p>Example composant</p>
<p>Random number: {{randomNumber}}</p>
<p><button (click)='disableChangeDetection()'>Disable change detection</button></p>
<p><button (click)='enableChangeDetection()'>Enable change detection</button></p>
Classe du composant
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';

@Component({
    selector: 'app-example',
    templateUrl: './example.component.html'
})
export class ExampleComponent implements OnInit {
    randomNumber: number;

    constructor(private changeDetectorRef: ChangeDetectorRef) {}

    ngOnInit(): void {
        setInterval(() => {
            this.randomNumber = Math.random();
        }, 1000);
    }

    disableChangeDetection(): void {
        this.changeDetectorRef.detach();
    }

    enableChangeDetection(): void {
        this.changeDetectorRef.reattach();
    }
}

A l’exécution, on peut voir que le nombre ne sera plus mis à jour dans la vue si on clique sur “Disable change detection”. La mise à jour est réativée si on clique sur “Enable change detection “.

Ainsi, quand on a exécuté ChangeDetectorRef.detach() dans le composant Example, la détection de changements est désactivée pour ce composant et pour les composants enfant:

Vérifier si des changements sont détectés

On peut se demander l’utilité de cette méthode en sachant qu’elle déclenche une erreur dans le cas où un changement est détecté. La raison est que cette détection a pour but d’éviter des erreurs d’implémentation en vérifiant que des changements ne sont pas effectués trop tardivement dans le cycle de vie d’un composant (cf. ExpressionChangedAfterItHasBeenCheckedError).

La vérification peut être effectuée en exécutant:

ChangeDetectorRef.checkNoChanges();

En conclusion

Pour résumer, on peut personnaliser la détection de changements en:

  • Contournant l’exécution de la détection: en injectant l’objet NgZone dans le composant. Avec NgZone.runOutsideAngular(), on peut exécuter du code qui n’aboutira pas à la détection automatique de changements.
  • Forçant la mise à jour d’un élément d’une vue en modifiant directement l’objet natif, par exemple:
    • En effectuant une requête sur la vue avec @ViewChild() pour obtenir l’objet natif:
      @ViewChild('elementName', { static: true }) element: ElementRef<HTMLParagraphElement>
      
    • En affectant une valeur avec la propriété nativeElement:
      this.element.nativeElement.textContent = ... 
      
  • Forçant l’exécution de la détection de changements en injectant l’objet ApplicationRef et exécutant ApplicationRef.tick().
  • Désactivant définitivement la détection dans un composant avec le paramètre changeDetection dans le décorateur @Component():
    @Component({
        selector: ...,
        templateUrl: ...,
        changeDetection: ChangeDetectionStrategy.OnPush
    })
    
  • En injectant l’objet ChangeDetectorRef dans le constructeur pour:
    • Désactiver l’exécution automatique de la détection en exécutant ChangeDetectorRef.detach(),
    • Ré-activer la détection automatique en exécutant ChangeDetectorRef.reattach(),
    • Forcer l’exécution de la détection avec ChangeDetectorRef.detectChanges(),
    • Vérifier si des changements sont détectés en exécutant ChangeDetectorRef.checkNoChanges() ou
    • Marquer la vue du composant pour que la détection de changements soit effectuée en exécutant ChangeDetectorRef.markForCheck().
Références
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Fonctionnement de la détection de changement dans une application Angular

@dlohmar

La détection de changements est un sujet important car l’algorithme qui le gère est le même que celui qui met à jour le DOM et qui déclenche les callbacks du cycle de vie d’un composant (i.e. lifecycle hooks). Certains comportements d’Angular découlent directement de cet algorithme de détection.

Le but de cet article est de rentrer dans quelques détails de l’implémentation du mécanisme de détection de changements en s’appuyant sur le code d’Angular 9 sur GitHub. Le code concernant ce mécanisme a été beaucoup modifié entre les versions 7 et 8, les explications de cet article peuvent ne pas correspondre pour les versions précédant la version 9. Les mécanismes permettant de provoquer la détection de changements avec ChangeDetectorRef ne seront pas abordés dans cet article mais dans Personnaliser la détection de changements.

Pourquoi effectuer une détection de changements ?

Pour beaucoup de frameworks Javascript, il existe, au moins 2 versions des objets présentés sur la page web:

  • Une version maintenue complètement par le framework: il peut modifier ces objets ou leurs propriétés à sa guise. Cette version contient des propriétés plus ou moins spécifiques au framework. Dans le cas d’Angular, ces objets contiennent des références vers les objets équivalents dans le DOM.
  • Le DOM contenant des objets dont les propriétés sont utilisées pour présenter des éléments HTML sur l’interface.

Pour qu’une application puisse être fonctionnelle, il faut qu’elle puisse réagir aux différents évènements extérieurs correspondant à tous les évènements Javascript possibles. Ces évènements peuvent provoquer l’exécution d’un traitement dans l’application qui pourra éventuellement mettre à jour un élément graphique. 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.

Cette détection automatique présente de multiples avantages:

  • Les accès au DOM sont limités aux cas où un changement est nécessaire,
  • Graphiquement les changements permettent de modifier certains éléments sans recharger la page, ce qui limite les manipulations effectuées sur les éléments graphiques et améliore l’expérience utilisateur.
  • Enfin le développeur est moins préoccupé par la mise à jour des éléments graphiques d’un composant puisqu’Angular encapsule une grande partie de la complexité des bindings entre des objets du code et les éléments graphiques correspondant.

Zone.js

De façon à éventuellement mettre à jour des éléments graphiques, la détection de changements doit s’exécuter quand un évènement se déclenche dans le browser. Ce déclenchement est possible gràce à la bibliothèque Zone.js. Zone.js a la particularité de pouvoir intercepter les évènements asynchrones comme setInterval() et de fournir à Angular un contexte d’exécution de cet évènement. Il notifie ensuite Angular qui déclenche l’algorithme de détection de changements.

Les évènements extérieurs à Angular qui peuvent subvenir sont:

  • Les évènements Javascript comme les clicks, keydown, submit, input etc… (Référence des événements),
  • Les évènements XHR provenant de l’objet XmlHttpRequest,
  • Les évènements provenant de Timers comme setTimeout() ou setInterval().

Tous les évènements notifiés par Zone.js ne nécessitent pas forcément une modification d’un élément graphique. C’est le mécanisme de détection de changements qui va permettre de filtrer les évènements pour ne garder que les évènements pertinents qui affectent réellement un objet de l’interface de façon . Pour ne pas trop affecter les performances et le temps d’exécution, cette détection de changements ne doit pas être trop coûteuse.

Cycle de vie des composants

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.

Ainsi, lors de la création et du rendu de la vue d’un composant, les 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”.

Le but de ces callbacks est de pouvoir interagir finiment avec le framework durant les différentes phases de la création d’un composant et de sa vue dans un premier temps ou lors de la détection d’un 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 d’un changement: avant ou après la vérification d’un changement sur un élément particulier du 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().

Lien entre la détection des changements et le cycle de vie des composants

Dans le cadre de la détection des changements, le cycle de vie des composants permet à Angular d’indiquer des étapes des vérifications qu’il effectue afin de détecter des éventuels changements, par exemple:

  • Après la création d’un contenu projeté, les callbacks ngAfterContentInit() et ngAfterContentChecked() seront déclenchées de façon à indiquer que le contenu a été vérifié et que les changements dans le contenu projeté ne seront plus vérifiés après cette phase.
  • De même après avoir terminé la création de la vue, lorsque les callbacks ngAfterViewInit() et ngAfterViewChecked() seront déclenchées, il n’y aura plus de vérification de changements.

Ainsi, si des modifications sont effectuées sur des éléments interagissant avec des vues après les détections de changements, les éléments correspondants sur les vues ne seront pas mis à jour. Il est donc important d’effectuer ces modifications au bon moment lors de l’exécution des callbacks du cycle de vie.

Erreur “ExpressionChangedAfterItHasBeenCheckedError”

L’erreur "ExpressionChangedAfterItHasBeenCheckedError" survient lors de l’exécution en mode développement pour indiquer qu’une modification a été effectuée trop tardivement par rapport à la détection de changements. Les conséquences de ces modifications ne seront probablement pas répercutées sur des éléments des vues.

L’erreur "ExpressionChangedAfterItHasBeenCheckedError" est déclenchée seulement en développement

L’erreur "ExpressionChangedAfterItHasBeenCheckedError" est déclenchée en développement pour indiquer qu’une erreur de conception risque de compromettre la détection de changement et empêcher la mise à jour correcte de la vue. En production, Angular n’effectue pas de vérifications pouvant amener aux déclenchements de cette erreur.

Par exemple, si considère un composant pour lequel on implémente:

  • Les callbacks ngAfterContentInit(), ngAfterContentChecked(), ngAfterViewInit() et ngAfterViewChecked(),
  • Une interpolation dans le template vers une propriété de la classe du composant.

L’implémentation est:

Template
{{updatedValue}} 
Classe du composant
import { Component, ngAfterViewInit, ngAfterViewChecked, 
    ngAfterContentInit, ngAfterContentChecked } from '@angular/core'; 

@Component({ 
    templateUrl: './example.component.html' 
})    
export class ExampleComponent implements ngAfterViewInit, ngAfterViewChecked, 
    ngAfterContentInit, ngAfterContentChecked { 
    updatedValue = 'Not updated'; 

    ngAfterContentInit(): void {} 

    ngAfterContentChecked(): void { 
        this.updatedValue = 'Updated'; 
    } 

    ngAfterViewInit(): void {} 

    ngAfterViewChecked(): void {} 
}

Si on exécute en mode développement en lançant la commande suivante:

ng serve 

Le texte Updated sera correctement affiché. Si on modifie le code pour mettre à jour la valeur dans ngAfterViewInit() ou dans ngAfterViewChecked():

ngAfterContentChecked(): void { 
    //this.updatedValue = 'Updated'; 
} 

ngAfterViewInit(): void { 
    this.updatedValue = 'Updated'; 
} 

On constate l’erreur suivante dans la console du browser:

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

Par suite, le texte contient la même valeur qu’à l’initialisation: Not updated.

Si on exécute en mode production en lançant la commande:

ng serve --prod 

L’erreur n’est pas affichée dans la console, toutefois le texte n’est pas mis à jour et contient toujours sa valeur initiale: Not updated.

La valeur n’est pas mise à jour dans le template car la modification de la propriété updatedValue est effectuée après la dernière détection de changements. En effet quand la callback ngAfterViewInit() est déclenchée, la vue est créée et Angular considère que des modifications ne doivent plus être effectuées à ce stade.

Dans la suite de cet article, on va expliquer plus en détails l’origine de ce problème et pourquoi Angular affiche cette erreur en mode développement.

Ivy

Depuis Angular 9, l’implémentation d’Angular a été complêtement modifiée pour que le view engine (appelé view Engine “render 2”) soit remplacé par Ivy (appelé view engine “render 3”). Ivy est un compilateur et un moteur de rendu dont les fonctionnalités et les performances sont meilleures que le view engine utilisé dans les versions précédentes notamment avec un rendu plus rapide avec la compilation AOT, la fonctionnalité Tree-Shaking et un runtime qui permet de mettre à jour le DOM de façon plus performante.

Compilation Ahead-of-Time (AOT)

La compilation AOT est le mode de compilation par défaut dans Angular à partir de la version 9 en opposition à la compilation Just-In-Time (JIT) utilisée dans les versions précédentes. La différence entre les 2 modes de compilation est que l’AOT effectue une compilation du code qui va permettre de modifier le DOM pendant la compilation de l’application (build time) et non lors de son exécution (runtime).

Dans le cadre de la compilation JIT (i.e. Just-In-Time), la compilation des templates est effectuée à l’exécution de façon à s’adapter au contexte d’exécution. Le compilateur se trouve, donc, dans le bundle Javascript chargé par le browser au moment d’exécuter l’application. Ce bundle comprend aussi les fichiers HTML correspondant aux templates, le compilateur utilise les fichiers HTML pour les convertir en code exécutable qui sera capable de mettre à jour le DOM. Les gros inconvénients de ce mode de compilation sont que:

  • Le bundle chargé à l’exécution est plus lourd à cause des fichiers HTML et surtout du compilateur,
  • Certaines erreurs surviennent lors de l’exécution et lors de la compilation au runtime.
  • L’application est plus lente puisque la compilation est nécessaire avant le rendu des vues.

A l’opposé, le compilation AOT (i.e. Ahead-of-Time) effectue la compilation des templates au moment de la compilation de l’application en produisant un code qui n’est plus dépendant du contexte d’exécution. Ainsi, le bundle chargé au moment de l’exécution ne contient ni le compilateur, ni les fichiers HTML correspondant aux templates. Le code des templates est déjà compilé et permet de mettre à jour le DOM efficacement sans compilation préalable. Les inconvénients de la compilation JIT sont levés avec la compilation AOT:

  • Le bundle est plus léger puisqu’il ne contient pas le compilateur et le code HTML,
  • Certaines erreurs liées aux vues peuvent être détectées au moment de la compilation,
  • L’exécution est plus rapide.

Tree-shaking

Le tree-shaking est une optimisation qui permet de ne pas inclure le code qui est inutile dans le bundle obtenu après compilation et chargé par le browser au runtime. L’intérêt de cette optimisation est de réduire la taille du bundle et s’avère particulièrement efficace car elle s’applique aussi au code du framework. Ivy permet de mettre en place cette optimisation lors de la compilation d’une application Angular.

Compilation d’un composant

Lors de la compilation avec Ivy, les metadatas se trouvant dans les décorateurs sont utilisées pour générer du code et des propriétés qui seront exécutées directement au runtime suivant les différentes mises à jour nécessaires des vues .

Ainsi quand on implémente un composant, on utilise le décorateur @Component() et on indique des metadatas dans ce décorateur. Le compilateur génère une factory permettant d’instancier le composant à l’exécution et de générer le code qui permettra de convertir ces metadatas en propriétés qui seront affectées à un objet qui sera la définition du composant.

Dans la définition de ce composant, on trouvera, par exemple un type, un template, des styles, des animations etc… Cette définition du composant sera compilée pour former un type qui pourra être instancié par la suite.

La compilation permet de générer le code qui sera exécuté au runtime pour mettre à jour le DOM. Par exemple, les méthodes les plus importantes permettant ces mises à jour sont:

  • template: le code de la vue contient du code statique avec des éléments HTML fixe et du code dynamique au travers des property bindings, event bindings ou interpolations.
  • contentQueries: ce code est exécuté pour mettre à jour des requêtes effectuées sur le contenu projeté des vues avec @ContentChild() ou @ContentChildren().
  • hostBindings: cette fonction est exécutée pour mettre à jour le binding entre une propriété d’un composant et une propriété d’un élément HTML hôte avec @HostBinding() ou @HostListener().
  • viewQuery: ce code est exécuté pour mettre à jour des requêtes effectuées sur le contenu des vues avec @ViewChild() ou @ViewChildren().

La compilation est effectuée par la méthode compileComponent().

Exemple du code généré pour un composant

Pour mieux comprendre la façon dont la définition du composant est compilée puis utilisée, on va montrer un exemple à partir d’un exemple simple. Si on considère le composant suivant:

Template
<p>Example component</p> 
{{updatedValue}}
Classe du composant
import { Component, OnInit } from '@angular/core'; 

@Component({ 
    selector: 'app-example', 
    templateUrl: './example.component.html' 
})    
export class ExampleComponent implements OnInit { 
    updatedValue: string; 
 
    ngOnInit(): void { 
        this.updatedValue = 'Updated value'; 
    } 
} 

Si on affiche ce composant et si on consulte le code source généré en Javascript (on peut trouver l’implémentation correspondant à la définition du composant dans le fichier main.js):

ExampleComponent.ɵfac = function ExampleComponent_Factory(t) { return new (t || ExampleComponent)(); }; 
ExampleComponent.ɵcmp = _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵdefineComponent"]({ 
type: ExampleComponent, 
selectors: [["app-example"]], decls: 3, vars: 1, 
template: function ExampleComponent_Template(rf, ctx) { if (rf & 1) { 
        _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](0, "p"); 
        _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](1, "Example component"); 
        _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"](); 
        _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](2); 
    } if (rf & 2) { 
        _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵadvance"](2); 
        _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtextInterpolate1"]("\n", ctx.updatedValue, "\n"); 
    } }, 
encapsulation: 2 }); 
/*@__PURE__*/ (function () { _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵsetClassMetadata"](ExampleComponent, [{ 
        type: _angular_core__WEBPACK_IMPORTED_MODULE_0__["Component"], 
        args: [{ 
                selector: 'app-example', 
                templateUrl: './example.component.html' 
            }] 
    }], function () { return []; }, null); })(); 

Ce code permet de définir dans ExampleComponent.ɵcmp la définition du composant Example contenant ses metadatas et son template.

Creation Mode

Avec Ivy, de façon à augmenter les performances, les fonctions utilisées pour mettre à jour le DOM sont divisées en 2 parties:

  • Une partie permettant de modifier le DOM avec le contenu statique d’une vue: la partie statique correspond aux éléments HTML statiques d’un template. Ces éléments ne seront pas modifiés dans le cas d’un changement d’un binding.
  • Une partie permettant de mettre à jour le DOM avec le contenu dynamique d’une vue: cette partie est modifiée si un élément dynamique est mise à jour comme un binding.

Ces 2 parties des fonctions ne sont pas exécutées au même moment. Elles sont exécutées suivant la valeur de l’enum RenderFlags qui matérialise l’étendu du rendu souhaité par Ivy:

export const enum RenderFlags { 
    /* Whether to run the creation block (e.g. create elements and directives) */ 
    Create = 0b01, 
    /* Whether to run the update block (e.g. refresh bindings) */ 
    Update = 0b10 
} 

Ainsi:

  • RenderFlags.Create: ce niveau de rendu permet d’exécuter la partie statique du code des fonctions. Il est exécuté à l’initialisation d’un composant au moment du rendu de la vue.
  • RenderFlags.Update: ce niveau de rendu exécute la partie dynamique du code des fonctions. Il est exécuté à l’initialisation d’un composant et quand la détection de changements a détecté un changement nécessitant la mise à jour du DOM.

De façon à optimiser les performances, l’utlisation de ces 2 niveaux de rendu permet d’eviter d’exécuter du code inutile lors de la mise à jour du contenu dynamique d’une vue. Ainsi quand un changement est détecté et que le contenu dynamique doit être mise à jour, il est inutile d’exécuter le code permettant de mettre à jour la partie statique de la vue.

Le niveau de rendu souhaité est piloté par l’activation du mode creation (i.e. creation mode) suivant les différentes étapes d’exécution:

  • A l’initialisation d’un composant:
  • Lors de la détection de changements:

Au niveau du code Angular, les 2 niveaux de rendu sont effectués par des méthodes différentes:

  • Création contenu statique: la méthode renderView() permet d’exécuter ce niveau. Par exemple, si on se limite au cas de l’appel à la fonction template, l’appel dans renderView() est du type:
    executeTemplate(tView, lView, templateFn, RenderFlags.Create, context); 
    

    On peut voir que la fonction template templateFn est appelée avec RenderFlags.Create.

  • Mise à jour contenu dynamique: ce niveau est exécuté par la méthode refreshView(). Dans le cas de l’appel à la fonction template, on peut voir l’appel dans refreshView():
    executeTemplate(tView, lView, templateFn, RenderFlags.Update, context);
    

    La fonction template templateFn est appelée avec RenderFlags.Update.

Si on prend le code de la fonction template résultant de la compilation par Ivy du code du template du composant ExampleComponent présenté plus haut, on peut voir les 2 parties correspondant au 2 modes:

function ExampleComponent_Template(rf, ctx) { if (rf & 1) { 
        _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](0, "p"); 
        _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](1, "Example component"); 
        _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"](); 
        _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](2); 
    } if (rf & 2) {
        _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵadvance"](2); 
        _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtextInterpolate1"]("\n", ctx.updatedValue, "\n"); 
    } } 

Ainsi:

  • if (rf & 1) permet d’exécuter le code si le mode est creation (i.e. si la valeur de rf est RenderFlags.Create).
  • if (rf & 2) permet d’exécuter le code si le mode est update (i.e. si la valeur de rf est RenderFlags.Update).

Interfaçage avec le DOM

Comme on a pu le dire précédemment, Ivy permet à Angular de s’interfacer avec le DOM au moyen de méthodes qui sont compilées en même temps que la compilation de l’application. Le code généré permet de créer des nœuds dans le DOM, d’insérer de nouveaux nœuds, de mettre à jour des propriétés d’éléments se trouvant dans le DOM etc…

Partie statique

Si on prend l’exemple de la fonction template du composant ExampleComponent. Le template de ce composant contient le code:

<p>Example component</p> 
{{updatedValue}} 

Le code statique de ce template se trouve dans la fonction template dans la partie correspondant au mode creation:

_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](0, "p"); 
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](1, "Example component"); 
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"](); 
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](2); 

Si on regarde la 1ère ligne, elle permet d’exécuter la méthode ɵɵelementStart pour ajouter l’élément HTML p dans le DOM. Le code de cette méthode est:

function ɵɵelementStart(index, name, attrsIndex, localRefsIndex) { 
    const lView = getLView(); 
    const tView = getTView(); 
    // [...]     
    const native = lView[adjustedIndex] = elementCreate(name, renderer, getNamespace()); 
    // [...]     
    appendChild(tView, lView, native, tNode); 
    // [...]     
} 

La méthode ɵɵelementStart fait partie des méthodes utilisées pour s’interfacer avec le DOM. D’autres méthodes effectuent des opérations similaires permettant le rendu de la vue comme ɵɵtext pour ajouter du texte.

Le point commun de ces méthodes est de:

  • créer un élément natif avec la méthode elementCreate():
    function elementCreate(name, renderer, namespace) { 
        if (isProceduralRenderer(renderer)) { 
            return renderer.createElement(name, namespace); 
        } 
        else { 
            return namespace === null ? renderer.createElement(name) : 
                renderer.createElementNS(namespace, name); 
        } 
    } 
    
  • rajouter un nœud au DOM en utilisant la méthode appendChild() qui après d’autres appels va permettre de rajouter un élément au DOM:
    function nativeAppendChild(renderer: Renderer3, parent: RElement, child: RNode): void { 
        ngDevMode && ngDevMode.rendererAppendChild++; 
        ngDevMode && assertDefined(parent, 'parent node must be defined'); 
    
        if (isProceduralRenderer(renderer)) { 
            renderer.appendChild(parent, child);
        } else { 
            parent.appendChild(child); 
        } 
    }
    

Partie dynamique

Dans le cadre de la partie dynamique de la fonction template, on peut voir que le code permet de mettre à jour des propriétés dans le DOM. Par exemple en prenant la partie de la fonction template correspondant au mode update:

_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵadvance"](2); 
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtextInterpolate1"]("\n", ctx.updatedValue, "\n");

On peut voir que cette partie permet de mettre à jour le texte interpolé correspondant au code:

{{updatedValue}} 

La 2e ligne permet de faire appel à la méthode ɵɵtextInterpolate1 qui, par suite, permettra d’affecter la valeur dans l’élément du DOM correspondant:

export function textBindingInternal(lView: LView, index: number, value: string): void { 
    // [...] 
    const renderer = lView[RENDERER]; 

    isProceduralRenderer(renderer) ? renderer.setValue(element, value) : element.textContent = value; 
} 

D’autres méthodes permettent d’effectuer des traitements similaires comme par exemple:

Fonctionnement de la détection de changements

Dans cette partie, on va détailler le fonctionnement d’Angular de façon à expliquer quelques subtilités de la détection de changements telle qu’elle fonctionne avec Ivy.

Comment détecter les changements ?

On pourrait se demander comment détecter les changements dans le contenu dynamique d’un composant. Intuitivement, 2 méthodes nous paraissent possibles:

  • 1ère méthode: dès qu’on effectue un changement sur une valeur, on indique explicitement que sa valeur a changé. L’inconvénient de cette méthode est qu’elle nécessite d’indiquer explicitement les changements et qu’il est plus difficile de l’automatiser.
  • 2e méthode: à chaque modification d’une valeur, on compare la valeur actuelle avec la valeur précédente pour savoir si la valeur a changé. Si la valeur est la même, il n’y a pas eu de changement; si la valeur est différente alors un changement est détectée. L’avantage de cette méthode sur la 1ère est qu’elle est facilement automatisable, il n’est pas nécessaire d’indiquer explicitement le changement de valeur. Son inconvénient majeur est qu’elle est plus complexe à mettre en œuvre.

Avec Angular, il est possible d’utiliser les 2 méthodes. On peut exécuter la détection de changements explicitement dans le cas où on désactive volontairement la détection automatique de changements ou si la détection automatique n’est pas suffisante. Une méthode pour solliciter explicitement la détection de changements est de faire appel à l’objet ChangeDetectorRef (le détail de cette méthode se trouve dans Personnaliser la détection de changements).

La 2e méthode est celle adoptée par Angular pour détecter les changements de façon automatique. A chaque fois qu’un évènement survient, Angular déclenche une méthode qui effectuera la détection de changements sur tous les éléments dynamiques des vues. Le changement est détecté en comparant les valeurs actuelles avec les valeurs précédentes, si les valeurs sont différentes alors des mises à jour des éléments correspondant dans le DOM sont effectuées.

Pour optimiser le traitement et éviter d’effectuer des mises à jour inutiles, seules les valeurs ayant été modifiées entraînent une mise à jour d’une propriété dans le DOM.

Ainsi pour résumer, la détection de changement repose sur 3 mécanismes:

  • Le déclenchement de la détection de changements,
  • La comparaison des valeurs pour savoir si la valeur a changé,
  • La mise à jour du DOM dans le cas où la valeur a changé.

Déclenchement de la détection de changements

Comme on l’a indiqué plus haut, les évènements qui permettent de déclencher la détection de changements sont exécutés par Zone.js.

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

Template
<p>Click example</p>
Classe du composant
import { Component, HostListener } from '@angular/core'; 

@Component({ 
    selector: 'app-example', 
    templateUrl: './example.component.html' 
}) 
 export class ExampleComponent { 
    @HostListener('click') onHostClicked(triggeredEvent) : void {  
        console.log(triggeredEvent); 
    } 
} 

Si on place un point d’arrêt dans la méthode ApplicationRef.tick() se trouvant dans le fichier core.js, on peut voir que son exécution est délenchée à chaque fois qu’on clique sur le texte Click example.

La méthode tick() est appelée pour chaque évènement provenant de Zone.js. En suivant la pile d’appels tick()tickRootContext()renderComponentOrTemplate(), on peut voir que cette méthode appelle renderView() et refreshView() qui sont les méthodes permettant la détection de changements:

export function renderComponentOrTemplate<T>( 
    tView: TView, lView: LView, templateFn: ComponentTemplate<{}>|null, context: T) { 
    // [...] 

    if (creationModeIsActive) { 
        renderView(tView, lView, context);
    } 
    refreshView(tView, lView, templateFn, context);

    // [...] 
} 

Comme on l’a indiqué précédemment, renderView() et refreshView() permettent d’exécuter la création et la mise à jour de la vue. Le booléen creationModeIsActive matérialise l’activation du mode Creation permettant la création des éléments statiques de la vue par renderView().

Dans le cadre d’une exécution en mode update, refreshView() peut aussi être appelé par detectChanges()detectChangesInternal():

export function detectChangesInternal<T>(tView: TView, lView: LView, context: T) { 
    const rendererFactory = lView[RENDERER_FACTORY]; 
    if (rendererFactory.begin) rendererFactory.begin(); 
    try { 
        refreshView(tView, lView, tView.template, context);
    } catch (error) { 
        handleError(lView, error); 
        throw error; 
    } finally { 
        if (rendererFactory.end) rendererFactory.end(); 
    } 
} 

Comparaison des valeurs

Dans le code d’Angular, on peut voir régulièrement des objets de type LView et TView. Ces 2 objets contiennent toutes les données relatives aux vues:

  • LView contient toutes les données nécessaires à l’exécution des traitements concernant un template. Il existe une instance de type LView pour chaque vue d’un composant et pour chaque vue intégrée (i.e. embedded view).
  • TView contient les données statiques partagées par tous les templates correspondant à un type donné. Le code permet de voir les fonctions qui sont compilées et générées par Ivy et qui sont exécutées lors du rendu d’une vue ou de la mise à jour des éléments dynamiques d’une vue:
    export interface TView { 
        // [...] 
        type: TViewType; 
        readonly id: number; 
    
        // [...] 
        template: ComponentTemplate<{}>|null; 
        viewQuery: ViewQueriesFunction<{}>|null; 
        node: TViewNode|TElementNode|null; 
    
        // [...] 
        expandoInstructions: ExpandoInstructions|null; 
        staticViewQueries: boolean; 
        queries: TQueries|null; 
        contentQueries: number[]|null; 
    
        // [...] 
        preOrderHooks: HookData|null; 
        preOrderCheckHooks: HookData|null; 
        contentHooks: HookData|null; 
        contentCheckHooks: HookData|null; 
        viewHooks: HookData|null; 
        viewCheckHooks: HookData|null; 
        destroyHooks: DestroyHookData|null; 
    
        // [...] 
        components: number[]|null; 
    }
    

Les comparaisons des valeurs sont indirectement effectuées par refreshView(). Sans rentrer dans le détail d’implémentation de cette méthode, elle permet de déclencher l’exécution de toutes les fonctions permettant de mettre à jour des éléments dynamiques d’une vue susceptible d’en modifier le contenu comme par exemple:

  • Un property binding,
  • Un attribute binding,
  • Une interpolation,
  • Le changement d’un paramètre d’entrée d’un composant (de type Input),
  • Une propriété d’un élément hôte natif (différent de Input),
  • L’évaluation d’une expression,
  • Etc…

Par suite, toutes ces méthodes déclenchent les méthodes générées par Ivy correspondant à la mise à jour des éléments du template en mode update. Lors de cette mise à jour, la fonction bindingUpdated() est déclenchée pour vérifier si les bindings sont mis à jour ou non:

export function bindingUpdated(lView: LView, bindingIndex: number, value: any): boolean { 
    ngDevMode && assertNotSame(value, NO_CHANGE, 'Incoming value should never be NO_CHANGE.'); 
    ngDevMode && 
    assertLessThan(bindingIndex, lView.length, `Slot should have been initialized to NO_CHANGE`); 

    const oldValue = lView[bindingIndex]; 
    if (Object.is(oldValue, value)) { 
        return false; 
    } else { 
        // [...] 
        lView[bindingIndex] = value; 
        return true; 
  } 
}

On peut voir que cette fonction récupère l’ancienne valeur avec:

const oldValue = lView[bindingIndex]; 

Elle effectue une comparaison entre la valeur actuelle et l’ancienne valeur pour détecter le changement:

Object.is(oldValue, value) 

Enfin en cas de changement, la valeur sauvegardée est remplacée par la nouvelle valeur:

lView[bindingIndex] = value; 

La valeur de retour de la fonction est true si une mise à jour du binding a été détectée. Cette fonction n’effectue pas elle-même la modification des propriétés dans le DOM. Ces modifications sont effectuées par les méthodes générées par Ivy seulement dans le cas où un changement est détecté.

Implémentation de la détection de changements

Comme le suggère l’implémentation qu’on a pu décrire plus haut, le mécanisme de détection de changements est lié à l’interfaçage avec le DOM lors de l’initialisation d’un composant ou lors de sa mise à jour. Il n’est pas possible de s’interfacer avec le DOM sans exécuter la détection de changements.

Même si la compréhension de la détection de changements n’est pas nécessaire pour implémenter des composants avec Angular, elle permet de mieux appréhender le lien entre les différents mécanismes suivants:

  • Création des objets,
  • Déclenchement des callbacks du cycle de vie d’un composant,
  • Rendu d’un vue,
  • Mise à jour du contenu dynamique d’une vue,
  • Lien entre un composant et ses composants enfant.

Tous ces mécanismes sont implémentés dans les 2 méthodes:

Le but de cette partie est d’expliciter ces 2 méthodes pour comprendre davantage le lien entre ces différents mécanismes.

Composant parent et composant enfant

Expliciter le fonctionnement des méthodes renderView() et refreshView() n’est pas facile car ces méthodes sont appelées récursivement par les composants parent sur leur composant enfant. Pour un composant donné, la moitié des traitements est effectuée par des appels provenant de l’exécution de renderView() ou refreshView() de son composant parent.

L’imbrication d’un composant avec ses parents est valable pour tous les composants car même le composant root de l’application possède un parent.

Création de contenu statique

Le contenu statique est créé par la méthode renderView(). Elle n’est exécutée que lors de l’initialisation d’un composant. Comme le contenu statique d’une vue n’est pas modifié dans la suite de l’exécution, la méthode ne sera plus sollicitée.

Cette méthode peut prêter à confusion car certains appels sont effectués avec RenderFlags.Update.

Ainsi la création du contenu statique est effectuée en exécutant les étapes suivantes:

  1. Exécution de la fonction contentQueries avec RenderFlags.Create: cette fonction permet de créer d’autres fonctions qui permettront d’effectuer des bindings entre le composant et sa vue en effectuant des requêtes sur du contenu projeté par l’intermédiaire de @ContentChild() ou @ContentChildren().
    Cet appel n’est pas effectué dans le corps de la méthode renderView() du composant mais dans celui de son parent. Il est effectué lorsque le constructeur du composant parent est exécuté. Cet appel n’effectue pas de mise à jour du DOM, il ne fait que créer de nouveaux objets nécessaires aux traitements futurs.
  2. Exécution de la fonction viewQuery avec RenderFlags.Create: crée d’autres fonctions qui effectueront des bindings entre le composant et sa vue quand on utilise @ViewChild() ou @ViewChildren(). Cet appel n’effectue pas de création ou de mise à jour d’objets dans le DOM.
  3. Exécution de la fonction template avec RenderFlags.Create: cet appel est le plus important que c’est celui qui permettra de créer tous les objets correspondant au contenu statique de la vue en créant les éléments natifs dans le DOM.
  4. Exécution du constructeur du composant: lorsque la fonction template est exécutée et au premier appel d’une méthode qui crée un objet où le code du composant est nécessaire, le composant est instancié en exécutant son constructeur.
  5. Exécution de la fonction contentQueries avec RenderFlags.Update: cet appel peut prêter à confusion car il est effectué avec RenderFlags.Update. Il concerne les requêtes effectuées sur du contenu projeté se trouvant sur la vue du composant avec @ContentChild() ou @ContentChildren() dans le cas où ils utilisent l’option static: true. Cette option va imposer aux requêtes d’être effectuées au moment de la création de la vue.
    Cet appel est effectué avec RenderFlags.Update car les objets permettant d’effectuer la requête sont déjà créés (point 1). Cet appel met à jour les résultats des requêtes après création des éléments statiques de la vue.
  6. Exécution de la fonction viewQuery avec RenderFlags.Update: comme l’appel précédent, cet appel est effectué avec RenderFlags.Update car les objets permettant de requêter la vue existe déjà (point 2). L’appel concerne les requêtes effectuées sur la vue avec @ViewChild() ou @ViewChildren() s’ils utilisent l’option static: true.
Légende du schéma

Mise à jour du contenu dynamique

Le contenu dynamique d’une vue d’un composant est mis à jour lors de l’initialisation et à chaque exécution du mécanisme de détection de changements. La méthode qui effectue ce traitement est refreshView().

Cette méthode est plus complexe que renderView() car beaucoup de traitements effectués concernant le composant courant sont effectués lors de l’exécution de refreshView() dans le cadre son parent.

Les étapes principales de refreshView() sont:

  1. Exécution des callbacks ngOnChanges(), ngOnInit() et ngDoCheck(): ces callbacks sont exécutées lors de l’exécution de refreshView() du composant parent.
    La callback ngOnInit() est exécutée seulement à l’initialisation du composant.
  2. Exécution de la fonction contentQueries avec RenderFlags.Update: cette fonction permet de mettre à jour les requêtes effectuées sur le contenu projeté de la vue avec @ContentChild() ou @ContentChildren().
    L’appel de cette fonction est effectué lors de l’exécution de refreshView() du composant parent.
  3. Exécution des callbacks ngAfterContentInit() et ngAfterContentChecked(): l’exécution de ces callbacks vient clôturer la mise à jour des requêtes effectuées sur le contenu projeté. Ce contenu projeté provient du parent. La vue correspondante du parent a été rendu après exécution de la fonction template du parent. Au moment d’effectuer les requêtes sur le contenu projeté, la vue de son parent est donc à jour.
    La callback ngAfterContentInit() est exécutée seulement à l’initialisation du composant.
  4. Exécution de la fonction hostBindings avec RenderFlags.Update: cette fonction permet d’affecter les bindings provenant de l’élement HTML hôte avec @HostBinding(). Les éléments hôte font partie des éléments statiques et sont créés lors de l’exécution de la fonction template avec RenderFlags.Create donc à ce moment de l’exécution, ils ont déjà été créés.
  5. Exécution de la fonction template avec RenderFlags.Update: cette étape est la 1ère effectuée dans le corps de la méthode refreshView() du composant courant. Elle servira à mettre à jour le DOM avec tous les éléments dynamiques de la vue.
  6. Exécution des callbacks ngOnChanges(), ngOnInit() et ngDoCheck() dans les composants enfant.
  7. Exécution de refreshView() pour les vues intégrées (i.e. embedded views).
  8. Exécution de la fonction contentQueries() dans les directives enfant: cette étape permet de mettre à jour les requêtes sur le contenu projeté des directives enfant. Cette étape est possible car le contenu projeté a été rendu lors de l’exécution de la fonction template du composant.
  9. Exécution des callbacks ngAfterContentInit() et ngAfterContentChecked() des composants enfant.
  10. Exécution de la fonction hostBindings des directives enfant.
  11. Exécution de la méthode refreshView() des composants enfants.
Légende du schéma
  1. Exécution de la fonction viewQuery avec RenderFlags.Update: cette étape permet de mettre à jour les requêtes effectuées sur la vue du composant avec @ViewChild() et @ViewChildren. La vue du composant, les vues des composants enfant et les view containers des vues intégrées (i.e. embedded views) ont été rendus au début de l’exécution de cette fonction. Les requêtes sont donc effectuées sur des vues à jour.
  2. Exécution des callbacks ngAfterViewInit() et ngAfterContentChecked() des composants enfant: cette appel termine l’exécution de la méthode refreshView() du composant courant.
  3. Exécution des callbacks ngAfterViewInit() et ngAfterContentChecked(): cet appel est effectué dans le corps de la méthode refreshView() du composant parent.
    La callback ngAfterViewInit() est exécutée seulement à l’initialisation du composant.

Cette description permet de se rendre compte de l’imbrication d’un composant avec son parent. Beaucoup de traitements sont effectués par le parent en dehors du corps de la méthode refreshView() du composant courant.

“No Changes Mode”

Dans le code de la méthode refreshView(), on peut voir régulièrement des références au “No Changes Mode” par l’intérmédiaire d’un appel à la fonction getCheckNoChangesMode() pour récupérer un booléen indiquant l’activation ou non de ce mode.

A chaque détection de changements, la méthode refreshView() est exécuté 2 fois:

  • Une fois avec le mode “No Changes” désactivé: il s’agit de l’exécution normal de la méthode refreshView() pour effectuer la détection de changements et pour modifier le contenu dynamique de la vue.
  • Une 2e fois avec le mode “No changes” activé: cette exécution est effectuée seulement avec la configuration de développement. Elle permet d’exécuter la détection de changements une 2e fois sans modifier le contenu des vues en s’il y a de nouveaux changements détectés. Au cours d’une exécution idéale, il ne doit pas y avoir de changements détectés à la fin d’une exécution du mécanisme. Tous les changements doivent avoir été détectés. Cette 2e exécution vise à vérifier si tous les changements ont effectivement été détectés. Si de nouveaux changements sont détectés alors il y a une erreur d’implémentation car ces changements n’entraîneront pas une modification de la vue.

Dans le cas où un changement est détecté lors de la 2e exécution, une erreur de type “ExpressionChangedAfterItHasBeenCheckedError” est lancée.

L’appel est de ces 2 modes est effectué dans la méthode tick() appelée dans la callback exécutée par Zone.js:

tick(): void { 
    // [...] 
    for (let view of this._views) { 
        view.detectChanges(); 
    } 
    if (this._enforceNoNewChanges) { 
        for (let view of this._views) { 
            view.checkNoChanges();
        } 
    } 
    // [...] 
  } 

Dans ce code:

  • view.detectChanges() permet de lancer la 1ère exécution du changement de détection.
  • view.checkNoChanges() lance la 2e exécution en mode “No Changes”.

Pour résumer

Si on simplifie les schémas plus haut pour ne considérer que les étapes concernant le composant courant, on obtient le schéma suivant:

Légende du schéma

Ce schéma permet de se rendre compte de la succession des étapes de création et de mise à jour des éléments dans le DOM et l’exécution des callbacks du cycle de vie d’un composant. Ainsi, si un élément dyamique est modifié après sa mise à jour dans le DOM, on peut comprendre pourquoi il ne sera pas mis à jour dans la vue d’un composant.

Par exemple, si on modifie la valeur d’un binding lors de l’exécution des callbacks ngAfterViewInit() ou ngAfterViewChecked(), étant donné que la fonction template permettant de mettre à jour le DOM a été exécuté avant, il ne sera pas possible de mettre à jour la vue seulement avec le mécanisme de détection automatique de changements.

Il en est de même pour l’exécution de requête de vue et de contenu projeté qui intervient à des moments précis avant et après le rendu. Le résultat de ces requêtes peut être différent de celui attendu suivant si le rendu a été effectué ou non.

Références

Ivy:

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Les composants enfant (Angular)

@jplenio

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.

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 contiendra ChildComponent.

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
<h3>Child component</h3>
Classe du composant
@Component({
    selector: 'app-child',
    templateUrl: './child.component.html'
})
export class ChildComponent {}

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.

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
<h1>Parent component</h1> 
<app-child></app-child> 
Classe du composant
import { Component } from '@angular/core'; 

@Component({
    templateUrl: './parent.component.html' 
}) 
 
export class ParentComponent { 
    innerValue = 'Value defined in parent'; 
} 

Et si l’implémentation du composant enfant est:

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

Dans le composant enfant, on peut ainsi accèder à l’instance de son parent.

Double dépendance = couplage fort

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 de ParentComponent et
  • forwardRef() pour faire suivre la référence vers le type du composant parent.

L’implémentation du composant parent devient:

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(); 
    } 
} 

L’implémentation du composant enfant devient:

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

@Component({ 
    selector: 'app-child', 
    templateUrl: './child.component.html' 
}) 
export class ChildComponent { 
    constructor(valueHandler: ValueHandler) { 
        console.log(valueHandler.innerValue); 
    }
} 

Dans le composant enfant, la référence vers le composant parent a disparu.

On ne peut pas utiliser d’interfaces

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 de MiddleComponent et
  • MiddleComponent est un composant enfant de ParentComponent.

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); 
    } 
} 
Paramètre optionel

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 et
  • ChildComponent est le composant enfant de ParentComponent.

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
<p>Child component with identifier: {{identifier}}</p>
Classe du composant
import { Component, Input } from '@angular/core';

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

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

Ainsi:

  • On rajoute la propriété childIdentifier dans le classe du composant ParentComponent 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
<p>Child component with identifier: {{identifier}}</p>
Classe du composant
import { Component, Input } from '@angular/core';

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

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 de internalCount.
  • Dans la classe du composant enfant (child.component.ts): on ajoute la propriété internalCount et on déclare l’évènement countUpdated 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 type EventEmitter 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.
  • 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
<h1>Parent component</h1>
<app-child identifier='1'></app-child>
Classe du composant
import { Component } from '@angular/core';

@Component({
    templateUrl: './parent.component.html',
    styleUrls: ['./parent.component.css']
})
export class ParentComponent {}

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
<h3>Child component {{identifier}}</h3>
Classe du composant
import { Component } from '@angular/core';

@Component({
    templateUrl: './child.component.html',
    styleUrls: ['./child.component.css']
})
export class ChildComponent {
    @Input() identifier: number;
}

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 de ParentComponent et
  • ChildComponent est un composant enfant de MiddleComponent.

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 de MiddleComponent est 'app-middle' et
  • Le selector de ChildComponent 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 de ChildComponent.
  • Le contenu de <h4></h4> est projeté dans la partie <ng-content select='h4'></ng-content> du template de ChildComponent.

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

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:

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 (dans caller.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 (dans library.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 dans LibraryComponent et
  • Dans ProjectedComponent, l’instance provenant de LibraryComponent puisque ProjectedComponent est projeté dans LibraryComponent.

On modifie LibraryComponent pour instancier une nouvelle classe Dependency et ajouter le paramètre providers à ce niveau:

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'],
  providers: [ { provide: Dependency, useValue: dependency} ]
})
export class LibraryComponent {
  constructor(public dependency: Dependency) {}
}

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 dans LibraryComponent (pas de changement) et
  • Dans ProjectedComponent, l’instance provenant du module car viewProviders permet de limiter l’injection au contenu non projeté. ProjectedComponent étant projeté dans LibraryComponent, l’instance de Dependency injectée sera celle du module.

On modifie LibraryComponent pour utiliser viewProviders au lieu de providers:

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'],
  viewProviders: [ { provide: Dependency, useValue: dependency} ]
})
export class LibraryComponent {
  constructor(public dependency: Dependency) {}
}

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 de Dependency est celle du module à cause du paramètre viewProviders au niveau de LibraryComponent.
  • Dans l’instance de ProjectedComponent qui fait partie du contenu non projeté, l’instance injectée de Dependency est celle de LibraryComponent.

On modifie seulement le fichier template de LibraryComponent pour ajouter ProjectedComponent en tant composant enfant:

Template
<p><b>LibraryComponent</b></p>
<p>{{dependency.message}}</p>

Projected content from outside:
<ng-content></ng-content>

Child:
<app-projected>
    <span>Projected content from LibraryComponent</span>
</app-projected>
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'],
  viewProviders: 
    [ { provide: Dependency, useValue: dependency} ]
})
export class LibraryComponent {
  constructor(public dependency: Dependency) {}
}

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:

  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().

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()
  1. ParentComponent.ngOnChanges()
  2. ParentComponent.ngOnInit()
  3. ParentComponent.ngDoCheck()
  4. ParentComponent.ngAfterContentInit()
  5. ParentComponent.ngAfterContentChecked()
Exécution des callbacks du composant enfant y compris ngAfterViewInit() et ngAfterViewChecked()
  1. ChildComponent.ngOnChanges()
  2. ChildComponent.ngOnInit()
  3. ChildComponent.ngDoCheck()
  4. ChildComponent.ngAfterContentInit()
  5. ChildComponent.ngAfterContentChecked()
  6. ChildComponent.ngAfterViewInit()
  7. ChildComponent.ngAfterViewChecked()
Exécution des callbacks restantes du parent
  1. ParentComponent.ngAfterViewInit()
  2. ParentComponent.ngAfterViewChecked().

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()
  1. ParentComponent.ngOnChanges()
  2. ParentComponent.ngDoCheck()
  3. ParentComponent.ngAfterContentChecked()
Exécution des callbacks du composant enfant
  1. ChildComponent.ngOnChanges()
  2. ChildComponent.ngDoCheck()
  3. ChildComponent.ngAfterContentChecked()
  4. ChildComponent.ngAfterViewChecked()
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è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>
    

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() et EventEmitter:

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

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:
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Les directives Angular

@gersonrepreza

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

L’affichage obtenu avec cet exemple est:

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

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

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

Le résultat devient:

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

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

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

Créer une directive avec le CLI Angular

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

ng generate directive <nom de la directive>  

Ou

ng g d <nom de la directive>

Cette commande crée:

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

Configuration des directives

Paramètre selector

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

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

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

La syntaxe de la valeur peut être:

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

    selector: 'custom-directive'
    

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

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

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

    <span custom-directive></span>
    

    L’attribut peut comporter une valeur:

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

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

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

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

  • Classe CSS:
    Par exemple:

    selector: '.custom-directive' 
    

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

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

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

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

    selector: '[title=custom-directive]'
    

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Paramètre inputs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Utilisation d’un alias

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@Input()

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

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

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

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

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

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

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

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

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

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

Utilisation d’un alias

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

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

Par exemple:

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

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

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

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

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

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

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

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

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

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

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

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

Paramètre outputs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  constructor() { 
    this.clickCount = 0; 
  } 

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

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

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

Utilisation d’un alias

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

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

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

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

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

  // ... 
} 

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

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

@Output()

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

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

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

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

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

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

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

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

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

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

Utilisation d’un alias

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

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

Par exemple:

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

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

  // ... 
} 

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

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

Paramètre providers

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

@HostBinding()

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

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

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

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

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

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

Le résultat est du type:

@HostListener()

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

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

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

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

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

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

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

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

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

Attribute directives

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

ElementRef et Renderer2

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

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

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

Ainsi:

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

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

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

Exemple de directive attribut

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

L’implémentation de la directive est:

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

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

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

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

L’implémentation du composant est:

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

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

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

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

Ainsi:

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

Le résultat est:

Structural directives

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

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

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

<ng-template>

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

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

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

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

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

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

Affichage Code HTML généré

View container

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

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

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

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

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

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

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

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

Dans ce code:

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

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

Affichage Code HTML généré

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

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

  constructor(private parentViewRef: ViewContainerRef) {} 

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

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

Affichage Code HTML généré

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ainsi:

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

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

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

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

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

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

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

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

Le résultat est:

$implicit

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

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

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

La valeur des variables:

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

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

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

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

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

Le contexte doit comporter une propriété implicite:

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

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

ngTemplateOulet et ngTemplateOutletContext

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

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

Ainsi, si on écrit:

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

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

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

Le résultat est:

Affichage Code HTML généré

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

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

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

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

Utiliser ngTemplate avec une directive

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

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

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

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

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

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

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

Dans cet exemple:

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

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

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

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

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

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

Pour le composant hôte:

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

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

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

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

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

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

L’implémentation du composant hôte:

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

Implémentation d’une directive structurelle

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

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

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

    Par exemple, si on utilise la syntaxe suivante:

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

    Elle est équivalent à:

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

Configurer une directive structurelle par microsyntaxe

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

Par exemple:

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

Est équivalent à:

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

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

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

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

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

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

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

    Ainsi, si on écrit:

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

    Cette implémentation est équivalente à:

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

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

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

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

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

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

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

    Est équivalent à:

    *testDirective="3+4 let content" 
    

Les opérations de la microsyntaxe permettent de:

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

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

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

Par exemple:

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

Est équivalent à:

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

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

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

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

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

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

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

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

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

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

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

Ainsi si on écrit:

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

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

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

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

L’implémentation de la directive est:

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

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

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

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

This is the directive input content.

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

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

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

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

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

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

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

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

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

<expression> as <nom variable locale> 

Ainsi as s’utilise de 3 façons:

Ou

Ou

Par exemple, si on écrit:

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

La microsyntaxe permet d’effectuer 2 opérations:

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

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

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

L’implémentation de la directive est:

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

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

Le résultat est:

Template content: This is the directive input content. 

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

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

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

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

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

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

ViewContainerRef

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

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

Exemple de directive structurelle

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

L’implémentation de la directive est:

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

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

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

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

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

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


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

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

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

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

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

Ainsi, dans le template du composant hôte:

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

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

Pour résumer…

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

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

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

ng g c <nom de la directive>

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

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

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

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

ou

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

ou

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

ou

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

ou

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

ou

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

ou

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

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

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

    Par exemple:

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

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

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

    Par exemple:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

La microsyntaxe doit satisfaire certaines règles:

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

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

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

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

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

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

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

Documentation Angular:

Code source d’Angular:

Autres:

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Injection de dépendances dans une application Angular

L’injection de dépendances est un design pattern pris en charge nativement dans Angular.
Pour davantage d’explications sur la théorie de ce design pattern, voir L’injection de dépendances en théorie.

A l’instanciation d’un composant, Angular peut effectuer la résolution de ces dépendances puis de les injecter en utilisant le constructeur du composant. 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 peuvent être des classes ou des services.

On va illustrer quelques exemples d’injection de dépendances en utilisant différents mécanismes. On terminera par présenter les services qui sont les éléments le plus souvent injectés.

@jeremybishop

Notion de “provider”

Quand un objet est injecté, il faut pouvoir déterminer si on doit utiliser une nouvelle instance ou une instance déjà existante. Ce mécanisme est effectué par le framework d’injection de dépendances qui va configurer des injecteurs en fonction de la façon dont les objets sont déclarés. Une déclaration importante concerne le provider de dépendances (i.e. fournisseur de dépendances) car il permettra de configurer les injecteurs.

En fonction de l’endroit où le provider sera déclaré, l’injecteur utilisé pourra instancier la dépendance. Il est possible de déclarer des providers dans les metadatas des modules ou des composants:

  • Au niveau d’un composant: avant chaque exécution du constructeur du composant, la dépendance sera résolue et l’injecteur créera une nouvelle instance de l’objet injecté.
    L’injecteur utilisé est de type ElementInjector, une instance de cet injecteur est créé pour chaque composant. Pour injecter une dépendance, si l’injecteur du composant ne réussit pas à la résoudre, il va appeler les injecteurs des composants parents s’ils existent sinon il va appeler l’injecteur du module parent.
  • Au niveau d’un module: si le module est importé, le provider sera aussi importé et l’injecteur utilisera la même instance de l’objet à tous les endroits où il est injecté.
    L’injecteur utilisé est de type ModuleInjector, une instance de cet injecteur est créée pour chaque module. Si l’ElementInjector du composant échoue à résoudre une dépendance, l’injecteur du module auquel appartient le composant sera appelé pour essayer d’instancier la dépendance.
  • Au niveau de l’application: il est possible de préciser un provider directement au niveau de l’élément à injecter en utilisant le paramètre providedIn de la directive @Injectable(). Dans ce paramètre on peut indiquer la valeur 'root' pour désigner le module root. Si le provider est le module root alors la résolution de la dépendance sera effectuée par l’injecteur du module root.

La déclaration d’un provider peut se faire de différentes façons:

  • Paramètre providers d’un composant ou d’un module: c’est la méthode le plus ancienne. L’inconvénient de cette méthode est qu’elle ne permet pas de tirer partie de la fonctionnalité tree-shaking.
    Si le provider est un module, l’instanciation de la classe à injecter est liée à celle du module:

    • Eagerly-loaded module: si le module est chargé au démarrage de l’application, alors la classe à injecter sera instanciée sous forme d’un singleton qui sera disponible dans toute l’application.
    • Lazy-loaded module: si le module est chargé par chargement différé, l’instance de la classe à injecter ne sera disponible que si le module est chargé. Si un élément tente d’utiliser une instance de la classe par injection alors que le module n’est pas chargé il y aura une erreur de ce type dans la console du browser:
      NullInjectorError: No provider for <nom de la classe à injecter>!
      

    Dans le cas d’un composant, l’instance de la classe à injecter sera liée à la durée de vie du composant c’est-à-dire qu’à chaque fois que le constructeur du composant est exécuté une nouvelle instance de la classe est créée.

    Cette méthode permet aux composants et modules de désigner explicitement les objets dont ils auront la charge toutefois il peut préter à confusion si on déclare la classe à injecter dans le paramètre providers de plusieurs éléments.

  • Paramètre providedIn de la directive @Injectable(): on peut indiquer 'root' pour désigner le module root ou directement le module qui sera le provider. Cette méthode est plus récente que la précédente et elle permet de tirer du tree-shaking c’est-à-dire que si l’élément injectable n’est injecté nul part, il ne sera pas inclus dans le bundle obtenu après compilation.
    Cette méthode est la plus simple et convient dans la grand majorité des cas. Elle est simple à mettre en oeuvre car elle permet d’indiquer directement que l’instance de la classe à injecter est valable pour toute l’application dans le cas de l’utilisation de la valeur 'root'.

    L’inconvénient de cette méthode est qu’elle peut rendre plus difficile la réutilisation d’un service si on souhaite utiliser des valeurs différentes de providedIn. En outre cette méthode ne permet de désigner seulement des modules et non des composants en tant que provider (i.e. le paramètre providedIn ne peut être renseigné que par un module et non un composant).

    Dans le cas d’un module chargé par lazy-loading, la classe est instanciable seulement si le module est chargé dans les 2 types de configuration de providedIn: 'root' ou le type du module.

Tree-shaking

Le tree-shaking est une optimisation qui permet de ne pas inclure dans le bundle obtenu après compilation, les éléments qui sont inutiles.
Ainsi, si un élément se trouve dans le paramètre providers d’un composant ou d’un module alors qu’il n’est pas injecté, il sera quand même inclus dans le bundle.
A l’opposé, si le paramètre providedIn du décorateur @Injectable() est utilisé pour une classe à injecter, elle sera présente dans le bundle seulement si elle est effectivement injectée.

Pour résumer:

Type de déclaration Tree-shaking ? Elément fournissant la classe à injecter Type de module Type d’instance de l’objet injecté Commentaire
providers Non @Component() N/A Nouvelle instance à chaque utilisation du composant L’instance de la classe est accessible dans le composant, dans son template et, le cas échéant, dans le composant effectuant le rendu s’il y a un paramètre selector.
@NgModule() root Singleton L’instance de la classe est injectable partout dès le démarrage de l’application.
Eagerly-loaded
Lazy-loaded L’instance de la classe n’est utilisable que si le module est chargé sinon il se produit une erreur.
providedIn Oui 'root' N/A L’instance de la classe est injectable partout dès le démarrage de l’application.
<type du module> Eagerly-loaded
Lazy-loaded L’instance de la classe n’est utilisable que si le module est chargé sinon il se produit une erreur.

Exemples d’implémentation pour indiquer le “provider”

En utilisant le paramètre “providers”

Dans @NgModule()

Si on déclare le provider en utilisant le paramètre providers des metadatas d’un module, la déclaration est:

@Injectable()
export class Dependency {}
 
@NgModule({
  providers: [ Dependency ]
})
export class CustomModule {}

L’instance injectée est un singleton.

Dans @Component()

Si le provider est déclaré avec le paramètre providers des metadatas d’un composant, la déclaration est:

@Injectable()
export class Dependency {}
 
@Component({
  providers: [ Dependency ]
})
export class CustomComponent {}

Une nouvelle instance est injectée à chaque exécution du constructeur.

Options utilisables avec le paramètre “providers”

Le paramètre providers permet d’ajouter des options pour perfectionner l’injection.

Pour les exemples suivants, on utilise les classes:

@Injectable()
class FirstDependency {
  innerId = 'first';
}
 
@Injectable()
class SecondDependency {
  innerId = 'second';
}

Le membre innerId permet d’identifier l’instance pendant le débug.

useClass
L’option useClass permet d’indiquer le type de la classe qui sera injectée dans le but de remplacer le type injecté.

Dans cet exemple, le paramètre secondDependency contiendra une instance de la classe FirstDependency. L’instance sera différente de celle du paramètre firstDependency:

@Component({
  templateUrl: './custom.component.html',
  providers: [
      FirstDependency,
      { provide: SecondDependency, useClass: FirstDependency }
  ]
})
export class CustomComponent {
  constructor(private firstDependency: FirstDependency,
    private secondDependency: SecondDependency) {
      console.log('firstDependency: ' + firstDependency.innerId);
      console.log('secondDependency: ' + secondDependency.innerId);
  }
}

Le type SecondDependency est remplacé par le type FirstDependency.

useExisting
L’option useExisting permet d’utiliser une instance déjà fournie par le provider.

Par exemple, si on configure le paramètre providers de cette façon:

@Component({
  templateUrl: './custom.component.html',
  providers: [
      FirstDependency,
      { provide: SecondDependency, useExisting: FirstDependency }
  ]
})
export class CustomComponent {
  constructor(private firstDependency: FirstDependency,
    private secondDependency: SecondDependency) {
      console.log('firstDependency: ' + firstDependency.innerId);
      console.log('secondDependency: ' + secondDependency.innerId);
  }
}

Les 2 paramètres firstDependency et secondDependency contiennent la même instance de type FirstDependency.

useValue
L’option useValue permet d’utiliser explicitement une instance. Si on configure le paramètre providers de cette façon:

@Injectable()
class SecondDependency {
  constructor(public innerId: string) {}
}

const dependency = new SecondDependency('other');

@Component({
  templateUrl: './custom.component.html',
  providers: [
      FirstDependency,
      { provide: SecondDependency, useValue: dependency }
  ]
})
export class CustomComponent {
  constructor(private firstDependency: FirstDependency,
    private secondDependency: SecondDependency) {
      console.log('firstDependency: ' + firstDependency.innerId);
      console.log('secondDependency: ' + secondDependency.innerId);
  }
}

Le paramètre SecondDependency contiendra l’instance créée au préalable.

useFactory

L’option useFactory permet d’utiliser une fonction factory pour instancier un objet à injecter dans le cas où l’instanciation nécessite l’implémentation d’une logique. La fonction factory peut comporter des arguments qui seront injectés.

Par exemple, si on redéfinit la classe SecondDependency de cette façon:

@Injectable()
class SecondDependency {
  innerId: string;
 
  constructor(firstDependency: FirstDependency) {
    this.innerId = firstDependency.innerId;
  }
}

Si on implémente la fonction factory de cette façon:

let dependencyFactory = (firstDependency: FirstDependency) => {
  return new SecondDependency(firstDependency);
};

Il est possible de configurer le provider pour que la fonction factory crée l’instance de l’objet SecondDependency à injecter:

@Component({
  templateUrl: './comp-second-mod.component.html',
  providers: [
    FirstDependency,   
    { provide: SecondDependency, useFactory: dependencyFactory, deps: [ FirstDependency ] }
  ]
})
export class CustomComponent {
  constructor(private firstService: FirstServiceService,
    private firstDependency: FirstDependency,
    private secondDependency: SecondDependency) {
      console.log('firstDependency content: ' + firstDependency.innerId);
      console.log('secondDependency content: ' + secondDependency.innerId);
  }
}

Ainsi, la fonction factory dependencyFactory crée une instance de SecondDependency et l’injecte dans le composant CustomComponent.

En utilisant le paramètre “providedIn” dans @Injectable()

Le décorateur @Injectable() est utilisé pour indiquer qu’un objet est injectable en utilisant l’injection de dépendances. On précise son utilisation plus bas. Ce décorateur permet d’indiquer directement le provider d’un objet à injecter en utilisant le paramètre providedIn.

Pour déclarer en utilisant le paramètre providedIn, la déclaration est:

@Injectable({
  providedIn: 'root'
})
class Dependency {}

Dans ce cas, le provider sera le module root et l’instance injectée est un singleton.

Pour indiquer un module en tant que provider, on indique le type du module, la déclaration est du type:

import { ItemModule } from '../item.module.ts';
 
@Injectable({
  providedIn: ItemModule
})
class Dependency {}

Exemples d’implémentation de l’injection

Pour implémenter l’injection, après avoir précisé le provider, il faut indiquer qu’un objet est injectable en utilisant les décorateurs:

  • @Inject() utilisable sur les paramètres du constructeur d’un composant ou
  • @Injectable() utilisable sur la classe de l’objet à injecter.

En utilisant @Inject()

Le décorateur @Inject() est plus rarement utilisé, il permet d’indiquer que l’élément est injectable. Il se déclare dans les paramètres du constructeur du composant où l’élément doit être injecté. Si on utilise @Inject(), il n’y a pas d’autres décorateurs à préciser sur la classe de l’objet à injecter.

Par exemple:

class FirstDependency {}
 
@Component({
  providers: [ FirstDependency ]
})
export class CustomComponent {
  constructor(@Inject(FirstDependency) private firstDependency) {}
}

En utilisant @Injectable()

La décorateur @Injectable() peut être utilisé directement sur la classe de l’objet à injecter. C’est le décorateur utilisé le plus couramment. Si on utilise @Injectable(), il n’est pas nécessaire d’utiliser @Inject.

Par exemple:

@Injectable()
class FirstDependency {}
 
@Component({
  providers: [ FirstDependency ]
})
export class CustomComponent {
  constructor(private firstDependency: FirstDependency) {}
}

@Injectable() permet de préciser directement le provider avec le paramètre providedIn.

Injecter des services

Les composants sont très liés à leur template, par exemple ils permettent de mettre à disposition des données à afficher. L’existence de l’instance du composant ne survit pas au passage d’un composant à l’autre. il est donc nécessaire de prévoir un type d’élément permettant de stocker des données dont l’existence va survivre aux changements de vue. Les éléments utilisés pour être partagés entre les différents composants sont les services, ils permettent:

  • d’effectuer des traitements communs à plusieurs composants,
  • de mettre à disposition des propriétés utilisables par plusieurs composants.

D’un point de vue implémentation, un service est une classe injectable, il n’y a pas de différence entre un service et une simple classe à injecter. Ainsi, tous les éléments indiqués dans le paragraphe précédent sont valable pour les services.

Par crééer un service avec l’CLI Angular, il faut exécuter dans le répertoire de l’application ou dans le répertoire d’un module, l’instruction suivante:

ng generate service <nom du service>

ou

ng g s <nom du service>

Le fichier du service sera généré avec le décorateur suivant:

@Injectable({
  providedIn: 'root'
})

Ce paramétrage permet de configurer le service pour que le provider soit le module root. L’instance du service est un singleton injectable dans toute l’application.

Pour résumer…

Pour injecter un service, il faut:

  • Quel sera le provider de ce service: le module root, module importé ou un composant. Suivant le choix du provider, une nouvelle instance du service ou un singleton sera utilisé à chaque injection.
  • Suivant le provider choisi, il faut choisir la syntaxe à utiliser:
    • Décorateur @Injectable() avec le paramètre providedIn.
    • Paramètre providers dans le décorateur @Component() ou @NgModule() + décorateur @Inject().
    • Paramètre providers dans le décorateur @Component() ou @NgModule() + décorateur @Injectable().

Les syntaxes peuvent être synthétiser dans ce tableau:

Syntaxe Commentaires
Décorateur @Injectable() avec le paramètre providedIn
@Injectable({
    providedIn: 'root'
})
export class InjectedService {}

@Component({
    ...
})
export class ServiceConsumerComponent() {
    constructor(service: InjectedService) {}
}
Cette syntaxe est la plus triviale.
La valeur du paramètre providedIn dans @Injectable() peut être:

  • 'root' pour indiquer le module root, dans ce cas le service sera un singleton.
  • utilisé comme provider, le service sera un singleton utilisable si le service est chargé.
Paramètre providers dans le décorateur @Component() + @Inject()
export class InjectedService {}

@Component({
  providers: [ InjectedService ],
  ...
})
export class ServiceConsumerComponent() {
  constructor(
     @Inject(InjectedService) private service) 
  {}
}
Une nouvelle instance du service sera instanciée à chaque injection.
Paramètre providers dans le décorateur @NgModule() + Inject()
export class InjectedService {}

@NgModule({
  providers: [ InjectedService ],
  ...
})
export class CustomModule() {}

@Component({
  ...
})
export class ServiceConsumerComponent() {
  constructor(
    @Inject(InjectedService) private service) 
  {}
}
Le service injecté est un singleton utilisable si le module est chargé.
Paramètre providers dans le décorateur @NgModule() + le décorateur @Injectable()
@Injectable()
export class InjectedService {}

@NgModule({
  providers: [ InjectedService ],
  ...
})
export class CustomModule() {}

@Component({
  ...
})
export class ServiceConsumerComponent() {
  constructor(service: InjectedService) {}
}
Le service injecté est un singleton utilisable si le module est chargé.

La syntaxe avec le paramètre providers dans le décorateur @NgModule() et @Component() autorise des options:

@Component({
  providers: [
    ...
    { provide: InjectedService, <option>  }
  ]
})
export class ServiceConsumerComponent(
  constructor(service: InjectedService) {}
)

Les options peuvent être:

  • { provide: InjectedService, useClass: OtherTypeService } pour injecter une instance du type OtherTypeService quand le type demandé est InjectedService.
  • { provide: InjectedService, useExisting: OtherTypeService } pour injecter une instance existante de type OtherTypeService quand le type demandé est InjectedService.
  • { provide: InjectedService, useValue: new InjectedService() } pour injecter une instance particulière quand le type demandé est InjectedService.
  • { provide: InjectedService, useFactory: () => { return new InjectedService(); } } pour utiliser une logique particulière implémentée dans une factory.
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Trouver l’origine d’une fuite mémoire avec WinDbg

Lors de l’exécution d’une application, des fuites mémoires peuvent subvenir y compris dans un cadre managé. Dans les pires cas, trouver l’origine de ces fuites peut s’avérer compliqué car elles peuvent se produire dans des circonstances qu’on a du mal à identifier ou reproduire. Par exemple, ces fuites peuvent se produire dans un environnement de production quand l’application est particulièrement sollicitée. Il n’est pas forcement facile de reproduire les mêmes conditions dans un environnement de développement.

D’autre part, si l’application comporte beaucoup de code, on peut être assez démuni pour trouver l’origine de la fuite simplement en regardant statiquement le code.

Dans l’article Performance Monitor en 10 min, on avait indiqué une méthode simple pour monitorer un processus et être capable de détecter une fuite mémoire dans un processus. Dans le cadre de .NET, il est généralement possible d’aller plus loin et d’analyser plus précisement la mémoire d’un processus pour mettre en évidence l’origine d’une fuite mémoire.

Le but de cet article est d’indiquer une méthode pour mettre en évidence une fuite en mémoire dans un processus .NET en utilisant WinDbg. Dans un 2e temps, on indiquera comment tenter de trouver une fuite mémoire dans un processus natif.

@danedeaner

Préambule

Avant d’expliciter les méthodes utilisées pour tenter de trouver l’origine de fuites mémoire, on va indiquer brièvement comment utiliser WinDbg. En effet, même si WinDbg est souvent très utile pour analyser un processus en cours d’exécution ou lors d’un crash, il est particulièrement peu ergonomique.

Comment utiliser WinDbg ?

Il y a 2 façons d’utiliser WinDbg: pendant l’exécution d’un processus en mode debug et en mode statique avec un fichier dump. Dans l’article Les “dumps” mémoire en 5 min, on avait montré comment utiliser WinDbg en mode statique en ouvrant un fichier dump.

L’utilisation de WinDbg avec un dump peut s’avérer particulièrement utile dans un environnement de production puisqu’on est capable de générer le dump pendant l’exécution du processus.

Installation de WinDbg

On peut se procurer WinDbg en téléchargeant les “Debugging Tools for Windows”. Après installation, WinDbg est disponible dans le menu Windows: “Windows Kits” ⇒ “Debugging Tools for Windows” (x86 ou x64). On peut y accéder directement dans les répertoires:

  • Sur un système 32 bits: C:\Program Files\Windows Kits\10\Debuggers\x86\windbg.exe
  • Sur un système 64 bits:
    • Pour la version x86: C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\windbg.exe
    • Pour la version x64: C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe

A l’époque où WinDbg était installé avec le SDK Windows, il pouvait se trouver dans un répertoire du type:
C:\Program Files\Microsoft SDKs\Windows\v7.1\Redist\Debugging Tools for Windows

Dans un environnement de développement

On peut lancer WinDbg de 2 façons pour analyser la mémoire d’un processus au cours de son exécution. Ces 2 méthodes sont plus adaptées dans un environnement de développement.

  • 1ère méthode: exécuter WinDbg à la ligne de commandes
    Il est possible d’exécuter WinDbg en mode debug en étant attaché à un processus.

    Pour lancer WinDbg dans ce mode, on peut exécuter la ligne suivante:

    windbg.exe <fichier exécutable>
    

    L’exécutable sera lancé mais son exécution sera directement stoppé.

    Pour continuer l’exécution, il faut utiliser la commande g, appuyer sur [F5] ou utiliser le menu “Debug” ⇒ “Go”.

    On peut lancer directement l’exécution en exécutant la ligne suivante:

    windbg.exe -g <fichier exécutable>
    

    D’autres options sont disponibles sur la page suivante: docs.microsoft.com/en-us/windows-hardware/drivers/debugger/windbg-command-line-options.

  • 2e méthode: s’attacher à un processus en cours d’exécution
    On peut directement s’attacher à un processus en cours d’exécution en appuyant sur [F6] ou en utilisant le menu “File” ⇒ “Attach to a process…”

    Après s’être attaché au processus, l’exécution est en mode debug.

Quelques commandes exécutables dans l’invite de commandes WinDbg utiles pendant le debug:

  • .attach <PID> pour s’attacher à un processus.
  • .detach pour stopper l’exécution en mode debug et stopper l’exécution du processus.
  • .restart pour relancer le processus en mode debug.
  • q permet d’arrêter le débuggage et quitte WinDbg.
  • g relance l’exécution si elle a été stoppée (équivalent à [F5]).
  • t permet d’exécuter une instruction en pas à pas détaillé (équivalent à [F11] ou [F8]).
  • p pour exécuter une instruction en pas à pas en restant au niveau principal (équivalent à [F10]).

Lecture d’un “dump”

Une autre méthode pour utiliser WinDbg peut consister à lire un dump mémoire. Cette méthode est plus intéressante lorsqu’on veut analyser la mémoire d’un processus dans un environnement de production. Pour capturer un dump, quelques méthodes sont indiquées dans l’article Les “dumps” mémoire en 5 min.

On peut lire le fichier dump avec WinDbg en cliquant sur “File” ⇒ “Open Crash Dump”.

Quelques autres commandes utiles

D’autres commandes sont disponibles lorsque l’exécution est interrompue mais pas complêtement stoppée (ces commandes sont aussi disponibles si on lit un fichier dump):

r Permet d’afficher l’état des registres
lm Liste les modules
.lastevent Affiche des informations sur le debuggage
!analyze -v Affiche des informations détaillées sur le session de debug
k ou kp Affiche la pile d’appels (i.e. call stack) du thread courant
~ Affiche la liste des threads du processus
~* k Affiche la pile d’appels de tous les threads
db <adresse emplacement mémoire> Lire la mémoire sous forme d’octets simples et l’affiche sous forme de valeurs hexadécimales. Ces valeurs sont suivies de l’interprétation en caractères ASCII (quand c’est possible)
dc <adresse emplacement mémoire> Lire la mémoire sous forme de mot de 2 octets et l’affiche sous forme de valeurs hexadécimales.
dd <adresse emplacement mémoire> Lire la mémoire sous forme de double mot de 4 octets et l’affiche sous forme de valeurs hexadécimales.
dq <adresse emplacement mémoire> Lire la mémoire sous forme de quadri mot de 8 octets et l’affiche sous forme de valeurs hexadécimales.
dW <adresse emplacement mémoire> Lire la mémoire sous forme de mot de 2 octets et l’affiche sous forme de valeurs hexadécimales. Ces valeurs sont suivies de l’interprétation en caractères Unicode (quand c’est possible)
ds <adresse emplacement mémoire> Lire la mémoire sous forme d’une chaine de caractères ANSI
dS <adresse emplacement mémoire> Lire la mémoire sous forme d’une chaine de caractères Unicode
!dlls Afficher des informations sur les DLL chargées

Trouver l’origine d’une fuite mémoire

On va expliciter 2 méthodes pour tenter de trouver l’origine de fuites mémoires: dans un processus managé et dans un processus natif.

Le plus souvent des fuites mémoires proviennent d’objets instanciés et qui n’ont pas été libérés après utilisation. Plus les objets sont instanciés fréquemment et plus les fuites mémoires seront évidentes.

Pour détecter la fuite, dans le cadre des 2 méthodes, le but est dabord de détecter quel est le type d’objet le plus fréquent dans la mémoire occupée par le processus. Ensuite, on peut tenter de trouver le code qui a généré ce type. Dans le cadre d’une application réelle, ces 2 méthodes peuvent s’avérer plus complexes à mettre en œuvre car il peut y avoir beaucoup d’objets instanciés qui ne participent pas à la fuite mémoire menant ainsi à de fausses pistes.

Dans un processus managé

WinDbg peut aider à trouver l’origine de fuites mémoires dans un processus managé en s’aidant de quelques commandes. Ces commandes ont pour but de compter le nombre d’occurences des objets dans le tas. En identifiant judicieusement les objets les plus fréquents, puis en s’aidant de la pile d’appels ayant amené à la création de ces objets, on peut identifier l’origine de la fuite mémoire.

Pour montrer comment repérer l’origine d’une fuite mémoire, on se propose de créer une application .NET créant des instances d’objets de façon continue pour simuler une fuite mémoire. Cette application permettra de mettre en application une méthode pour trouver le type des objets instanciés et le code créant ces instances.

L’implémentation de l’application est:

class Program
{
    static void Main(string[] args)
    {
        var managedBigObjectGenerator = new ManagedBigObjectGenerator();
        managedBigObjectGenerator.CreateObjects();

        Console.WriteLine("Done");

        Console.ReadLine();
    }
}

Le détail de l’objet ManagedBigObjectGenerator est:

public class ManagedBigObjectGenerator
{
    private List<BigObject> objects = new List<BigObject>();

    public void CreateObjects()
    {
        for (int i = 0; i < 100000; i++)
        {
            this.objects.Add(new BigObject());

            if (i % 10 == 0)
            Thread.Sleep(10);
        }
    }
}

internal class BigObject
{
    private const string loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";

    private List<string> strings = new List<string>();

    public void CreateObjects()
    {
        for (int i = 0; i < 100000; i++)
        {
            strings.Add(loremIpsum);
        }
    }
}

Le code complet de cette application se trouve dans le repo GitHub github.com/msoft/memory_leak_managed.

Ce code permet de créer plusieurs instances de l’objet BigObject qui seront placées dans une liste. Chaque instance de BigObject crée plusieurs instances d’une chaîne de caractères et place chacune d’elles dans une liste. Dans la boucle créant les objets BigObject, on ralentit l’exécution avec une instruction Thread.Sleep(10) de façon à ce que la consommation de mémoire ne soit pas trop rapide.

On compile cette application pour générer un exécutable nommé ManagedMemoryLeak.exe.

Si on lance l’exécutable, l’utilisation de la mémoire augmente régulièrement de façon à illustrer une fuite mémoire.

Pour trouver l’origine de la fuite mémoire, on effectue les étapes suivantes:

Lancer WinDbg en mode debug

  1. Il faut exécuter WinDbg avec les privilèges Administrateur
  2. Lancer l’exécution de l’exécutable NativeMemoryLeak.exe
  3. S’attacher un processus:
    • En cliquant sur “File” ⇒ “Attach to a process…” ⇒ Sélectionner le processus ⇒ cliquer sur “OK” ou
    • Appuyer sur [F6] et sélectionner le processus

Après ces étapes, l’exécution s’arrête et le mode debug est lancé.

On peut aussi lancer WinDbg et NativeMemoryLeak.exe directement en exécutant dans le répertoire de l’exécutable:

"C:\Program Files\Windows Kits\10\Debuggers\x86\windbg.exe" -g ManagedMemoryLeak.exe

Pour interrompre l’exécution sans la stopper il faut cliquer sur l’icone suivante sur la barre de tâche:

Dans WinDbg, pour que la fenêtre soit en plein écran, on peut cliquer sur “Window” ⇒ “Dock All”.

Charger l’extension SOS

Cette extension permet à WinDbg d’afficher davantage d’informations concernant la mémoire managée du processus.

Pour charger l’extension, il faut exécuter la commande suivante:

.loadby sos clr

Afficher les détails des objets du tas managé

En mode debug et après avoir interrompu l’exécution du processus, il est possible d’afficher quelques informations sur les objets se trouvant dans le tas managé. Par exemple, on peut compter les occurences des objets avec la commmande:

!dumpheap -stat

Dans notre cas, le résultat est:

0:004> !dumpheap -stat
Statistics:
      MT    Count    TotalSize Class Name
67539664        1           12 System.AppDomainPauseManager
67531454        1           12 System.Security.HostSecurityManager
67530414        1           12 System.Collections.Generic.ObjectEqualityComparer`1[[System.Type, mscorlib]]
00234dd4        1           12 ManagedMemoryLeak.ManagedBigObjectGenerator
6753140c        1           16 System.Security.Policy.Evidence+EvidenceLockHolder
6752f54c        1           16 System.Char[]
6752de9c        1           16 System.Security.Policy.AssemblyEvidenceFactory
6752dde8        1           20 Microsoft.Win32.SafeHandles.SafePEFileHandle
6752eee0        2           24 System.Object
00234ea4        1           24 System.Collections.Generic.List`1[[ManagedMemoryLeak.BigObject, ManagedMemoryLeak]]
67531328        1           28 System.Reflection.RuntimeAssembly
6752ef7c        1           28 System.SharedStatics
6752de44        1           32 System.Security.Policy.PEFileEvidenceFactory
6752f944        1           36 System.Security.PermissionSet
6752f848        1           40 System.Security.Policy.Evidence
675312e0        1           44 System.Threading.ReaderWriterLock
6708a64c        1           48 System.Collections.Generic.Dictionary`2[[System.Type, mscorlib],[System.Security.Policy.EvidenceTypeDescriptor, mscorlib]]
67530460        1           52 System.Type[]
6752f3b0        1           68 System.AppDomainSetup
6752ee64        1           84 System.ExecutionEngineException
6752ee20        1           84 System.StackOverflowException
6752eddc        1           84 System.OutOfMemoryException
6752ec88        1           84 System.Exception
6752f698        3          108 System.String[]
002b3c88        9          110      Free
6752eff8        1          112 System.AppDomain
6752eea8        2          168 System.Threading.ThreadAbortException
67530958        4          444 System.Int32[]
67531100        3          468 System.Collections.Generic.Dictionary`2+Entry[[System.Type, mscorlib],[System.Security.Policy.EvidenceTypeDescriptor, mscorlib]][]
6752ff54       20          560 System.RuntimeType
6752eb40       38         2522 System.String
6752ef34        4        17604 System.Object[]
00235298       15       262308 ManagedMemoryLeak.BigObject[]
00234e54    24651       295812 ManagedMemoryLeak.BigObject
67091e00    24651       591624 System.Collections.Generic.List`1[[System.String, mscorlib]]
Total 49425 objects
Erreur possible

A la 1ère exécution, une erreur de ce type peut se produire:

0:004> !dumpheap -stat
c0000005 Exception in C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos.dumpheap debugger extension.
      PC: 014cfa73  VA: 00000000  R/W: 0  Parameter: 0001003f

Il faut réexecuter la commande !dumpheap -stat pour que ça marche.

On remarque que les instances de type BigObject sont les plus fréquentes.

On peut ensuite afficher les différentes instances d’objets de type ManagedMemoryLeak.BigObject ou contenant des objets de ce type (dans le cas de listes) en exécutant:

!dumpheap -type ManagedMemoryLeak.BigObject

On obtient une liste de toutes les instances des objets de type ManagedMemoryLeak.BigObject et la dernière ligne indique probablement une liste contenant tous ces objets:

Address  MT             Size
01edaeb8 00234e54       12     
01edaedc 00234e54       12     
01edaf00 00234e54       12     
01edaf24 00234e54       12     
01edaf48 00234e54       12     
01edaf6c 00234e54       12     
01edaf90 00234e54       12     
01edafb4 00234e54       12     
01edafd8 00234e54       12
02de5530 00235298   131084

Pour voir les lignes qui ont généré ces objets, on choisit une adresse au hasard et on exécute:

0:004> !gcroot 02de5530
Thread 1918:
    001cf4b8 006f05af ManagedMemoryLeak.ManagedBigObjectGenerator.CreateObjects() [C:\ManagedMemoryLeak\BigObjectGenerator.cs @ 35]
        ebp+10: 001cf4c0
            ->  01de249c ManagedMemoryLeak.ManagedBigObjectGenerator
            ->  01de24a8 System.Collections.Generic.List`1[[ManagedMemoryLeak.BigObject, ManagedMemoryLeak]]
            ->  02de5530 ManagedMemoryLeak.BigObject[]

Found 1 unique roots (run '!GCRoot -all' to see all roots).

Dans ce cas il s’agit bien d’une liste de ManagedMemoryLeak.BigObject.

Si on exécute la commande suivante:

0:004> !gcroot 01edafd8
Thread 1918:
    001cf4b8 006f05af ManagedMemoryLeak.ManagedBigObjectGenerator.CreateObjects() [C:ManagedMemoryLeak\BigObjectGenerator.cs @ 35]
        ebp+10: 001cf4c0
            ->  01de249c ManagedMemoryLeak.ManagedBigObjectGenerator
            ->  01de24a8 System.Collections.Generic.List`1[[ManagedMemoryLeak.BigObject, ManagedMemoryLeak]]
            ->  02de5530 ManagedMemoryLeak.BigObject[]
            ->  01edafd8 ManagedMemoryLeak.BigObject

    001cf4b8 006f05af ManagedMemoryLeak.ManagedBigObjectGenerator.CreateObjects() [C:\ManagedMemoryLeak\BigObjectGenerator.cs @ 35]
        ebp+14: 001cf4bc
            ->  01edafd8 ManagedMemoryLeak.BigObject

Found 2 unique roots (run '!GCRoot -all' to see all roots).

Ainsi les différentes lignes indiquent d’où proviennent les objets ayant conduit à la fuite mémoire de façon à avoir une idée précise de son origine.

Dans un processus natif

Avoir une fuite mémoire dans un processus natif est courant et il est souvent difficile d’en trouver l’origine en particulier lorsqu’il y a beaucoup de code. WinDbg peut aider à trouver l’origine de la fuite même si le résultat n’est pas garanti à tous les coups.

Comme pour les fuites dans du code managé, la méthode consiste à lister les allocations en mémoire en considérant la taille des blocs. On peut, ainsi, repérer le ou les blocs qui se répètent le plus souvent en considérant la taille des blocs dont l’occurence se répète le plus fréquemment.

En listant tous les blocs dont la taille est la plus courante dans le processus, on peut en déduire l’objet le plus fréquemment alloué.

Pour montrer comment repérer les lignes de code à l’origine d’une fuite mémoire, on se propose de créer une application créant des instances d’objets de façon continue. On va ensuite essayer d’en trouver l’origine en utilisant la méthode décrite précédemment. L’application utilisée est très simple et n’est pas représentative de la complexité du code que l’on retrouve courammment dans des applications réelles. Le but est simplement de mettre en application une méthode d’analyse possible.

L’implémentation de l’application est:
Dans NativeMemoryLeak.cpp:

#include <windows.h>
#include <iostream>
#include "LeakingObject.h"

int main()
{
    int objectCount = 10000;

    LeakingObject **leakingObjects = new LeakingObject*[objectCount];
    for (int i = 0; i < objectCount; i++)
    {
        leakingObjects[i] = new LeakingObject(10);
    }

    for (int i = 0; i < objectCount; i++)
    {
        leakingObjects[i]->CreateObjects();

        if (i % 10 == 0)

        Sleep(100);
    }
}

L’objet LeakingObject est:

#include "LeakingObject.h"

LeakingObject::LeakingObject(int count)
{
    this->count = count;
}

LeakingObject::~LeakingObject()
{
}

void LeakingObject::CreateObjects()
{
    this->innerStrings = new std::string[this->count];

    for (int i = 0; i < this->count; i++)
    {
        this->innerStrings[i] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
    }
}

Le code complet de cette application se trouve dans le repo GitHub github.com/msoft/memory_leak_unmanaged.

Ce code permet de créer plusieurs instances de l’objet LeakingObject et les place dans un tableau. Chaque instance de LeakingObject crée plusieurs instances d’une chaîne de caractères et place chacune d’elles dans un tableau. Dans la boucle créant les objets LeakingObject, on ralentit l’exécution avec des instructions Sleep(100) de façon à ce que la consommation de mémoire ne soit pas trop rapide.

On compile cette application pour générer un exécutable nommé NativeMemoryLeak.exe.

Si on lance l’exécutable, l’utilisation de la mémoire augmente régulièrement de façon à illustrer une fuite mémoire.

Pour trouver l’origine de la fuite mémoire dans le processus natif, on effectue les étapes suivantes:

Activer gflags.exe

Dans un premier temps et avant de lancer l’exécutable NativeMemoryLeak.exe, on doit activer la création d’une base des informations des piles d’appels en utilisant l’utilitaire gflags.exe. Cet utilitaire est livré avec Windbg, il permet d’ajouter des informations qui sont stockées dans la base de registres et qui seront utiles lors du debug avec WinDbg. Par exemple, il est capable de monitorer les allocations sur le tas (i.e. heap) pour aider à trouver l’origine d’une fuite mémoire.

Après installation, gflags.exe se trouve dans le même répertoire que WinDbg:

  • Sur un système 32 bits: C:\Program Files\Windows Kits\10\Debuggers\x86\gflags.exe
  • Sur un système 64 bits:
    • Pour la version x86: C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\gflags.exe
    • Pour la version x64: C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\gflags.exe

Pour activer gflags.exe sur un exécutable:

  1. Lancer une invite de commandes avec les droits administrateur
  2. Aller dans le répertoire de NativeMemoryLeak.exe
  3. Exécuter la ligne suivante:
    gflags.exe /i <fichier .exe> +ust
    

Le détail des options est:

  • /i <fichier .exe> permet d’indiquer qu’on veut créer les traces pour un exécutable particulier,
  • +ust indique qu’on veut créer la base des traces d’appels en mode utilisateur (i.e. user mode stack trace).

On peut rajouter l’option +hpa pour activer la vérification des pages du tas (i.e. page heap) toutefois cette option ne semble pas compatible avec +ust. Lorsque l’option +hpa est activée, l’option +ust ne semble plus activée dans WinDbg.

Ne pas oublier de désactiver la base de traces

L’option +ust dégrade les performances lors de l’exécution, il ne faut pas oublier de la désactiver après utilisation en exécutant:

gflags.exe /i <fichier .exe> -ust

Dans notre cas, pour activer l’option +ust, on exécute la ligne suivante:

"C:\Program Files\Windows Kits\10\Debuggers\x86\gflags.exe" /i NativeMemoryLeak.exe +ust

Le résultat est:

Current Registry Settings for NativeMemoryLeak.exe executable are: 00001000
    ust - Create user mode stack trace database

Pour plus de détails sur gflags: docs.microsoft.com/en-us/windows-hardware/drivers/debugger/gflags-commands.

Lancer WinDbg en mode debug

  1. Il faut exécuter WinDbg avec les privilèges Administrateur
  2. Lancer l’exécution de l’exécutable NativeMemoryLeak.exe
  3. S’attacher un processus:
    • En cliquant sur “File” ⇒ “Attach to a process…” ⇒ Sélectionner le processus ⇒ cliquer sur “OK” ou
    • Appuyer sur [F6] et sélectionner le processus

Après ces étapes, l’exécution s’arrête et le mode debug est lancé.

On peut aussi lancer WinDbg et NativeMemoryLeak.exe directement en exécutant:

"C:\Program Files\Windows Kits\10\Debuggers\x86\windbg.exe" -g NativeMemoryLeak.exe

Pour interrompre sans stopper l’exécution il faut cliquer sur l’icone suivante sur la barre de tâche:

On peut vérifier le mode activé avec gflags.exe en exécutant la commande:

!gflag

Dans notre cas, le résultat est:

0:001> !gflag
Current NtGlobalFlag contents: 0x00001000
    ust - Create user mode stack trace database

Afficher l’état du tas

On peut afficher la mémoire occupée par les tas et ainsi vérifier la présence d’une fuite mémoire en exécutant la commande:

!heap -s

L’option -s permet d’afficher un résumé sur les différents tas.

Le résultat est:

0:001> !heap -s
NtGlobalFlag enables following debugging aids for new heaps:
    stack back traces
LFH Key                   : 0x0a17ba9d
Termination on corruption : ENABLED
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
01590000 08000002   32576  17468  32576    135    13     6    0      0   LFH
00010000 08008000      64      4     64      2     1     1    0      0      
00020000 08008000      64     64     64     62     1     1    0      0      
-----------------------------------------------------------------------------

On peut aussi exécuter:

!heap -stat

-stat permet d’afficher des informations sur l’utilisation des tas.

Le résultat est dans notre cas:

0:001> !heap -stat
_HEAP 01590000
     Segments            00000006
         Reserved  bytes 01fd0000
         Committed bytes 0110f000
     VirtAllocBlocks     00000000
         VirtAlloc bytes 00000000
_HEAP 00020000
     Segments            00000001
         Reserved  bytes 00010000
         Committed bytes 00010000
     VirtAllocBlocks     00000000
         VirtAlloc bytes 00000000
_HEAP 00010000
     Segments            00000001
         Reserved  bytes 00010000
         Committed bytes 00001000
     VirtAllocBlocks     00000000
         VirtAlloc bytes 00000000

Pour davantage de détails sur la commande !heap: docs.microsoft.com/en-us/windows-hardware/drivers/debugger/-heap.

Dans le résumé présenté, on peut voir la quantité de mémoire allouée pour tous les tas. On peut supposer que le tas nécessitant le plus de mémoire est celui dans lequel se trouve les instances provoquant la fuite mémoire.

Pour s’en assurer, on peut continuer brièvement l’exécution en exécutant l’instruction suivante dans WinDbg:

g

Si on interrompt l’exécution après un instant et si exécute de nouveau !heap -s, on peut vérifier si l’utilisation de la mémoire a évolué:

0:001> !heap -s
NtGlobalFlag enables following debugging aids for new heaps:
    stack back traces
LFH Key                   : 0x0a17ba9d
Termination on corruption : ENABLED
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
01590000 08000002   48768  32592  48768    151    14     7    0      0   LFH
00010000 08008000      64      4     64      2     1     1    0      0      
00020000 08008000      64     64     64     62     1     1    0      0      
-----------------------------------------------------------------------------

La quantité de mémoire utilisée par le tas situé à l’adresse virtuelle 3e0000 semble augmenter:

Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
01590000 08000002   32576  17468  32576    135    13     6    0      0   LFH

01590000 08000002   48768  32592  48768    151    14     7    0      0   LFH

Afficher les détails des tas

Après avoir répéré le tas où pourrait se trouver la fuite mémoire, on va afficher des détails sur l’utilisation des allocations mémoire par taille en exécutant la commande:

!heap -stat -h <adresse du tas>

Le résultat est:

0:001> !heap -stat -h 01590000
 heap @ 01590000
group-by: TOTSIZE max-display: 20
    size     #blocks     total     ( %) (percent of total busy bytes)
    1e4 c7a6 - 17975d8  (84.84)
    2c eed1 - 290bec  (9.23)
    140 13f7 - 18f4c0  (5.61)
    9c64 1 - 9c64  (0.14)
    39a6 1 - 39a6  (0.05)
    1806 1 - 1806  (0.02)
    c24 1 - c24  (0.01)
    20 61 - c20  (0.01)
    858 1 - 858  (0.01)
    824 1 - 824  (0.01)
    6ec 1 - 6ec  (0.01)
    78 e - 690  (0.01)
    224 3 - 66c  (0.01)
    244 2 - 488  (0.00)
    440 1 - 440  (0.00)
    400 1 - 400  (0.00)
    200 2 - 400  (0.00)
    36d 1 - 36d  (0.00)
    220 1 - 220  (0.00)
    208 1 - 208  (0.00

On affiche un détail des blocs mémoire ayant une taille particulière. On s’intéresse à l’allocation la plus fréquente obtenue précédemment. Ainsi is on regarde le pourcentage d’utilisation le plus élevé:

size     #blocks     total     ( %) (percent of total busy bytes)
1e4 c7a6 - 17975d8  (84.84)

Pour afficher tous les blocs, on exécute la commande:

!heap -flt s <taille>

Dans notre cas, on obtient:

0:001> !heap -flt s 1e4
_HEAP @ 3e0000
HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
0348d388 0041 0041  [00]   0348d3a0    001e4 - (busy)
0348d590 0041 0041  [00]   0348d5a8    001e4 - (busy)
0348d798 0041 0041  [00]   0348d7b0    001e4 - (busy)
0348d9a0 0041 0041  [00]   0348d9b8    001e4 - (busy)
0348dba8 0041 0041  [00]   0348dbc0    001e4 - (busy)
0348ddb0 0041 0041  [00]   0348ddc8    001e4 - (busy)
0348dfb8 0041 0041  [00]   0348dfd0    001e4 - (busy)
0348e1c0 0041 0041  [00]   0348e1d8    001e4 - (busy)
0348e3c8 0041 0041  [00]   0348e3e0    001e4 - (busy)
0348e5d0 0041 0041  [00]   0348e5e8    001e4 - (busy)
0348e7d8 0041 0041  [00]   0348e7f0    001e4 - (busy)
0348e9e0 0041 0041  [00]   0348e9f8    001e4 - (busy
...

Si on prends au hasard l’adresse d’un bloc, on peut accéder à la pile d’appel de l’entrée du tas:

!heap -p -a <adresse UsrPtr>

Dans notre cas, le résultat est:


0:001> !heap -p -a 0348d798
    address 0348d798 found in
    _HEAP @ 1590000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        0348d798 0041 0000  [00]   0348d7b0    001e4 - (busy)
        7731d78c ntdll!RtlAllocateHeap+0x00000274
        52d7b178 ucrtbased!heap_alloc_dbg_internal+0x00000198
        52d7af96 ucrtbased!heap_alloc_dbg+0x00000036
        52d7d72a ucrtbased!_malloc_dbg+0x0000001a
        52d7e054 ucrtbased!malloc+0x00000014
        39526d NativeMemoryLeak!operator new+0x0000000d
        393731 NativeMemoryLeak!std::_Default_allocate_traits::_Allocate+0x00000031
        391d1e NativeMemoryLeak!std::_Allocate<8,std::_Default_allocate_traits,0>+0x0000004e
        3944af NativeMemoryLeak!std::allocator<char>::allocate+0x0000003f
        3922bf NativeMemoryLeak!std::basic_string<char,std::char_traits<char>,std::allocator<char> >::_Reallocate_for<<lambda_9366063389c5f42a00a5088cf24e69de>,char const *>+0x0000008f
        39467d NativeMemoryLeak!std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign+0x000000ad
        39459f NativeMemoryLeak!std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign+0x0000004f
        3930b9 NativeMemoryLeak!std::basic_string<char,std::char_traits<char>,std::allocator<char> >::operator=+0x00000039
        393389 NativeMemoryLeak!LeakingObject::CreateObjects+0x000000f9
        394e2e NativeMemoryLeak!main+0x0000012e
        395a4e NativeMemoryLeak!invoke_main+0x0000001e
        3958b7 NativeMemoryLeak!__scrt_common_main_seh+0x00000157
        39574d NativeMemoryLeak!__scrt_common_main+0x0000000d
        395ac8 NativeMemoryLeak!mainCRTStartup+0x00000008
        755fef6c kernel32!BaseThreadInitThunk+0x0000000e
        77303618 ntdll!__RtlUserThreadStart+0x00000070
        773035eb ntdll!_RtlUserThreadStart+0x0000001b

Une ligne peut s’avérer intéressante pour trouver la ligne responsable de l’allocation:

393389 NativeMemoryLeak!LeakingObject::CreateObjects+0x000000f9

La commande uf permet de désassembler une fonction en mémoire. En utilisant l’adresse de l’entrée dans le tas, on peut voir le détail de la fonction et la correspondance dans le code:

0:001> uf 393389 
NativeMemoryLeak!LeakingObject::CreateObjects [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 17]:
   17 00393290 55              push    ebp
   17 00393291 8bec            mov     ebp,esp
   17 00393293 81ecf4000000    sub     esp,0F4h
   ...

NativeMemoryLeak!LeakingObject::CreateObjects+0x72 [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 18]:
   18 00393302 8b8d14ffffff    mov     ecx,dword ptr [ebp-0ECh]
   18 00393308 8b9520ffffff    mov     edx,dword ptr [ebp-0E0h]
   18 0039330e 8911            mov     dword ptr [ecx],edx
   ...

NativeMemoryLeak!LeakingObject::CreateObjects+0xb3 [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 18]:
   18 00393343 c7850cffffff00000000 mov dword ptr [ebp-0F4h],0

NativeMemoryLeak!LeakingObject::CreateObjects+0xbd [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 18]:
   18 0039334d 8b45f8          mov     eax,dword ptr [ebp-8]
   18 00393350 8b8d0cffffff    mov     ecx,dword ptr [ebp-0F4h]
   18 00393356 894804          mov     dword ptr [eax+4],ecx
   ...

NativeMemoryLeak!LeakingObject::CreateObjects+0xd2 [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 20]:
   20 00393362 8b45ec          mov     eax,dword ptr [ebp-14h]
   20 00393365 83c001          add     eax,1
   20 00393368 8945ec          mov     dword ptr [ebp-14h],eax

NativeMemoryLeak!LeakingObject::CreateObjects+0xdb [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 20]:
   20 0039336b 8b45f8          mov     eax,dword ptr [ebp-8]
   20 0039336e 8b4dec          mov     ecx,dword ptr [ebp-14h]
   20 00393371 3b08            cmp     ecx,dword ptr [eax]
   ...

NativeMemoryLeak!LeakingObject::CreateObjects+0xe5 [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 22]:
   22 00393375 6838cd3900      push    offset NativeMemoryLeak!`string' (0039cd38)
   22 0039337a 6b4dec1c        imul    ecx,dword ptr [ebp-14h],1Ch
   22 0039337e 8b45f8          mov     eax,dword ptr [ebp-8]
   ...

NativeMemoryLeak!LeakingObject::CreateObjects+0xfb [c:\nativememoryleak\nativememoryleak\leakingobject.cpp @ 24]:
   24 0039338b 5f              pop     edi
   24 0039338c 5e              pop     esi
   24 0039338d 5b              pop     ebx
   ...

Ces lignes correspondent au code:

void LeakingObject::CreateObjects()
{
    this->innerStrings = new std::string[this->count];

    for (int i = 0; i < this->count; i++)
    {
        this->innerStrings[i] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
    }
}

Pour quitter WinDbg et le mode debug, il suffit d’utliiser la commande:

q

De la même façon que pour le processus managé, on peut ainsi identifier les différentes lignes de code indiquant où les objets ayant conduit à la fuite mémoire ont été créés.

Pour résumer

Pour identifier l’origine d’une fuite mémoire:

  • Pour un processus managé, on exécute:
    1. On lance WinDbg en mode Début ou avec un fichier de dump
    2. On charge l’extension SOS dans WinDbg:
      .loadby sos clr
      
    3. On affiche les statistiques des objets instanciés dans le tas managé:
      !dumpheap -stat
      
    4. Obtenir les adresses des instances d’un objet d’un type spécifié:
      !dumpheap -type <type de l'objet>
      
    5. Obtenir les lignes ayant générées l’instance de l’objet
      !gcroot <adresse de l'instance>
      
  • Pour un processus natif:
    1. Activer la base de données permettant de stocker les traces d’appels
      gflags.exe /i <fichier .exe> +ust
      
    2. Lancer WinDbg en mode debug
    3. Afficher les statistiques sur l’état des tas, cette instruction permet d’indiquer le tas nécessitant le plus de mémoire
      !heap -s
      
    4. On sélectionne le tas nécessitant le plus de mémoire et on affiche des statistiques plus précises sur les objets se trouvant dans ce tas:
      !heap -stat -h <adresse du tas>
      

      A la suite de l’instruction précédente, on obtient une liste d’objets classés par taille.

    5. L’instruction suivante permet d’afficher les objets ayant une taille particulière:
      !heap -flt s <taille>
      

      On obtient une liste d’objets avec leur adresse

    6. Pour afficher la pile d’appels ayant permis de créer l’objet, on exécute:
      !heap -p -a <adresse UsrPtr>
      
    7. On affiche ensuite le détail de la fonction ayant permis de créer l’objet:
      uf <adresse de l'instruction ayant permis l'instanciation de l'objet>
      
Références

Commandes WinDbg:

Précisions .NET:

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Angular CLI en 5 min

Le CLI Angular (i.e. Command Line Interface) permet de faciliter l’exécution de nombreuses tâches sur un projet Angular. Il n’est, cependant, pas facile de se rappeler de toutes les commandes. Cet article a pour but d’expliciter quelques-unes des commandes principales et de leurs options.

@meteorphoto

La doc complête du CLI Angular se trouve sur les pages suivantes:

Avant d’installer le CLI Angular avec NPM, il faut installer, au préalable, Node.js: installation de Node.js.

L’installation du CLI est ensuite directe:

npm install @angular/cli 

Certaines options sont communes à plusieurs commandes, par exemple:

Commande Commentaires
Afficher l’aide générale
ng –h 
ng help
Afficher l’aide d’une commande
ng <commande> --help 
ng <alias de la commande> --help 
ng <commande> <option> --help 
ng <alias de la commande> <alias option> --help
Simuler l’exécution
ng <commande> <options> --dry-run 
ng <commande> <options> -d 
Aucune modification des fichiers existants ne sera effectuée
Forcer la génération de fichiers en écrasant ceux existants
ng <commande> <options> --force 
ng <commande> <options> -f

Les options peuvent être indiquées de différentes façons:

  • Sans indiquer de valeur pour l’option: c’est la valeur par défaut de l’option qui sera utilisée.
  • En indiquant explicitement une valeur.

Voici quelques-unes des syntaxes possibles pour préciser un option:

Commande Exemple
Si la valeur attendue de l’option est un booléen
ng <commande> --<nom option> 

La valeur de l’option sera l’opposé de la valeur par défaut.

ng new <nom application> --skip-tests 

Les fichiers de tests ne seront pas générés (par défaut, les fichiers de test sont générés).

Préciser la valeur de l’option explicitement
ng <commande> --<nom option> <valeur option> 
ng <commande> --<nom option>=<valeur option>
ng new <nom application> --skip-tests true 
ng new <nom application> --skip-tests=true
Utiliser directement des alias
ng <commande> --<alias option> <valeur option> 
ng <commande> --<alias option>=<valeur option> 
ng new <nom application> --S true 
ng new <nom application> --S=true 
Utiliser des options sans tirets avec des majuscules Par exemple, les options suivantes sont équivalentes:

--skip-tests
--skipTests

Créer une nouvelle application (ng new)

La commande est:

ng new <nom application> <options> 
ng n <nom application> <options> 

Quelques options courantes:

Commande Commentaires
Créer une nouvelle application
ng new <nom application> 
ng n <nom application>
Eviter la génération des fichiers de test *.spec.ts
ng new <nom application> --skip-tests 
ng new <nom application> -S
Eviter l'installation des packages NPM
ng new <nom application> --skip-install
npm install ne sera pas exécuté après création des fichiers de l'application
Installer un module de routing
ng new <nom application> --routing
Rajoute le fichier src/app/app-routing.module.ts.
Générer dans un répertoire
ng new <nom application> --directory <chemin répertoire>
Préciser l'extension des fichiers de style
ng new <nom application> --style <extension> 
Les valeurs possibles sont: "css", "scss", "less", "styl".

Les fichiers suivants seront générés:

  • src/styles.<extension>
  • src/app/app.component.<extension>

L'indication de l'extension peut aussi être indiquée dans le fichier angular.json en utilisant le paramètre "styleext".

Inclure le style dans le fichier du composant
ng new <nom application> --inline-style 
ng new <nom application> -s 
Le style est inclus dans les fichiers <nom composant>.component.ts.
Indiquer la valeur préfixant les sélecteurs
ng new <nom application> --prefix <nom préfixe> 
ng new <nom application> -p <nom préfixe>
Le préfixe des sélecteurs sera utilisé dans les composants:

@component({ 
    selector: '<nom préfixe>-root', 
    templateUrl: './app.component.html', 
    styleUrls: ['./app.component.css'] 
}) 

Le préfixe peut être modifié dans le fichier angular.json au niveau du paramètre "prefix".

Créer un workspace vide sans projet
ng new <nom application> --create-application false 
Le répertoire src et son contenu ne seront pas générés.

Générer un nouvel élément (ng generate)

La commande est:

ng generate <type élément> <options> 
ng g <type élément> <options> 

Un élément peut être généré dans un répertoire spécifique en rajoutant le chemin de ce répertoire devant le nom de l'élement, par exemple:

ng generate component repertoire1/repertoire2/<nom composant>

Dans ce cas, si le répertoire repertoire1/repertoire2 n'existe pas, il sera créé.

Quelques exemples des options possibles:

Commande Options Commentaire

Composant

ng g component  
ng g c
--help
Affiche toutes les options
ng g component <nom composant> 
ng g c <nom composant>
--flat
Le composant sera créé directement dans le répertoire src/app
(pas de répertoire pour le composant).
--inline-template
-t
Le template sera inclus dans le fichier du composant
(<nom composant>.component.ts).
--inline-style
-s
Le style sera inclus dans le fichier du composant.
--spec
Si la valeur est false, le fichier .spec pour les tests ne sera pas généré.

La valeur par défaut est true.
--module <nom  module> 
-m <nom module>
Précise le cas échéant le module dans lequel le composant sera créé.
--project <nom projet> 
-p <nom projet> 
Précise le cas échéant le projet dans lequel le composant sera créé.
--view-encapsulation <nom strategie> 
-v <nom stratégie>
Indique la stratégie d'encapsulation de la vue à utiliser.
--change-detection <nom stratégie> 
-c <nom stratégie>
Indique la stratégie de détection de la vue à utiliser.

Application

ng g application <nom appli.> 
ng g app <nom appli.> 
Permet de créer une application dans le répertoire "projects"
du workspace.

Le nom du répertoire "projects" peut être modifié
avec le paramètre newProjectRoot dans le fichier angular.json:

{ 
  "$schema": "./node_modules/@angular/
cli/lib/config/schema.json", 
  "version": 1, 
  "newProjectRoot": "projects",
  "projects": { }, 
} 
ng g application 
ng g app
--help
Affiche toutes les options
ng g application <nom appli.> 
ng g app <nom appli>
--routing
Permet de rajouter un module de routing. Le fichier créé sera
projects/<nom application>/src/app/app-routing.module.ts.

Le RouterModule sera importé dans le module de routing avec
RouterModule.forChild():

@NgModule({ 
    imports: [RouterModule.forChild([])], 
    // ...
}) 
--skip-install
npm install ne sera pas exécuté après création de l'application.
--skip-package-json
Ne rajoute pas les dépendances dans le fichier package.json.

Bibliothèque

ng g library <nom lib.> 
ng g lib <nom lib.> 
Permet de créer une bibliothèque générique dans le répertoire
"projects/<nom lib.>" du workspace.

Le nom du répertoire "projects" peut être modifié avec le paramètre
newProjectRoot dans le fichier angular.json:

{ 
  "$schema": "./node_modules/@angular/
cli/lib/config/schema.json", 
  "version": 1, 
  "newProjectRoot": "projects", 
  "projects": { }, 
} 

Le point d'entrée de la bibliothèque est
projects/<nom lib.>/src/public-api.ts. Tous les
composants, modules et services implémentés dans la bibliothèque sont exportés dans ce fichier.

La bibliothèque est un cas particulier de projet, il est donc indiqué
dans le fichier angular.json:

{ 
  "$schema": "./node_modules/@angular/
cli/lib/config/schema.json", 
  "version": 1, 
  "newProjectRoot": "projects", 
  "projects": { 
    "mylibrary": { 
      "projectType": "library", 
      "root": "projects/mylibrary", 
      "sourceRoot": "projects/mylibrary/src", 
      ... 
    }}, 
} 

Le fichier tsconfig.json est modifié car les chemins vers la bibliothèque sont rajoutés:

{ 
  "compileOnSave": false, 
  "compilerOptions": { 
    ... 
    "paths": { 
      "mylibrary": [ 
        "dist/mylibrary" 
      ], 
      "mylibrary/*": [ 
        "dist/mylibrary/*" 
      ] 
    } 
  }, 
  ... 
  } 
} 
ng g library  
ng g lib
--help
Affiche toutes les options
ng g library <nom lib.> 
ng g lib <nom lib.>
--entry-file <chemin du fichier> 
Précise le point d'entrée de la bibliothèque.
--skip-ts-config
Ne pas mettre à jour le fichier tsconfig.json pour rajouter les chemins de la bibliothèque.

Module

ng g module <nom module> 
ng g m <nom module> 
Crée un feature module dans le répertoire src/app/<nom module>
ng g module  
ng g m
--help
Affiche toutes les options
ng g module <nom module> 
ng g m <nom module> 
--project <nom project>
Précise le cas échéant, le projet dans lequel le feature module sera créé.
--flat
Crée le feature module directement dans le répertoire src/app.
--module <nom module> 
-m <nom module>
Précise le nom du module dans lequel le feature module à créer sera déclaré.
--routing
Permet de rajouter un module de routing pour le feature module à créer.
Le fichier de ce module sera nommé <nom du module>.module.ts.

Le RouterModule sera importé dans le module de routing avec
RouterModule.forChild():

@NgModule({ 
    imports: [RouterModule.forChild([])], 
    // ...
}) 

Service

ng g service <nom service> 
ng g s <nom service>
Crée un service directement dans le répertoire app/src.
ng g service  
ng g s
--help
Affiche toutes les options.
ng g service <nom service> 
ng g s <nom service> 
--project <nom projet>
Précise le cas échéant, le projet dans lequel le service sera créé.
--spec false 
--skip-tests 
Permet d'empêcher la création d'un fichier de test
src/app/<nom service>.spec.ts

Classe

ng g class <nom classe> 
ng g cl <nom classe>
Crée une classe directement dans le répertoire app/src.
ng g class  
ng g cl
--help
Affiche toutes les options.
ng g class <nom classe> 
ng g cl <nom classe>
--project <nom projet>
Précise le cas échéant, le projet dans lequel la classe sera créée.
--spec false 
--skip-tests 
Permet d'empêcher la création d'un fichier de test
src/app/<nom service>.spec.ts
--type <valeur type>
Permet d'indiquer le nom d'un type qui sera utilisé dans le nom
du fichier de la classe <nom classe>.<nom type>.ts.

Interface

ng g interface <nom interface> 
ng g i <nom interface> 
Crée une interface dans le répertoire src/app.
ng g interface 
ng g i
--help
Affiche toutes les options.
ng g interface <nom interface> 
ng g i <nom interface> 
--project <nom projet>
Précise le cas échéant, le projet dans lequel l'interface sera créée.

Enum

ng g enum <nom enum> 
ng g e <nom enum> 
Crée un enum dans le répertoire src/app.
ng g enum  
ng g e 
--help
Affiche toutes les options.
ng g enum <nom enum> 
ng g e <nom enum> 
--project <nom projet>
Précise le cas échéant, le projet dans lequel l'enum sera créé.

Guard

ng g guard <nom guard> 
ng g g <nom guard>
Crée un guard dans le répertoire src/app.
ng g guard  
ng g g
--help
Affiche toutes les options.
ng g guard <nom guard> 
ng g g <nom guard>
--implements <nom interface> 
Précise le cas échéant, le nom de l'interface que doit implémenter
le guard.
--project <nom projet>
Précise le cas échéant, le projet dans lequel le guard sera créé.

Pipe

ng g pipe <nom pipe> 
ng g p <nom pipe> 
Crée un pipe dans le répertoire src/app et le rajoute dans le
module de l'application.
ng g pipe  
ng g p 
--help
Affiche toutes les options.
ng g pipe <nom pipe> 
ng g p <nom pipe> 
--project <nom projet>
Précise le cas échéant, le projet dans lequel le pipe sera créé.
--module <nom module> 
-m <nom module> 
Précise le cas échéant le module dans lequel le pipe sera déclaré.
Par défaut, c'est src/app/app.module.ts.
--skip-import
Permet d'éviter d'importer le pipe dans le module par défaut
c'est-à-dire src/app/app.module.ts.
--spec false 
--skip-tests 
Permet d'empêcher la création d'un fichier de test
src/app/<nom pipe>.pipe.spec.ts.
Comment consommer une bibliothèque ?

Il faut construire la bibliothèque avant de construire l'application qui la consomme:

ng build <nom bibliothèque> 

Après création de la bibliothèque, étant donné que son chemin est indiqué dans le fichier tsconfig.json avec la configuration:

{ 
  "compileOnSave": false, 
  "compilerOptions": { 
    ... 
    "paths": { 
      "mylibrary": [ 
        "dist/mylibrary" 
      ], 
      "mylibrary/*": [ 
        "dist/mylibrary/*" 
      ] 
    } 
  }, 
  ... 
  } 
} 

Il suffit d'importer directement un élément de la bibliothèque:

import { <nom élément à importer> } from 'mylibrary'; 

Effectuer une build (ng build)

La commande est (à exécuter dans le répertoire du workspace):

ng build <options> 
ng b <options> 

On peut effectuer une build pour un projet spécifique en exécutant:

ng build <nom du projet> <options> 
ng b <nom du projet> <options> 

Le répertoire de sortie peut être modifié dans le fichier angular.json avec le paramètre "outputPath". Par défaut, le répertoire est dist/<nom du workspace> .

La compilation va générer les fichiers:

  • runtime.js contenant le runtime WebPack.
  • main.js contenant le code de l'application.
  • polyfills.js contenant les platform polyfills.
  • style.js contenant les styles.
  • vendor.js contenant les fichiers angular.

Ces fichiers sont référencés par le fichier index.html généré.

Options Commentaires
Pour effectuer la build avec les paramètres de production Ces syntaxes sont équivalentes:

--prod 
--configuration=production 
--configuration production 
--c=production 
--c production 
--prod=true 
--prod true
Par défaut cette configuration est présente dans le fichier angular.json.
Pour lancer une compilation avec une configuration personnalisée Ces syntaxes sont équivalentes:

--configuration <nom configuration> 
--configuration=<nom configuration> 
--c <nom configuration> 
--c=<nom configuration>
Il faut rajouter la configuration personnalisée dans le fichier angular.json dans la liste "configurations" sous la liste "builds".
Pour builder dans un répertoire spécifique
--output-path <chemin du répertoire>
Pour générer une carte des sources
--source-map
Pour compiler "Ahead Of Time"
--aot
Il s'agit d'une optimisation pour réduire le temps de compilation du code Javascript par le browser au moment de son chargement.
Pour compiler et s'abonner aux changements des fichiers sources
--watch
En cas de modifictions des fichiers source, le code est recompilé.
Pour indiquer explicitement un point d'entrée de l'application
--index <nom du fichier>
Par défaut, le point d'entrée est index.html.
Augmenter la verbosité
--verbose
Utiliser un fichier tsconfig.json spécifique
--ts-config <chemin du fichier>

Quand on utilise la configuration de production, par défaut, les options importantes de compilation sont (indiquées dans le fichier angular.json):

"configurations": { 
            "production": { 
              "fileReplacements": [ 
                { 
                  "replace": "src/environments/environment.ts", 
                  "with": "src/environments/environment.prod.ts" 
                } 
              ], 
              "optimization": true, 
              "outputHashing": "all", 
              "sourceMap": false, 
              "extractCss": true, 
              "namedChunks": false, 
              "aot": true, 
              "extractLicenses": true, 
              "vendorChunk": false, 
              "buildOptimizer": true, 
            }, 

Parmi ces options:

  • "optimization" permet d'appliquer des optimisations sur la build générée.
  • "outputHashing": permet de générer un bundle avec des noms de fichier comportant une clé de hashage du fichier. Cette clé de hashage rend le nom du fichier différent si le fichier est différent entre 2 versions. Les noms différents permettent d'éviter au browser d'utiliser une version antérieure du fichier récupérée à partir de son cache.

    Pour désactiver cet élément, on peut lancer une build avec l'option:

    ng build --output-hashing=none 
    
  • "aot" (i.e. ahead of time): le code Javascript généré est compilé au moment du build de façon à optimiser son chargement par le browser.
  • "vendorChunk" permet de ne pas utiliser un bundle séparé pour le code des dépendances.
  • "buildOptimizer": des optimisations sont appliquées au moment de la compilation et tout le code et ses dépendances externes se trouvent dans un bundle unique.

    Cette option applique un "tree-shaking" sur le code JS généré c'est-à-dire que le bundle résultant ne contient pas tout le code des dépendances. Il contient seulement le code des dépendances qui est réellement utilisé.

Lancer un service web de développement (ng serve)

La commande est (à exécuter dans le répertoire du workspace):

ng serve <options> 
ng s <options> 

Avant de lancer un service web pour exécuter l'application, une build est effectuée.

Options Commentaires
Pour lancer un browser au moment d'exécuter l'application
--open 
-o
Le browser par défaut est ouvert.
Lancer l'exécution sur un port spécifique
--port <numéro du port>
Le port par défaut est 4200.
Recharger la page si le code est modifié
--live-reload
Lancer le service web en utilisant HTTP
--ssl
Recompile en cas de changement du code
--watch 

Exécuter une target (ng run)

Les targets sont définies pour chaque projet dans le fichier angular.json au niveau du paramètre "architect" d'un projet.

Par exemple:

{ 
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 
  "version": 1, 
  "newProjectRoot": "projects", 
  "projects": { 
    "mynewapplication": { 
      "projectType": "application", 
      "schematics": {}, 
      "root": "", 
      "sourceRoot": "src", 
      "prefix": "app", 
      "architect": { 
        "build": {}, 
        "serve": {}, 
        "extract-i18n": {}, 
        "test": {}, 
        "lint": {}, 
        "e2e": {} 
      } 
    } 
  }, 
  "defaultProject": "mynewapplication" 
} 

On peut lancer l'exécution d'une target en exécutant:

ng run <nom projet>:<nom de la target> 

Par exemple dans l'exemple précédent pour exécuter la target "build" du projet "mynewapplication":

ng run mynewapplication:build 

On peut préciser une configuration:

ng run <nom projet>:<nom de la target> --configuration <nom configuration> 
ng run <nom projet>:<nom de la target> -c <nom configuration> 

Exécuter les tests (ng test et ng e2e)

Exécuter les tests unitaires

Les tests unitaires peuvent être exécutés en lançant la commande:

ng test <options> 
ng t <options> 

Ces tests se trouvent au niveau de chaque composants dans des fichiers nommés <nom du composant>.spec.ts

Les tests peuvent être lancés pour un projet spécifique en exécutant:

ng test <nom du projet> <options> 
ng t <nom du projet> <options> 

Les tests sont exécutés par un outil appelé karma. Cet outil indique le résultat des tests sur un browser à l'adresse par défaut localhost:9876 (le port est paramétrable dans karma.conf.js).

Options Commentaires
Pour générer un rapport contenant la converture de test
--code-coverage
Le résultat est généré dans un répertoire nommé /coverage.

On peut indiquer un répertoire particulier dans le fichier karma.conf.js:

coverageIstanbulReporter: { 
      dir: require('path').join(__dirname, './coverage/<nom application>'), 
      ... 
    },
Pour indiquer la progression sur la console lors de l'exécution
--progress
Pour exécuter les tests une seule fois
--watch false
Comment débugguer un test unitaire ?
  1. Exécuter le test avec ng test
  2. Quand la page de karma est affichée dans le browser, cliquer en haut à droite sur DEBUG
  3. Afficher les outils développeur:
    • Sous Firefox: on peut utiliser la raccourci [Ctrl] + [Maj] + [J] (sous MacOS: [⌘] + [Maj] + [J], sous Linux: [Ctrl] + [Maj] + [K]).
    • Sous Chrome: utiliser le raccourci [F12] (sous MacOS: [⌥] + [⌘] + [I], sous Linux: [Ctrl] + [Maj] + [I]).
    • Sous EDGE: utiliser le raccourci [F12].
  4. Aller dans l'onget "Sources"
  5. Taper [Ctrl]+[P] pour écrire le nom d'un fichier de code à débugguer
  6. Mettre un point d'arrêt dans le code du fichier <nom composant>.spec.ts.
  7. Recharger la page

Exécuter les tests de "bout en bout" (i.e. end-to-end)

Il faut lancer la commande:

ng e2e 

La configuration de ces tests se trouve dans e2e/protractor.conf.js. Par défaut, ces tests se trouvent dans le répertoire e2e/src dans des fichiers nommés <nom du fichier>.e2e-spec.ts.

Gérer des packages NPM

Installer un package NPM (ng add)

ng add <nom package> 

Les packages installés seront indiqués dans le fichier package.json avec les paramètres :

  • dependencies pour indiquer les packages utilisés pour le code déployé et pour le développement.
  • devDependencies pour indiquer les packages utilisés pour le développement seulement.

Mettre à jour un package NPM (ng update)

Sans options, la commande suivante permet d'indiquer les dépendances à mettre à jour et quelles sont les commandes à exécuter pour les mettre à jour:

ng udpate 

Pour mettre à jour un package spécifique:

ng update <nom package> <options> 

Pour que les mises à jour soient effectuées, il ne doit pas y avoir de fichiers modifiés et non commités (en sens Git).

Options Commentaires
Pour mettre à jour tous les packages présent dans le fichier package.json
--all
Forcer les mises à jour même en cas d'incompatibilité
--force
Indiquer une liste de packages à mettre à jour
--packages <liste des packages>
Forcer la mise à jour même dans le cas où des modifications n'ont pas été commitées
--allow-dirty

Pour d'autres commandes concernant les packages NPM, voir Les commandes npm courantes en 5 min.

Afficher les problèmes d'implémentation (ng lint)

Parcourt le code pour afficher les problèmes d'implémentation

ng lint 

Pour restreindre le parcours à un projet spécifique:

ng lint <nom du projet> 
Options Commentaires
Pour corriger les problèmes détectés
--fix 
Les fichiers sont modifiés
Pour changer la présentation des problèmes
--format <nom format>
Parmi les formats: json, stylish...
Pour exclure des fichiers lors du parcours
--exclude <chemin des fichiers à ignorer>
Le chemin peut contenir des wildcards.

Pour résumer...

Créer une nouvelle application
ng new <nom appli.> <options> 
ng n <nom appli.> <options> 
Détails | Documentation
Générer un nouvel élément Composant
ng g component <nom composant> 
ng g c <nom composant>
Détails | Documentation
Application
ng g application <nom appli.> 
ng g app <nom appli.> 
Détails | Documentation
Bibliothèque
ng g library <nom bibliothèque> 
ng g lib <nom bibliothèque> 
Détails | Documentation
Module
ng g module <nom module> 
ng g m <nom module> 
Détails | Documentation
Service
ng g service <nom service> 
ng g s <nom service>
Détails | Documentation
Classe
ng g class <nom classe> 
ng g cl <nom classe>
Détails | Documentation
Interface
ng g interface <nom interface> 
ng g i <nom interface> 
Détails | Documentation
Enum
ng g enum <nom enum> 
ng g e <nom enum> 
Détails | Documentation
Guard
ng g guard <nom guard> 
ng g g <nom guard>
Détails | Documentation
Pipe
ng g pipe <nom pipe> 
ng g p <nom pipe> 
Détails | Documentation
Construire une application
ng build <nom du projet> <options> 
ng b <nom du projet> <options> 
Détails | Documentation
Lancer un service web de développement
ng serve <options> 
ng s <options> 
Détails | Documentation
Exécuter une target
ng run <nom projet>:<nom de la target> 
Détails | Documentation
Exécuter des tests Exécuter des tests unitaires
ng test <nom du projet> <options> 
ng t <nom du projet> <options> 
Détails | Documentation
Exécuter les tests de "bout en bout"
ng e2e
Détails | Documentation
Gérer les packages NPM Installer un package
ng add <nom package> 
Détails | Documentation
Mettre à jour un package
ng update <nom package>
Détails | Documentation
Afficher les problèmes d'implémentation
ng lint 
Détails | Documentation
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Aide-mémoire sur les pointeurs et références en C++

Le but de cet article est de rappeler quelques éléments de syntaxe sur les pointeurs et références en C++.

On considère la classe suivante:

Point.h Point.cpp
class Point
{
public:
    Point();
    Point(int a, int b);
    Point(const Point &copy);
    ~Point();

    int a;
    int b;
};

Point::Point()
{}

Point::Point(int a, int b)
{
  this->a = a;
  this->b = b;
}

Point::Point(const Point &copy)
{
  this->a = copy.a;
  this->b = copy.b;
}

Point::~Point()
{
}

Déclaration par valeur

  • Les objets déclarés de cette façon ont une existence liée au scope dans lequel ils sont déclarés. Par exemple, un objet déclaré "par valeur" dans une fonction aura une existence liée au corps de la fonction. Si on sort de cette fonction, l'objet sera détruit implicitement, il n'est pas nécessaire d'implémenter une instruction pour détruire l'objet.
  • Les affectations de ces objets effectuent des copies "par valeur" c'est-à-dire qu'en absence de constructeur de copie, l'objet est copié intégralement c'est-à-dire que la copie de l'objet est effectuée membre par membre.
  • La plupart du temps, les objets de type primitif (int, bool, char, float etc...) sont déclarés de cette façon.
  • Les copies d'objets déclarés "par valeur" peuvent représenter un coût non négligeable en performance si les objets sont de grande taille.

Quelques exemples:

Syntaxe Remarques
Déclaration + initialisation
(sans valeur d'initialisation)
Point newPoint;
  • Il n'est pas possible d'effectuer seulement une déclaration. Cette syntaxe n'effectue pas seulement une déclaration, elle permet d'exécuter le constructeur par défaut.
  • La classe doit comporter un constructeur par défaut:
    Point::Point()
    {}
Déclaration + initialisation
(avec valeur d'initialisation)
Point newPoint(2, 4);

ou

Point newPoint = Point(2, 4);

ou

Point newPoint{2, 4};

ou

Point newPoint = {2, 4};
Implique l'existence du constructeur suivant:

Point::Point(int a, int b)
{
  this->a = a;
  this->b = b;
}

Dans le cas de la syntaxe suivante:
Point newPoint = Point(2, 4);
2 opérations sont effectuées même si le compilateur les optimise pour n'en faire qu'une seule:

  • L'instanciation d'un objet en utilisant le constructeur précédent et
  • L'instanciation d'un autre objet en utilisant le constructeur de copie (si ce constructeur n'existe pas explicitement, il est rajouté par le compilateur).
Point firstPoint(2, 4);
Point newPoint(firstPoint);

ou

Point firstPoint(2, 4);
Point newPoint{firstPoint};
Le constructeur de copie ne doit pas être obligatoirement déclaré pour écrire cette ligne. Il sera rajouté implicitement par le compilateur.
Copie
Point newPoint(2, 4);
Point otherPoint = newPoint;
  • Cette ligne effectue une copie par valeur, newPoint et otherPoint sont des objets différents.
  • Implique un constructeur de copie (copy constructor):
    Point::Point(const Point &point)
    {
      this->a = point.a;
      this->b = point.b;
    }
    

    Ce constructeur est créé par le compilateur s'il n'existe pas.

Accès aux membres avec '.'
Point newPoint(2, 4);
cout << newPoint.a << "\n";

Référence

  • Une variable contenant une référence d'un objet peut être considérée comme un alias de cet objet c'est-à-dire que toutes les modifications effectuées sur la référence impacte l'objet lui-même.
  • Utiliser des références permet d'éviter des copies d'objets "par valeur" lors des passages d'arguments ou des affectations.
  • Il n'existe pas de référence nulle.
  • Il existe 2 types de références (voir les différences entre Lvalue et Rvalue):
    • Référence Lvalue (i.e. Lvalue reference): ce sont les références usuelles déclarées avec un caractère '&'. Les références Lvalue permettent de référencer les arguments Lvalue d'une expression.
    • Référence Rvalue (i.e. Rvalue reference): les références Rvalue permettent de référencer les arguments Rvalue d'une expression. Ces références sont indiquées avec les caractères '&&'. Il s'agit d'une optimisation pour améliorer les performances dans le cas de manipulations d'objet déclaré "par valeur" de façon à éviter d'effectuer des copies "par valeur" lors des affectations.
  • Les références conviennent pour les données membres d'une classe quand l'instance de ce membre existe pendant toute la durée de vie de la classe.
Différences entre Lvalue et Rvalue

Dans une expression, un argument Lvalue (i.e. left-value) correspond à un objet possédant un nom. Une variable possède un nom et permet de désigner un objet en mémoire.

Par exemple:

int x = 1; // x est une Lvalue

Lvalue sous-entends qu'un argument de ce type est à gauche de l'opérateur d'affectation ce qui n'est pas forcément le cas. Une Lvalue peut apparaître à gauche ou à droite de l'opérateur d'affectation.

Par exemple, si on écrit:

int a = 1;
int b = 2;
a = b; 
b = a; 

a et b sont à la fois Lvalue et Rvalue.

Dans une expression, un argument Rvalue (i.e. right-value) correspond à une valeur temporaire qui n'est pas persistée ou accessible en utilisant une variable.

Par exemple:

int i = 2; // 2 est une Rvalue
int GetValue()
{
  //
}

int i = GetValue();

GetValue() est une Rvalue par contre i est une Lvalue.

Une Rvalue sert à affecter une valeur à une variable, elle est temporaire car il n'existe pas de nom pour y faire référence. Une Rvalue est toujours située à droite de l'opérateur d'affectation.

Par exemple, si on écrit le code suivant:

int a = 1;
int b = 2;

a + b = 3; // ERREUR

L'affectation n'est pas possible car le résultat de l'opération a + b est une Rvalue. En effet, l'opération ne fait pas référence à une variable, il s'agit d'une valeur temporaire. Une Rvalue doit toujours être à droite de l'opérateur d'affectation.

Quelques exemples:

Syntaxe Remarques
Déclaration + initialisation
(sans paramètres)
Point &newPoint();

ou

Point & newPoint();

ou

Point& newPoint();
  • Toutes ces notations sont équivalentes.
  • Il n'est pas possible d'effectuer seulement une déclaration d'une référence. Ce code n'effectue pas seulement une déclaration comme on pourrait le penser mais il exécute aussi le constructeur par défaut.
  • Chacune de ces lignes implique un constructeur par défaut.
Déclaration + initialisation
(avec paramètres)
Point newPoint;
Point &pointRef = newPoint;

ou

Point newPoint;
Point &pointRef(newPoint);
Un constructeur par défaut est nécessaire pour écrire la ligne:
Point newPoint;
const Point &pointRef = Point(2, 4);

ou

const Point &pointRef(Point(2, 4));
Référence constante.
Point &newPoint(2, 4);
Il n'est pas possible d'initialiser une référence de cette façon sans utiliser const.
Initialisation obligatoire
Point &newPoint; // ERREUR
ATTENTION: entraîne une erreur de compilation.
Référence Lvalue (i.e. Lvalue reference)
Point &newPoint = Point(1, 2);
Entraîne une erreur de compilation car Point(1, 2) est une Rvalue et la référence déclarée avec Point &newPoint est une référence Lvalue.
const Point &newPoint = Point(1, 2);
Cette ligne n'entraîne pas d'erreurs.
Une autre solution pourrait être d'utiliser une référence Rvalue (sans utiliser const).
Accès aux membres de l'objet référencé avec '.'
Point newPoint(2, 4);
Point &pointRef = newPoint;
newPoint.a = 6;
newPoint.b = 9;
L'accès aux membres de l'objet se fait avec '.'.
La modification de la référence modifie l'objet
Point newPoint(1, 2);
Point &pointRef = newPoint;
pointRef.a = 6;
cout << newPoint.a << "\n";
L'affichage est:
6.
Les modifications de l'objet sont visibles pour les autres références
Point newPoint(1, 2);
Point &firstRef = newPoint;
Point &secondRef = newPoint;
firstRef.a = 6;
cout << secondRef.a << "\n";
L'affichage est:
6.
Ré-affectation d'une référence
Point firstPoint(1, 2);
Point secondPoint(3, 4);
Point &refPoint = firstPoint;
refPoint = secondPoint;
refPoint référence désormais secondPoint.
Pas de référence nulle
Point &newPoint = NULL; // ERREUR
Impossible, la référence nulle n'existe pas.
Opérateur "address-of":
'&' permet d'obtenir un pointeur vers l'objet
Point firstPoint(1, 2);
Point &pointRef = firstPoint;
Point *pointer = &pointRef;
L'opérateur '&' ne peut être utilisé qu'avec la partie droite de l'opérateur '=' (appelé RHS pour Right Hand Side).

ATTENTION:
Le pointeur reste lié à l'objet d'origine. Si cet objet est détruit dans le cas où l'exécution se fait en dehors du scope de la déclaration de la variable firstPoint, le pointeur sera corrompu.

Référence constante
const Point &pointRef = Point(1, 2);
pointRef.a = 5; // ERREUR
La ligne pointRef.a = 5 entraîne une erreur de compilation car la référence est constante.
const Point &pointRef = Point(1, 2);
Point otherPoint(2, 4);
pointRef = otherPoint; // ERREUR
Les modifications sur la référence ne sont pas possibles.
const Point &pointRef = Point(1, 2);

est équivalent à:

Point const &pointRef = Point(1, 2);
Ces 2 lignes sont équivalentes.
Point point(1, 2);
Point & const pointRef = point;
const est ignoré par le compilateur dans ce cas.
Référence Rvalue L'utilisation de référence Rvalue est une optimisation permettant de transferer un objet dynamiquement alloué en mémoire d'un objet à l'autre sans avoir à l'affecter à une variable.
Par exemple, il n'est pas possible d'écrire la ligne suivante sans const car Point(1, 2) est une Rvalue et pointRef est une référence Lvalue:

const Point &pointRef = Point(1, 2);  // OK
Point &pointRef = Point(1, 2);  // ERREUR

On peut toutefois utiliser une référence Rvalue:

Point &&pointRef = Point(1, 2);  // OK

Move Semantics
L'intérêt de cette fonctionnalité est de permettre de déplacer directement des objets alloués dynamiquement sans avoir à effectuer de nouvelles allocations et sans effectuer de copies en mémoire (cette technique est appelée Move Semantics).
Par exemple, si on définit la fonction:

void UsePoint(Point &&point)
{
    // ...
}

On peut appeler cette fonction directement avec une Rvalue sans avoir à affecter l'argument à une variable au préalable:

UsePoint(Point(1, 2));

Sans référence Rvalue, il aurait fallu effectuer une affectation au préalable:

void UsePoint(Point &point)
{
    // ...
}

Point point(1, 2); // Affectation de la variable point
UsePoint(point);

Perfect forwarding
L'utilisation de références Rvalue permet de définir moins de surcharges de fonctions. Au lieu de déclarer des surcharges avec T& et const T&, on peut directement utiliser une référence Rvalue T&&.

Par exemple, sans référence Rvalue, il faut définir les surcharges suivantes...:

void UsePoint(Point &point)
{
    // ...
}

void UsePoint(const Point &point)
{
    // ...
}

...pour être capable d'effectuer les 2 appels suivants:

Point point(1, 2);
UsePoint(point); // cet appel utilise la surcharge avec Point &point.
UsePoint(Point(3, 4)); // cet appel utilise la surcharge avec const Point &point.

Avec une réference Rvalue, une seule fonction suffit:

void UsePoint(Point &&point)
{
    // ...
}

Pour tirer complétement partie de la fonctionnalité Move Semantics, il faut définir une constructeur de déplacement (i.e. Move Constructor) et éventuellement définir une surcharge à l'opérateur d'affectation:

// Constructeur de déplacement
Point(Point &&otherPoint)
{
    // ...
}

// Surchage de l'opérateur d'affectation
Point &operator=(Point &&otherPoint)
{
    // ...
}

L'implémentation du constructeur et de la surcharge de l'opérateur doit respecter certaines règles pour être efficace (voir Move Constructors and Move Assignment Operators (C++) pour plus de détails).

Pointeur

Quelques remarques générales sur les pointeurs:

  • Une variable de type pointeur contient une adresse permettant de pointer avec un typage fort vers un objet en mémoire.
  • Les objets pointés par une variable de type pointeur peuvent être alloués sur la pile ou dans le tas suivant comment ils ont été déclarés. Si on initialise ces objets avec l'opérateur new, ils sont alloués dans le tas. Toutefois il est possible d'obtenir un pointeur vers un objet alloué sur la pile avec l'opérateur '&'.
  • Quand un objet est instancié avec new, il doit être libéré en utilisant delete.
  • Les pointeurs conviennent en tant que données membres d'une classe lorsqu'il sera nécessaire de libérer l'instance de l'objet pointé lors de la durée de vie de la classe.

Quelques exemples:

Syntaxe Remarques
Déclaration
Point* newPoint = NULL;

ou

Point * newPoint = NULL;

ou

Point *newPoint = NULL;
Ces 3 notations sont équivalentes.
Déclaration + initialisation
Point *newPoint = new Point(1, 2);
Accès aux membres de l'objet avec l'opérateur '->'
Point *newPoint = new Point(1, 2);
cout << newPoint->a < "\n";
cout << newPoint->b < "\n";
Pointeur nul
Point *newPoint = NULL;
Initialisation à partir d'une référence en utilisant
l'opérateur de déférencement '&'
Point &newPoint(1, 2);
Point *pointer = &newPoint;
L'opérateur '&' ne peut être utilisé que pour la partie droite de l'opérateur d'affectation (appelé RHS pour Right Hand Side).
Obtenir l'objet pointé avec l'opérateur '*' et
affecter une copie de l'objet pointé
Point *pointer = new Point(1,3);
Point objectCopy = *pointer;

objectCopy.a = 6;
std::cout << pointer->a << "\n"; // le résultat est 1
L'opérateur '*' peut être utilisé dans la partie droite de l'opérateur d'affectation (RHS pour Right Hand Side).

ATTENTION: objectCopy est une copie de l'objet pointé par pointer.

Obtenir une référence d'un objet à partir d'un pointeur
avec l'opérateur '*'
Point *pointer = newPoint(1,3);
Point &pointRef = *pointer;

pointer->a = 6;
std::cout << pointer->a << "\n"; // Le résultat est 6
On affecte une variable contenant une référence de l'objet.
Le pointeur possède un typage fort
Point *pointer = new Point(1, 3);
Line *newLine = newPoint; // ERREUR
ATTENTION: impossible, cette ligne entraîne une erreur de compilation car les types sont différents
Ré-affectation d'un pointeur
Point *firstPoint = new Point(1, 3);
Point *secondPoint = new Point(3, 6);
secondPoint = firstPoint;
Le pointeur est copié "par valeur" toutefois les 2 pointeurs pointent vers le même objet.
Ré-affectation de l'objet pointé
Point *firstPoint = new Point(3, 6);
Point newPoint(1, 2);
*firstPoint = newPoint;

firstPoint->a = 9;
cout << newPoint->a << "\n";
  • Le résultat est 1.
  • A la ligne:
    *firstPoint = newPoint;
    newPoint est copié "par valeur" dans l'objet pointé par firstPoint.
  • newPoint n'est pas modifié à la ligne:
    firstPoint->a = 9;
  • L'opérateur '*' peut être utilisé dans la partie gauche de l'opération d'affectation (appelé LHS pour Left Hand Side).
Utilisation d'un pointeur non typé void *
Point newPoint(1, 2);
Point *pointer = &newPoint;
void *untypedPointer = pointer;

Point *otherPoint = static_cast<Point *>(untypedPointer);
untypedPointer est un pointeur non typé.
Suppression d'un objet alloué
Point *pointer = new Point(1, 3);
delete pointer;
Avec l'opérateur new, l'objet est alloué dans le tas, il faut penser à le supprimer après utilisation pour éviter une fuite mémoire
Un pointeur vers un pointeur
Point *pointer = new Point(1, 2);
Point **pointerToPointer = &pointer;
(*pointerToPointer)->a = 5;
cout << pointer->a << "\n";
Le résultat est:
5
Valeur pointée constante
const Point *pointA = new Point(1, 2);

pointA->a = 5; // ERREUR
On ne peut pas modifier l'objet pointé, donc cette ligne provoque une erreur de compilation.
const Point *pointA = new Point(1, 2);
Point *pointB = new Point(3, 4);
pointA = pointB;
On peut modifier la valeur du pointeur.
const Point *pointA = new Point(1, 2);

est équivalent à:

Point const *pointA = new Point(1, 2);
Pointeur constant
Point * const pointA = new Point(1, 2);
pointA->a = 5;
L'objet pointé peut être modifié.
Point * const pointA = new Point(1, 2);
Point *pointB = new Point(3, 4);
pointA = pointB; // ERREUR
Le pointeur est constant et ne peut pas être modifié.

Référence managée (en C++/CLI)

  • Une référence managée est semblable aux références en C#.
  • Les objets déclarées sous forme de références managées sont instanciés dans le tas managé. Leur durée de vie est géré pour la Garbage collector.
  • Il n'est pas nécessaire de libérer les objets déclarés en tant que référence managée.
  • Ce code n'est valable que si l'exécutable supporte du code CLR. Les références managées sont aussi appelées handle.

On considère la classe Line telle que:

Line.h Line.cpp
public ref class Line
{
public:
    Line();
    Line(int x1, int y1, int x2, int y2);

    int x1;
    int y1;
    int x2;
    int y2;
};

Line::Line()
{}

Line::Line(int x1, int y1, int x2, int y2)
{
    this->x1 = x1;
    this->y1 = y1;
    this->x2 = x2;
    this->y1 = y1;
}

Quelques exemples:

Syntaxe Remarques
Déclaration
Line^ line;

ou

Line ^ line;

ou

Line ^line;
Les 3 notations sont équivalentes.
Déclaration + initialisation
Line ^line = gcnew Line(1, 2, 3, 6);
Accès aux membres de l'objet avec l'opérateur '->'
Line ^line = gcnew Line(1, 2, 3, 6);
cout << line->x1 << "\n";
cout << line->x2 << "\n";
Pointeur nul
Line ^line = nullptr;
Suppression d'une instance
Line ^line = gcnew Line(1, 2, 3, 6);
delete line;
  • line est alloué sur le tas managé géré par le Garbage Collector donc il n'y a pas de risque de fuite mémoire.
  • delete line sert seulement pour exécuter le destructeur de line volontairement. C'est l'équivalent du pattern IDisposable en C#.

Pointeur et réference dans les appels de fonctions

Arguments de fonctions

Quelques exemples de passage d'arguments à une fonction:

Syntaxe Remarques
Copie par valeur Déclaration de la fonction:

void MovePoint(Point point, 
  int newA, int newB)
{
  point.a = newA;
  point.b = newB;
}

Syntaxe de l'appel:

Point newPoint(1, 2);
Point::MovePoint(newPoint, 2, 4);
std::cout << newPoint.a << "\n";
std::cout << newPoint.b << "\n";
Le résultat est:
1
2

L'argument est passé par valeur, une copie est effectuée lors de l'appel.
Si on modifie l'objet à l'intérieur de la fonction, on modifie la copie et non l'original.
Cet appel implique un constructeur de copie (copy constructor).

Copie par référence Déclaration de la fonction:

void MovePoint(Point &point, 
  int newA, int newB)
{
  point.a = newA;
  point.b = newB;
}

Syntaxe de l'appel:

Point newPoint(1, 2);
Point::MovePoint(newPoint, 2, 4);
std::cout << newPoint.a << "\n";
std::cout << newPoint.b << "\n";
Le résultat est:
2
4

L'argument est passé par référence.
Si on modifie l'objet en utilisant la référence à l'intérieur de la fonction, on modifie l'original.

Utilisation d'un pointeur Déclaration de la fonction:

void MovePoint(Point *point, 
  int newA, int newB)
{
  point->a = newA;
  point->b = newB;
}

Syntaxe de l'appel:

Point newPoint(1, 2);
Point::MovePoint(&newPoint, 2, 4);
std::cout << newPoint.a << "\n";
std::cout << newPoint.b << "\n";
Le résultat est:
2
4

L'argument contient un pointeur.
Si on modifie l'objet dans la fonction, on modifie l'original.

Copie d'une référence managée
(En C++/CLI)
Déclaration de la fonction:

void MoveLine(Line ^line, 
  int newX1, int newY1, 
  int newX2, int newy2)
{
  line->x1 = newX1;
  line->y1 = newY1;
  line->x2 = newX2;
  line->y2 = newY1;
}

Syntaxe de l'appel:

Line ^line = gcnew Line(1, 2, 3, 4);

Line::MoveLine(line, 2, 4, 6, 8);
Console::WriteLine(line->x1);
Console::WriteLine(line->y1);
Console::WriteLine(line->x2);
Console::WriteLine(line->y2);
Le résultat est:
2
4
6
8

L'argument est passé par référence.
Si on modifie l'objet en utilisant la référence à l'intérieur de la fonction, on modifie l'instance originale.

Retour de fonction

Syntaxe Remarques
Par valeur Déclaration de la fonction:

Point CreatePoint(int a, int b)
{
  return Point(a, b);
}

Syntaxe de l'appel:

Point newPoint = 
  Point::CreatePoint(1, 2);

std::cout << newPoint.a << "\n";
std::cout << newPoint.b << "\n";
Le résultat est:
1
2

Une copie de l'objet créée dans la fonction, est retournée.

Par référence
(MAUVAISE IMPLEMENTATION)
Déclaration de la fonction:

Point &CreatePoint(int a, int b)
{
  Point newPoint(a, b);
  return newPoint;
}

Syntaxe de l'appel:

Point &newPoint = 
  Point::CreatePoint(1, 2);
Console::WriteLine(newPoint.a);
Console::WriteLine(newPoint.b);
Le résultat est:
1935459609
-1

ATTENTION: ne pas retourner une référence vers un objet créé dans la fonction.

Dans cet exemple, newPoint est alloué dans la pile. Quand on sort de la fonction, newPoint est supprimé de la pile et la référence retournée contient un objet supprimé.

Par référence
(A EVITER)
Déclaration de la fonction:

Point &CreatePoint(int a, int b)
{
  Point *newPoint = new Point(a, b);
  return *newPoint;
}

Syntaxe de l'appel:

Point &newPoint = 
  Point::CreatePoint(1, 2);
Console::WriteLine(newPoint.a);
Console::WriteLine(newPoint.b);
Le résultat est:
1
2

Un objet est créé et alloué sur le tas dans le corps de la fonction. On retourne une référence vers cet objet.
Il faut éviter cette implémentation car elle peut mener à des fuites mémoires, l'objet n'est jamais supprimé du tas.

Par pointeur Déclaration de la fonction:

Point *Point::CreatePoint(int a, int b)
{
  return new Point(a, b);
}

Syntaxe de l'appel:

Point *newPoint = 
  Point::CreatePoint(1, 2);

Console::WriteLine(newPoint.a);
Console::WriteLine(newPoint.b);
delete newPoint;
Le résultat est:
1
2

Ne pas oublier de supprimer l'objet après utilisation.

Par référence managée Déclaration de la fonction:

Line ^CreateLine(int x1, int y1, 
  int x2, int y2)
{
  return gcnew Line(x1, y1, x2, y2);
}

Syntaxe de l'appel:

Line ^line = 
  Line::CreateLine(1, 2, 3, 4);
Console::WriteLine(line->x1);
Console::WriteLine(line->y1);
Console::WriteLine(line->x2);
Console::WriteLine(line->y2);
Le résultat est:
1
2
3
4

L'objet est créé dans le tas managé, il n'y a pas de nécessité de le supprimer.

Membres d'une classe

Quelques exemples de déclarations d'objets membres d'une classe:

Syntaxe Remarques
Par valeur
(A éviter dans le cas d'objet complexe)
Définition .h:

#include "Point.h"

class Circle
{
public:
    Circle(Point center, int radius);

    Point center;
    int radius;
};

Définition .cpp:

#include "Circle.h"

Circle::Circle(Point center, int radius)
{
    this->center = center;
    this->radius = radius;
}
  • La déclaration suivante nécessite un constructeur par défaut dans la classe Point:
    Point center;
  • Le passage d'argument dans le constructeur implique un constructeur de copie.
  • Il faut éviter ce type de membre si l'objet est complexe car des copies sont effectuées lors des initialisations et passage d'arguments.
Par référence Définition .h:

#include "Point.h"

class Circle
{
public:
    Circle(Point center, int radius);

    Point &center;
    int radius;
};

Définition .cpp:

#include "Circle.h"

Circle::Circle(Point &center, int radius) :
  center(center)
{
  this->center = center);
  this->radius = radius;
}
  • La référence doit être initialisée dans le constructeur (car la référence nulle n'existe pas).
  • La classe Circle ne peut pas implémenter de constructeur par défaut.
  • Il est conseillé d'utiliser un membre de type référence si la durée de vie du membre est aussi longue que celle de la classe.
Par référence (avec const) Définition .h:

#include "Point.h"

class Circle{
public:
     Circle();
     
     const Point &center;
     int radius;
};

Définition .cpp:

#include "Circle.h"

Circle::Circle() :center(Point(0, 0))
{}
Par pointeur Définition .h:

#include "Point.h"

class Circle
{
public:
    Circle(Point &center, int radius);

    Point *center;
    int radius;
};

Définition .cpp:

#include "Circle.h"

Circle::Circle(Point &center, int radius)
{
  this->center = &center;
  this->radius = radius;
}
  • Cette implémentation convient quand on doit instancier ou détruire le membre pendant la durée de vie de la classe.
  • Attention à la destruction du membre quand la classe est détruite.
Par référence managée
(classe managée)
Définition .h:

#include "Line.h"

public ref class Canvas
{
public:
    Canvas(Line ^line);

    Line ^line;
};

Définition .cpp:

#include "Canvas.h"

Canvas::Canvas(Line ^line)
{
  this->line = line;
}
Le membre de type managée doit se trouver dans une classe managée.
Le comportement est le même qu'en C#.

Comparaison d'objets

Pour être capable de comparer des objets complexes, il faut surcharger l'opérateur d'égalité.

Par exemple:

bool operator==(const Point &pointA, const Point &pointB)
{
    return pointA.a == pointB.a && pointA.b == pointB.b;
};

Si les membres d'une classe sont privés, on doit ajouter la déclaration suivante dans le fichier .h de la classe pour que la surchage de l'opérateur puisse accèder à ces membres:

friend bool operator==(const Point &pointA, const Point &pointB);

L'implémentation de la surcharge de l'opérateur indiquée plus haut suffit à effectuer les comparaisons suivantes:

Syntaxe Remarques
Comparaison d'objets définis "par valeur"
Point firstPoint(1, 2);
Point secondPoint(3, 4);
Point thirdPoint(3, 4);

if (firstPoint == secondPoint)
    cout &kt;&kt; "KO\n";
else if (secondPoint == thirdPoint)
    cout &kt;&kt; "OK\n";
Le résultat est:
OK

On peut aussi surcharger l'opérateur de cette façon:

bool operator==(Point pointA, Point pointB)
{
    return pointA.a == pointB.a 
        && pointA.b == pointB.b;
};
Comparaison de références
const Point &firstRef(1, 2);
const Point &secondRef(3, 4);
const Point &thirdRef(3, 4);

if (firstPoint == secondPoint)
    cout << "KO\n";
else if (secondPoint == thirdPoint)
    cout << "OK\n";
Le résultat est:
OK

Si on surcharge l'opérateur de cette façon (sans const):

bool operator==(Point pointA, Point pointB)
{
    return pointA.a == pointB.a 
        && pointA.b == pointB.b;
};

On obtiendra une erreur de compilation.

Comparaison de pointeurs
Point *firstPointer = new Point(1, 2);
Point *secondPointer = new Point(3, 4);
Point *thirdPointer = new Point(3, 4);

if (*firstPointer == *secondPointer)
    cout << "KO\n";
else if (*secondPointer == *thirdPointer)
    cout << "OK\n";
Le résultat est:
OK

Il n'est pas possible de déclarer la surcharge suivante:

bool operator==(Point *pointA, Point *pointB)
{
    return pointA->a == pointB->a 
        && pointA->b == pointB->b;
};

Ce type de surcharge pourrait préter à confusion dans le cas où on compare des pointeurs directement:

if (firstPointer == secondPointer)
{
    // ...
}
Références
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page