Composants angulaires 101 — un aperçu

Publié: 2022-03-11

Les composants sont disponibles dans Angular depuis le début ; cependant, de nombreuses personnes se retrouvent à utiliser des composants de manière incorrecte. Dans mon travail, j'ai vu des gens ne pas les utiliser du tout, créer des composants au lieu de directives d'attributs, et plus encore. Ce sont des problèmes que les développeurs Angular juniors et seniors ont tendance à poser, moi y compris. Et donc, ce post est pour les gens comme moi quand j'apprenais Angular, puisque ni la documentation officielle ni la documentation non officielle sur les composants n'expliquent avec des cas d'utilisation comment et quand utiliser les composants.

Dans cet article, j'identifierai les façons correctes et incorrectes d'utiliser les composants angulaires, avec des exemples. Cet article devrait vous donner une idée claire sur :

  • La définition des composants angulaires ;
  • Quand vous devez créer des composants angulaires séparés ; et
  • Quand vous ne devez pas créer de composants angulaires séparés.

Image de couverture pour les composants angulaires

Avant de pouvoir passer à l'utilisation correcte des composants angulaires, je souhaite aborder brièvement le sujet des composants en général. De manière générale, chaque application Angular a au moins un composant par défaut, le root component . À partir de là, c'est à nous de décider comment concevoir notre application. Habituellement, vous créez un composant pour des pages distinctes, puis chaque page contient une liste de composants distincts. En règle générale, un composant doit répondre aux critères suivants :

  • Doit avoir une classe définie, qui contient les données et la logique ; et
  • Doit être associé à un modèle HTML qui affiche des informations pour l'utilisateur final.

Imaginons un scénario où nous avons une application qui a deux pages : Tâches à venir et Tâches terminées . Dans la page Tâches à venir , nous pouvons afficher les tâches à venir, les marquer comme « terminées » et enfin ajouter de nouvelles tâches. De même, dans la page Tâches terminées , nous pouvons afficher les tâches terminées et les marquer comme "annulées". Enfin, nous avons des liens de navigation, qui nous permettent de naviguer entre les pages. Ceci étant dit, nous pouvons diviser la page suivante en trois sections : composant racine, pages, composants réutilisables.

Diagramme des tâches à venir et des tâches terminées

Exemple de wireframe d'application avec des sections de composants séparées colorées

Sur la base de la capture d'écran ci-dessus, nous pouvons clairement voir que la structure de l'application ressemblerait à ceci :

 ── myTasksApplication ├── components │ ├── header-menu.component.ts │ ├── header-menu.component.html │ ├── task-list.component.ts │ └── task-list.component.html ├── pages │ ├── upcoming-tasks.component.ts │ ├── upcoming-tasks.component.html │ ├── completed-tasks.component.ts │ └── completed-tasks.component.html ├── app.component.ts └── app.component.html

Relions donc les fichiers de composants aux éléments réels sur le filaire ci-dessus :

  • header-menu.component et task-list.component sont des composants réutilisables, qui sont affichés avec une bordure verte dans la capture d'écran filaire ;
  • upcoming-tasks.component et filled completed-tasks.component sont des pages qui s'affichent avec une bordure jaune dans la capture d'écran filaire ci-dessus ; et
  • Enfin, app.component est le composant racine, qui s'affiche avec une bordure rouge dans la capture d'écran filaire.

Ainsi, cela étant dit, nous pouvons spécifier une logique et une conception distinctes pour chaque composant. Sur la base des structures filaires ci-dessus, nous avons deux pages qui réutilisent un composant task-list.component . La question se poserait : comment spécifier le type de données à afficher sur une page spécifique ? Heureusement, nous n'avons pas à nous en soucier, car lorsque vous créez un composant, vous pouvez également spécifier des variables d'entrée et de sortie .

Variables d'entrée

Les variables d'entrée sont utilisées dans les composants pour transmettre certaines données du composant parent. Dans l'exemple ci-dessus, nous pourrions avoir deux paramètres d'entrée pour le task-list.componenttasks et listType . En conséquence, tasks seraient une liste de chaînes qui afficheraient chaque chaîne sur une ligne distincte, tandis que listType serait soit coming soit done done , ce qui indiquerait si la case à cocher est cochée. Ci-dessous, vous pouvez trouver un petit extrait de code montrant à quoi pourrait ressembler le composant réel.

 // task-list.component.ts import { Component, Input } from '@angular/core'; @Component({ selector: 'app-task-list', templateUrl: 'task-list.component.html' }) export class TaskListComponent { @Input() tasks: string[] = []; // List of tasks which should be displayed. @Input() listType: 'upcoming' | 'completed' = 'upcoming'; // Type of the task list. constructor() { } }

