Imbriquer deux composants (différence entre ViewChild et ContentChild)

L’interface d’une application Angular se compose de composants imbriqués les uns dans les autres. Dans cette recette, je vous explique les deux manières d’imbriquer des composants : la technique ViewChild et la technique ContentChild.

Technique ViewChild

Si un composant A est affiché dans le template d’un composant B, alors A est un ViewChild de B.

// Ce composant est l'enfant
@Component({
  selector: 'child',
  template: '<p>I am the child</p>'
})
export class ChildComponent {}

// Ce composant est le parent : son template contient la balise de l'enfant
@Component({
  selector: 'parent',
  template: `<div>
    I am the parent. My child is: <child></child>
  </div>`
})
export class ParentComponent {}

C’est la syntaxe la plus fréquente pour imbriquer deux composants Angular.

Ses avantages sont :

  • Encapsulation. Quand on affiche le composant parent, on ne doit pas se préoccuper de son contenu. S’il contient une structure complexe de plusieurs composants enfant, ces derniers sont affichés automatiquement via leur parent.
  • Communication parent-enfant facilitée. Le parent peut très facilement transmettre des données à ses enfants via des inputs, et recevoir des données de ses enfants via des outputs. Voir Comprendre les Inputs et Outputs de composant.

Mais il y a un inconvénient :

  • Le lien parent-enfant est totalement opaque. Quand on affiche le composant parent, impossible de savoir s’il a des enfants juste en regardant sa balise. Il faut aller voir sans son template pour vérifier ce qu’il contient.
<!-- Impossible de savoir si ce composant a des composants enfants... -->
<parent></parent>

Technique ContentChild

On peut également imbriquer deux composants en écrivant la balise de l’enfant à l’intérieur de la balise du parent :

<parent>
  <child></child>
</parent>

On dit alors que le composant <child> et un ContentChild du composant <parent>. Finalement, c’est la même syntaxe que quand on imbrique des balises en HTML classique.

ATTENTION. Pour rendre la syntaxe ContentChild possible, le template du composant parent doit contenir une balise <ng-content>. Cette dernière sera automatiquement remplacée par le contenu qui se trouve entre la balise ouvrante et la balise fermante du parent.

Ainsi, si l’on prend le composant parent suivant :

@Component({
  selector: 'parent',
  template: `<p>I am the parent.</p>
             <ng-content></ng-content>`  // Notez la balise <ng-content>
})
export class ParentComponent {}

Et qu’on l’utilise ainsi, en plaçant du contenu entre ses balises :

<parent>
  <em>Hello!</em>
</parent>

Cela produit le HTML suivant :

<p>I am the parent.</p>
<em>Hello!</em>  <!-- ng-content a été remplacé par le contenu -->

Cette technique présente un gros avantage :

  • Flexibilité. L’utilisateur peut afficher un contenu différent dans le composant parent à chaque fois qu’il l’utilise, plutôt que de définir ce contenu “en dur” dans le template du parent. Cette technique est ainsi très utilisée pour créer une librairie de composants réutilisables, car on veut permettre aux utilisateurs de la librairie de personnaliser le contenu affiché dans les composants.

Il y a toutefois quelques inconvénients :

  • Ne fonctionne pas automatiquement. Il faut penser à utiliser <ng-content> dans le template du composant parent.
  • Impossible d’utiliser les inputs/outputs pour communiquer entre le parent et l’enfant. NDLR: Pertinent uniquement si le contenu du parent est un autre composant.
  • Pas d’encapsulation. Puisque le parent et l’enfant sont affichés dans le même template, ils ont tous les deux accès au même scope. NDLR: Pertinent uniquement si le contenu du parent est un autre composant.