Pour qu'un component Angular soit réutilisable, il doit être découplé de son contexte : il ne doit pas dépendre d'un service spécifique pour les interactions de données, par exemple. On doit pouvoir lui injecter ses données et lui dicter son comportement. C'est ça qui rend un component vraiment réutilisable.
Dans ce chapitre, vous allez créer un component qui permet d'afficher une liste de commentaires laissés par des utilisateurs. Il permettra également de laisser un nouveau commentaire. Ce genre de fonctionnalité pourrait être utile dans plusieurs contextes différents, d'où l'intérêt que le component soit réutilisable.
Ce component acceptera une liste de components en entrée, et devra être capable de signaler à son component parent l'envoi d'un commentaire par l'utilisateur. Dans un contexte non réutilisable, on passerait par un service, mais ici, les méthodes appelées pour les différents contextes ne peuvent pas être les mêmes. Un commentaire laissé sur un Post ne serait pas traité par la même méthode qu'un commentaire laissé sur une vidéo, par exemple.
Vous découvrirez donc une technique qui permet à un component parent de réagir à des événements venant de son enfant : les Outputs. Vous mettrez aussi tout ce que vous avez fait jusqu'ici en forme avec des components Material pour une finition professionnelle.
Pour mettre en forme PostListItemComponent, vous allez utiliser un component Material : MatCard.
Il faut donc exporter le module correspondant dans SharedModule :
@NgModule({
declarations: [],
imports: [
CommonModule
],
exports: [
MatToolbarModule,
MatCardModule
]
})
export class SharedModule { }Comme ça, dans le template de PostListItemComponent :
<mat-card>
<mat-card-header>
<mat-card-title>
<span>{{ post.title | titlecase }}</span>
</mat-card-title>
<mat-card-subtitle>
<span>{{ post.createdDate | date }}</span>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<img mat-card-image *ngIf="post.imageUrl as imageUrl" [src]="imageUrl" [alt]="post.title">
<span class="post-content">{{ post.content }}</span>
</mat-card-content>
</mat-card>Avec les différents éléments mis à disposition par MatCard, vous affichez les différents Posts. La directive mat-card-image attribue des styles à l'image, qui n'est ajoutée au DOM que si le Post contient une imageUrl .
Ajoutez ces styles simples pour mettre un peu d'espace :
mat-card {
margin-bottom: 20px;
}
.post-content {
margin-top: 10px;
}Et vous aurez quelque chose qui ressemble à ça :

Maintenant que tout est beau et propre, commençons la création du component réutilisable : CommentsComponent.
Puisque CommentsComponent sera réutilisé, il sera à sa place dans SharedModule :
ng g c shared/components/comments --export
Vous allez utiliser un bon nombre de components Material dans votre application, donc il serait judicieux de leur créer un module à part pour que tout soit le plus lisible possible. Dans le dossier shared , créez material.module.ts :
import { NgModule } from '@angular/core';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatCardModule } from '@angular/material/card';
import { MatListModule } from '@angular/material/list';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@NgModule({
exports: [
MatToolbarModule,
MatCardModule,
MatListModule,
MatButtonModule,
MatIconModule,
MatFormFieldModule,
MatInputModule
]
})
export class MaterialModule {}Vous pouvez maintenant exporter MaterialModule dans SharedModule au lieu d'exporter les modules Material séparés :
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CommentsComponent } from './components/comments/comments.component';
import { MaterialModule } from './material.module';
@NgModule({
declarations: [
CommentsComponent
],
imports: [
CommonModule,
MaterialModule
],
exports: [
CommentsComponent,
MaterialModule
]
})
export class SharedModule { }Vous avez aussi besoin d'importer MaterialModule. Voyez-vous pourquoi ?
…
…
Eh oui ! Parce que vous avez besoin de ce qui est exporté par MaterialModule à l'intérieur de SharedModule, pour la déclaration de CommentsComponent !
CommentsComponent doit accepter une liste de commentaires injectée par son parent. Il lui faut donc un @Input() :
export class CommentsComponent implements OnInit {
@Input() comments!: Comment[];
}Pour afficher cette liste de commentaires, vous allez utiliser le component MatList :
<mat-list *ngIf="comments.length">
<h2 matSubheader>Commentaires</h2>
<mat-list-item *ngFor="let comment of comments">
<span mat-line>{{ comment.comment }}</span>
<span mat-line>{{ comment.createdDate | date }}</span>
</mat-list-item>
</mat-list>La liste de commentaires ne sera affichée que s'il y en a, et il y aura un item à deux lignes (les mat-line ) pour chaque commentaire.
Ajoutez un style rapide à CommentsComponent :
mat-list-item {
border-bottom: thin solid #555;
}Et vous avez quelque chose comme :