Variables de sortie

Semblables aux variables d'entrée, les variables de sortie peuvent également être utilisées pour transmettre des informations entre les composants, mais cette fois au composant parent. Par exemple, pour le task-list.component , nous pourrions avoir la variable de sortie itemChecked . Il informerait le composant parent si un élément a été coché ou décoché. Les variables de sortie doivent être des émetteurs d'événements. Ci-dessous, vous pouvez trouver un petit extrait de code montrant à quoi pourrait ressembler le composant avec une variable de sortie.

 // task-list.component.ts import { Component, Input, Output } from '@angular/core'; @Component({ selector: 'app-task-list', templateUrl: 'task-list.component.html' }) export class TaskListComponent { @Input() tasks: string[] = []; // List of tasks which should be displayed. @Input() listType: 'upcoming' | 'completed' = 'upcoming'; // Type of the task list. @Output() itemChecked: EventEmitter<boolean> = new EventEmitter(); @Output() tasksChange: EventEmitter<string[]> = new EventEmitter(); constructor() { } /** * Is called when an item from the list is checked. * @param selected---Value which indicates if the item is selected or deselected. */ onItemCheck(selected: boolean) { this.itemChecked.emit(selected); } /** * Is called when task list is changed. * @param changedTasks---Changed task list value, which should be sent to the parent component. */ onTasksChanged(changedTasks: string[]) { this.taskChange.emit(changedTasks); } }
Contenu possible task-list.component.ts après l'ajout de variables de sortie

Utilisation du composant enfant et liaison de variable

Voyons comment utiliser ce composant dans le composant parent et comment faire différents types de liaisons de variables. Dans Angular, il existe deux manières de lier les variables d'entrée : la liaison unidirectionnelle, ce qui signifie que la propriété doit être entourée de crochets [] , et la liaison bidirectionnelle, ce qui signifie que la propriété doit être entourée de crochets. [()] . Jetez un œil à l'exemple ci-dessous et voyez les différentes manières dont les données peuvent être transmises entre les composants.

 <h1>Upcoming Tasks</h1> <app-task-list [(tasks)]="upcomingTasks" [listType]="'upcoming'" (itemChecked)="onItemChecked($event)"></app-task-list>
Contenu éventuel upcoming-tasks.component.html

Passons en revue chaque paramètre :

  • Le paramètre de tasks est transmis à l'aide d'une liaison bidirectionnelle. Cela signifie que, si le paramètre des tâches est modifié dans le composant enfant, le composant parent reflétera ces modifications sur la variable upcomingTasks . Pour autoriser la liaison de données bidirectionnelle, vous devez créer un paramètre de sortie, qui suit le modèle "[inputParameterName]Change", dans ce cas tasksChange .
  • Le paramètre listType est transmis à l'aide d'une liaison unidirectionnelle. Cela signifie qu'il peut être modifié dans le composant enfant, mais qu'il ne sera pas reflété dans le composant parent. Gardez à l'esprit que j'aurais pu attribuer la valeur 'upcoming' à un paramètre du composant et la transmettre à la place - cela ne ferait aucune différence.
  • Enfin, le paramètre itemChecked est une fonction d'écoute, et il sera appelé chaque fois que onItemCheck est exécuté sur le task-list.component . Si un élément est coché, $event contiendrait la valeur true , mais s'il n'est pas coché, il contiendrait la valeur false .

Comme vous pouvez le voir, en général, Angular fournit un excellent moyen de transmettre et de partager des informations entre plusieurs composants, vous ne devez donc pas avoir peur de les utiliser. Assurez-vous simplement de les utiliser à bon escient et de ne pas en abuser.

Quand créer un composant angulaire séparé

Comme mentionné précédemment, vous ne devriez pas avoir peur d'utiliser des composants angulaires, mais vous devez certainement les utiliser à bon escient.

Schéma des composants angulaires en action

Utiliser judicieusement les composants angulaires

