• 12 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 20/06/2022

Créez un component réutilisable

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.

Matérialisez les components

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 :

Des PostCard…
Des PostCards…

Maintenant que tout est beau et propre, commençons la création du component réutilisable : CommentsComponent.

Soyez écolo : réutilisez vos components

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 :

Un Post et ses Comment !
Un Post et ses Comments !

Vos utilisateurs peuvent lire les commentaires, mais comment faire pour qu'ils puissent en laisser un ?

Acceptez les nouveaux commentaires

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 !

Un champ de texte et un bouton
Un beau formulaire

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 :

Le champ de texte devient rouge pour avertir l'utilisateur
Entrée non valable !

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 ?

Communiquez avec les parents

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 :

Le parent qui répond
Le parent qui répond

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.

Faites des bulles

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 :

Le service qui répond !
Le service qui répond !

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.

En résumé

  • 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.

Bravo !

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 ! 

Exemple de certificat de réussite
Exemple de certificat de réussite