Vos utilisateurs peuvent lire les commentaires, mais comment faire pour qu'ils puissent en laisser un ?
Pour qu'un utilisateur puisse laisser un commentaire, il faut un champ de texte qui le lui permet !
Commencez par utiliser les components Material importés précédemment pour générer rapidement et facilement un beau template de formulaire :
<div class="comments-row">
<mat-form-field>
<input type="text" matInput placeholder="Laissez un commentaire…">
</mat-form-field>
<button mat-icon-button color="primary" (click)="onLeaveComment()">
<mat-icon>send</mat-icon>
</button>
</div>
<mat-list *ngIf="comments.length">
<!-- etc -->Dans ce template :
vous utilisez mat-form-field et la directive matInput pour générer un champ de texte ;
vous implémentez un placeholder plutôt qu'un label pour profiter de l'affichage Material ;
vous générez un bouton à partir d'une icône Material pour l'envoi du formulaire.
Ajoutez les styles suivants :
.comments-row {
display: flex;
border-top: thin solid #555;
padding-top: 10px;
justify-content: space-around;
align-items: center;
}
mat-form-field {
width: 90%;
}Et vous aurez un joli champ de texte !

Puisque ce formulaire est extrêmement simple, inutile de créer tout un FormGroup . Il est possible de créer simplement un FormControl :
commentCtrl!: FormControl;
constructor(private formBuilder: FormBuilder) { }
ngOnInit(): void {
this.commentCtrl = this.formBuilder.control('', [Validators.required, Validators.minLength(10)]);
}Le fonctionnement de FormBuilder.control() est similaire à celui deFormBuilder.group() : vous passez la valeur par défaut pour le formulaire, ainsi qu'un tableau de Validators. Ici, le champ sera requis, et il faudra laisser un commentaire d'au moins 10 caractères.
Lorsque vos contrôles se trouvent dans un FormGroup , vous utilisez formControlName pour les identifier. Ici, ce n'est pas possible, mais c'est encore plus simple. Vous allez lier le FormControl directement à votre input :
<input type="text" matInput placeholder="Laissez un commentaire…" [formControl]="commentCtrl">C'est ici que vous commencerez à voir la puissance des components Material ! Si vous regardez dans l'application, non seulement vous y verrez un * qui montre que le champ est requis, mais si vous y écrivez un texte de moins de 10 caractères et que vous cliquez sur le bouton d'envoi :