Alors, quand devez-vous créer des composants angulaires ?

  • Vous devez toujours créer un composant séparé si le composant peut être réutilisé à plusieurs endroits, comme avec notre task-list.component . Nous les appelons composants réutilisables .
  • Vous devez envisager la création d'un composant distinct si le composant rendra le composant parent plus lisible et lui permettra d'ajouter une couverture de test supplémentaire. Nous pouvons les appeler composants d'organisation de code .
  • Vous devez toujours créer un composant séparé si vous avez une partie d'une page qui n'a pas besoin d'être mise à jour souvent et que vous souhaitez augmenter les performances. Ceci est lié à la stratégie de détection des changements. Nous pouvons les appeler composants d'optimisation .

Ces trois règles m'aident à identifier si je dois créer un nouveau composant, et elles me donnent automatiquement une vision claire du rôle du composant. Idéalement, lorsque vous créez un composant, vous devez déjà savoir quel sera son rôle au sein de l'application.

Puisque nous avons déjà examiné l'utilisation des composants réutilisables , examinons l'utilisation des composants d'organisation du code . Imaginons que nous ayons un formulaire d'inscription et, en bas du formulaire, nous avons une boîte avec Termes et Conditions . Habituellement, le jargon juridique a tendance à être très volumineux, occupant beaucoup d'espace, dans ce cas, dans un modèle HTML. Alors, regardons cet exemple et voyons ensuite comment nous pourrions potentiellement le changer.

Au début, nous avons un composant - registration.component - qui contient tout, y compris le formulaire d'inscription ainsi que les termes et conditions eux-mêmes.

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <div class="terms-and-conditions-box"> Text with very long terms and conditions. </div> <button (click)="onRegistrate()">Registrate</button>
État initial avant de séparer </code>registration.component</code> en plusieurs composants

Le modèle semble maintenant petit, mais imaginez si nous remplacions "Texte avec des termes et conditions très longs" par un texte réel de plus de 1000 mots, cela rendrait l'édition du fichier difficile et inconfortable. Nous avons une solution rapide pour cela : nous pourrions inventer un nouveau composant terms-and-conditions.component , qui contiendrait tout ce qui concerne les termes et conditions. Jetons donc un coup d'œil au fichier HTML pour les terms-and-conditions.component .

 <div class="terms-and-conditions-box"> Text with very long terms and conditions. </div>
Modèle HTML </code>terms-and-conditions.component</code> nouvellement créé

Et maintenant, nous pouvons ajuster le registration.component et utiliser le terms-and-conditions.component en son sein.

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <app-terms-and-conditions></app-terms-and-conditions> <button (click)="onRegistrate()">Registrate</button>
Mise à jour du modèle </code>registration.component.ts</code> avec le composant d'organisation du code

Toutes nos félicitations! Nous venons de réduire la taille du registration.component de centaines de lignes et de faciliter la lecture du code. Dans l'exemple ci-dessus, nous avons apporté des modifications au modèle du composant, mais le même principe pourrait être appliqué à la logique du composant.

Enfin, pour les composants d'optimisation , je vous suggère fortement de passer par ce post, puisqu'il vous fournira toutes les informations nécessaires pour comprendre la détection de changement, et à quels cas précis vous pouvez l'appliquer. Vous ne l'utiliserez pas souvent, mais il peut y avoir des cas, et si vous pouvez ignorer les vérifications régulières de plusieurs composants lorsque ce n'est pas nécessaire, c'est un gagnant-gagnant pour les performances.

Cela étant dit, nous ne devrions pas toujours créer des composants séparés, alors regardons quand vous devriez éviter de créer un composant séparé.

Quand éviter la création d'un composant angulaire séparé

Il y a trois points principaux sur lesquels je suggère de créer un composant angulaire séparé, mais il y a des cas dans lesquels j'éviterais de créer un composant séparé également.

Illustration de trop de composants

Trop de composants vous ralentiront.

Encore une fois, regardons les puces qui vous permettront de comprendre facilement quand vous ne devez pas créer un composant séparé.

  • Vous ne devez pas créer de composants pour les manipulations DOM. Pour ceux-ci, vous devez utiliser des directives d'attribut.
  • Vous ne devez pas créer de composants si cela rend votre code plus chaotique. C'est le contraire des composants d'organisation du code .

