Découper l’interface en composants (arbre des composants)

L’interface d’une application Angular se compose de plusieurs composants imbriqués les uns dans les autres. Dans cette recette, je pars d’un exemple d’interface et je vous montre comment la découper en composants.

L’arbre des composants

Le point de départ d’une interface Angular, c’est le composant racine (root component) qui représente l’ensemble de l’application. Ce composant en contient d’autres, qui à leur tour en contiennent d’autres, et ainsi de suite. Cette imbrication de composants peut être représentée sous la forme d’une arborescence qu’on appelle l’arbre des composants.

Cet arbre rappelle l’arbre formé par les balises d’une page HTML. Là aussi, on part d’une balise racine (<html>), qui contient des balises enfant (<head> et <body>), qui contiennent à leur tour des balises enfant (par exemple, <title>, <p>)… En HTML, cet arbre s’appelle le DOM, Document Object Model. Dans Angular, cet arbre s’appelle l’arbre des composants.

On pourrait dire que les composants Angular sont un peu comme des balises HTML maison. Et de la même façon que le DOM représente l’imbrication des balises HTML, l’arbre des composants représente l’imbrication des composants Angular.

Exemple de découpage d’une interface en composants (FoodCheri.com)

Examinons ensemble le site FoodCheri.com, qui propose des repas frais livrés à domicile, et voyons comment nous aurions pu découper son interface en composants Angular (pour info, ce site n’a pas été réalisé avec Angular).

On part toujours d’un composant racine qui représente l’ensemble de l’application.

La solution de facilité serait de mettre tout le HTML correspondant à cet écran dans le composant racine. Ça serait possible, mais comme nous allons le voir, ça n’est pas conseillé car nos différents éléments d’interface ne seraient pas réutilisables du tout, et notre interface ne serait pas très dynamique.

Voyons une meilleure façon de faire.

Le composant racine va juste servir de conteneur pour l’ensemble de l’application. Appelons-le le niveau zéro.

Il va contenir un 1er niveau de sous-composants, qui représente la structure générale de la page :

  • Un composant pour l’en-tête.
  • Un composant pour la barre de navigation.
  • Un composant pour le contenu de la page (sachant que le contenu sera différent selon la page sur laquelle on est).
  • Un composant pour le pied de page.

Nous allons ensuite créer un 2ème niveau de sous-composants :

  • La barre de navigation contient trois sous-composants : Calendrier, Adresse, et Panier.
  • La zone de contenu contient un sous-composant “Liste de plats”, qui bouclera sur la liste des plats à afficher.

Enfin, nous aurons un 3ème niveau de sous-composants :

  • Dans la “Liste de plats”, un sous-composant “Plat” sera répété pour chaque plat.
  • Dans le Calendrier, on pourrait avoir un sous-composant pour chaque jour.
  • Etc.

On pourrait continuer à découper ainsi notre interface jusqu’à arriver à de tout petits composants, mais ça n’aurait pas grand intérêt.

Essayons plutôt de voir les règles qui vont nous aider à décider si un bout d’interface doit être placé dans un gros composant monolithique, ou dans plusieurs petits composants imbriqués les uns dans les autres.

Règles à suivre pour découper une interface en composants

REMARQUE IMPORTANTE. C’est VOUS qui décidez de la granularité de vos composants, gros ou petits. Il n’y a pas UNE bonne manière de découper son interface, et deux développeurs Angular peuvent très bien produire deux découpages différents.

Cela dit, quelques grands principes peuvent vous guider :

  • La réutilisabilité. Si un bout d’interface est affiché plusieurs fois, c’est une bonne idée d’en faire un composant. Un bout d’interface peut être réutilisé plusieurs fois sur la même page, ou sur des pages différentes, ou dans des projets différents. Sur le site FoodChéri, le composant Plat est répété plusieurs fois sur la même page. Les composants Calendrier ou Panier pourraient être réutilisés dans d’autres projets.
  • L’encapsulation. Si un bout d’interface contient du HTML ou une logique d’affichage complexes, c’est une bonne idée de l’encapsuler dans son propre composant. Cela simplifiera le code du composant parent tout en séparant clairement les responsabilités de chaque composant. Sur le site FoodChéri, les boutons Plus/Moins d’ajout au panier sont un bon exemple d’une logique un peu élaborée qu’on va vouloir encapsuler. (Il faut personnaliser les boutons en fonction du nombre d’articles, communiquer avec le composant panier en haut à droite de la page, mémoriser quelque part les articles ajoutés…).
  • Le routeur. Le routeur Angular permet d’afficher ou de masquer certains composants en fonction de l’URL en cours. Si vous avez un bout d’interface qui doit être visible sur une page précise mais masqué sur d’autres, c’est une bonne idée de le mettre dans un composant. Sur le site FoodChéri, quand on clique sur le panier, une nouvelle page est affichée avec le contenu du panier. Pour pouvoir être affichée par le routeur, cette page doit être placée dans un composant.
  • Le bon sens. Il n’y a pas de limites sur le nombre de composants ni sur la profondeur de leur imbrication. Cela dit, il est préférable de ne pas les multiplier inutilement pour s’y retrouver plus facilement dans son code. Plus vous aurez de composants, plus vous perdrez du temps à retrouver la partie à modifier.

Imbriquer les composants

Une fois votre découpage effectué et les différents composants créés, il ne vous reste plus qu’à les imbriquer.

La technique la plus répandue est de placer la balise du composant enfant dans le template du composant parent qui va bien.

Par exemple :

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

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

Pour plus d’infos, voir Imbriquer deux composants (différence entre ViewChild et ContentChild).