Tout ça grâce aux fonctionnalités des MatFormField et MatInput qui réagissent au FormControl que vous leur passez !
Et maintenant, la réponse à la question à 10 millions de $ :
Comment faire pour rendre CommentsComponent réutilisable ? Comment permettre au component parent – dans ce cas, PostListItemComponent – de réagir aux clics du bouton d'envoi ?
Jusqu'ici, vous avez créé des attributs personnalisés avec les @Input . Vous allez maintenant créer des événements personnalisés avec les @Output :
export class CommentsComponent implements OnInit {
@Input() comments!: Comment[];
@Output() newComment = new EventEmitter<string>();
// ...Un EventEmitter est un objet sur lequel on peut appeler la méthode emit() et qui, comme son nom l'indique, émet la valeur qu'on lui passe sous forme d'événement.
Dans le cas de CommentsComponent, l'objectif sera d'émettre le commentaire laissé par l'utilisateur. Vous pouvez donc implémenter onLeaveComment() :
onLeaveComment() {
if (this.commentCtrl.invalid) {
return;
}
this.newComment.emit(this.commentCtrl.value);
this.commentCtrl.reset();
}Dans cette méthode :
si l'utilisateur n'a pas entré un commentaire valable dans le champ de texte, vous ignorez l'événement – avec l'affichage en rouge fourni par Material, il s'agit d'une alternative à la désactivation du bouton ;
vous appelez emit sur votre nouvel EventEmitter en y passant le contenu du champ de texte ;
vous appelez reset sur le FormControl pour vider le champ de texte.
L' EventEmitter fait maintenant son job, et le décorateur @Output permet d'y lier une méthode depuis le parent à l'aide de l'event binding.
post-list-item.component.html :
<mat-card-actions>
<app-comments [comments]="post.comments" (newComment)="onNewComment($event)"></app-comments>
</mat-card-actions>Pour en vérifier le fonctionnement, créez une implémentation temporaire de onNewComment qui accepte une string comme argument, et qui log cette string à la console. Laissez un commentaire dans CommentsComponent, et vous verrez :

Cela vous permet de bien constater que l'événement de l'enfant (CommentsComponent) est capté par le parent (PostListItemComponent), sans avoir eu besoin de passer par un service !
CommentsComponent est donc réutilisable : son component parent lui injecte les données et réagit à ses événements.
Il reste une dernière étape pour bien respecter l'architecture container/presenter.
L'architecture de votre application fait que PostListItemComponent est censé être un presenter. Il ne doit pas contenir de logique de communication avec un service, par exemple.
Mais dans ce cas-là, comment on fait pour dire au serveur qu'un commentaire a été laissé ?
Vous allez retransmettre l'événement du commentaire pour que le parent container (PostListComponent) puisse l'envoyer au service, et donc au serveur. On appelle ce genre de retransmission du bubble up : les événements sont comme des bulles qui montent de plus en plus haut.
Et comment retransmettre cet événement ? Eh bien avec encore un @Output !
Envoyer le commentaire seul ne suffit pas : PostListComponent a besoin de savoir quel Post a été commenté. Le @Output dans PostListItemComponent va donc émettre un objet contenant le commentaire et l' id du Post commenté :
export class PostListItemComponent implements OnInit {
@Input() post!: Post;
@Output() postCommented = new EventEmitter<{ comment: string, postId: number }>();
// ...
onNewComment(comment: string) {
this.postCommented.emit({ comment, postId: this.post.id });
}
}Vous pouvez maintenant capturer la "bulle" – l'événement retransmis – dans PostListComponent, le parent container :
post-list.component.html :
<h2>Posts</h2>
<app-post-list-item *ngFor="let post of posts$ | async" [post]="post" (postCommented)="onPostCommented($event)"></app-post-list-item>Dans post-list.component.ts , injectez PostsService et implémentez onPostCommented :
onPostCommented(postCommented: { comment: string, postId: number }) {
this.postsService.addNewComment(postCommented);
}Enfin, implémentez la méthode addNewComment dans PostsService :
addNewComment(postCommented: { comment: string, postId: number }) {
console.log(postCommented);
}Laisser un commentaire sur un Post dans l'application vous donne maintenant :

Votre application comporte désormais :
un component parent container qui se charge de la logique de communication avec le service, qui souscrit aux Observables générés par ce dernier, et distribue les résultats à…
… des components enfants presenter qui s'occupent simplement d'afficher les données qu'ils reçoivent et de faire remonter les événements qui s'y produisent ;
un component réutilisable qui, en plus d'être un component presenter, n'est pas lié à un contexte spécifique : un commentaire pourrait être laissé sur plein de contenus différents.
Un component réutilisable ne doit dépendre que de son parent, aussi bien pour ses données que pour son comportement.
Un EventEmitter est un objet qui émet des événements personnalisés.
Un EventEmitter doté du décorateur @Output devient "écoutable" depuis le component parent avec l'event binding.
On parle de bubble up lorsqu'un événement émis par un component est réémis par son parent (et peut-être aussi son grand-parent, son arrière-grand-parent…).
On emploiera le pattern bubble up pour respecter l'architecture container/presenter.
Ces trois derniers chapitres ont été particulièrement longs et denses, donc félicitations d'avoir tenu jusqu'ici ! Vous y avez exploré un grand nombre de concepts architecturaux.
Les deux derniers chapitres de cette partie sont moins longs et moins complexes, mais tout aussi importants. Vous avez déjà utilisé les Pipes et Directives fournis par Angular : vous allez découvrir comment créer les vôtres pour enrichir l'architecture de vos applications.
Allez, c'est parti !