Maintenant, regardons de plus près et examinons les deux cas dans des exemples. Imaginons que nous voulions avoir un bouton qui enregistre le message lorsqu'il est cliqué. Cela pourrait être erroné et un composant séparé pourrait être créé pour cette fonctionnalité spécifique, qui contiendrait un bouton et des actions spécifiques. Vérifions d'abord l'approche incorrecte :

 // log-button.component.ts import { Component, Input, Output } from '@angular/core'; @Component({ selector: 'app-log-button', templateUrl: 'log-button.component.html' }) export class LogButtonComponent { @Input() name: string; // Name of the button. @Output() buttonClicked: EventEmitter<boolean> = new EventEmitter(); constructor() { } /** * Is called when button is clicked. * @param clicked - Value which indicates if the button was clicked. */ onButtonClick(clicked: boolean) { console.log('I just clicked a button on this website'); this.buttonClicked.emit(clicked); } }
Logique du composant incorrect log-button.component.ts

Et en conséquence, ce composant est accompagné de la vue html suivante.

 <button (click)="onButtonClick(true)">{{ name }}</button>
Modèle de composant incorrect log-button.component.html

Comme vous pouvez le voir, l'exemple ci-dessus fonctionnerait et vous pourriez utiliser le composant ci-dessus dans vos vues. Il ferait ce que vous voulez, techniquement. Mais la bonne solution ici serait d'utiliser des directives. Cela vous permettrait non seulement de réduire la quantité de code que vous devez écrire, mais aussi d'ajouter la possibilité d'appliquer cette fonctionnalité à n'importe quel élément de votre choix, pas seulement aux boutons.

 import { Directive } from '@angular/core'; @Directive({ selector: "[logButton]", hostListeners: { 'click': 'onButtonClick()', }, }) class LogButton { constructor() {} /** * Fired when element is clicked. */ onButtonClick() { console.log('I just clicked a button on this website'); } }
directive logButton , qui peut être assignée à n'importe quel élément

Maintenant que nous avons créé notre directive, nous pouvons simplement l'utiliser dans notre application et l'affecter à n'importe quel élément que nous voulons. Par exemple, réutilisons notre registration.component .

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <app-terms-and-conditions></app-terms-and-conditions> <button (click)="onRegistrate()" logButton>Registrate</button>
Directive logButton utilisée sur le bouton des formulaires d'inscription

Maintenant, nous pouvons nous intéresser au deuxième cas, dans lequel nous ne devons pas créer de composants séparés, et c'est l'inverse des composants d'optimisation de code . Si le composant nouvellement créé rend votre code plus compliqué et plus volumineux, il n'est pas nécessaire de le créer. Prenons comme exemple notre registration.component . Un tel cas serait de créer un composant séparé pour l'étiquette et le champ d'entrée avec une tonne de paramètres d'entrée. Découvrons cette mauvaise pratique.

 // form-input-with-label.component.ts import { Component, Input} from '@angular/core'; @Component({ selector: 'app-form-input-with-label', templateUrl: 'form-input-with-label.component.html' }) export class FormInputWithLabelComponent { @Input() name: string; // Name of the field @Input() id: string; // Id of the field @Input() label: string; // Label of the field @Input() type: 'text' | 'password'; // Type of the field @Input() model: any; // Model of the field constructor() { } }
Logique du form-input-with-label.component

Et cela pourrait être la vue de ce composant.

 <label for="{{ id }}">{{ label }}</label><br /> <input type="{{ type }}" name="{{ name }}" [(ngModel)]="model" /><br />
Vue du form-input-with-label.component

Bien sûr, la quantité de code serait réduite dans registration.component , mais cela rend-il la logique globale du code plus facile à comprendre et à lire ? Je pense que nous pouvons clairement voir que cela rend le code inutilement plus complexe qu'il ne l'était auparavant.

Prochaines étapes : Composants angulaires 102 ?

Pour résumer : n'ayez pas peur d'utiliser des composants ; Assurez-vous simplement d'avoir une vision claire de ce que vous voulez réaliser. Les scénarios que j'ai énumérés ci-dessus sont les plus courants, et je les considère comme les plus importants et les plus courants ; cependant, votre scénario peut être unique et c'est à vous de prendre une décision éclairée. J'espère que vous en avez suffisamment appris pour prendre une bonne décision.

Si vous souhaitez en savoir plus sur les stratégies de détection de changement d'Angular et la stratégie OnPush, je vous recommande de lire Angular Change Detection et la stratégie OnPush. Il est étroitement lié aux composants et, comme je l'ai déjà mentionné dans l'article, il peut améliorer considérablement les performances de l'application.

Étant donné que les composants ne sont qu'une partie des directives fournies par Angular, il serait bon de connaître également les directives d' attribut et les directives structurelles . Comprendre toutes les directives permettra probablement au programmeur d'écrire plus facilement un meilleur code.