• 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

Récupérez les données

Il est enfin temps d'hydrater – de remplir de données – le service en récupérant des données depuis le serveur !

Vous allez considérer que les données sur les candidats sont "vieilles" après cinq minutes. Vous n'allez pas simplement faire une requête GET et en émettre le résultat : vous allez aussi implémenter un système qui fait qu'une nouvelle requête ne sera émise que si les données actuelles de l'application ont été récupérées il y a plus de cinq minutes.

Vous découvrirez la structure réactive qui découle du state management réactif, avec certains de ses avantages :

  • la facilité de créer des fonctionnalités avancées ;

  • la simplification de la structure de l'application, où tous vos components dépendent uniquement des émissions des Observables ;

  • vous n'aurez plus de problème de "timing" – où une donnée arrive au "mauvais moment" par rapport à l'initialisation de vos components.

Récupérez la liste de candidats

Il y a déjà un Observable  loading$  dans CandidateListComponent. Ajoutez un Observable  candidates$  , et assignez les deux dans une méthode :

loading$!: Observable<boolean>;
candidates$!: Observable<Candidate[]>;

constructor(private candidatesService: CandidatesService) {}

ngOnInit(): void {
    this.initObservables();
}

private initObservables() {
    this.loading$ = this.candidatesService.loading$;
    this.candidates$ = this.candidatesService.candidates$;
}

Je vous propose le template suivant :

<mat-card>
  <mat-card-title-group>
    <mat-card-title>
      Candidats
    </mat-card-title>
  </mat-card-title-group>
  <mat-spinner *ngIf="loading$ | async"></mat-spinner>
  <mat-nav-list *ngIf="candidates$ | async as candidates">
    <a *ngFor="let candidate of candidates" mat-list-item [routerLink]="candidate.id.toString()">
      <img [src]="candidate.imageUrl" [alt]="candidate.lastName" matListAvatar>
      <h3 matLine>{{ candidate.firstName }} {{ candidate.lastName }}</h3>
      <p matLine>{{ candidate.job }} chez {{ candidate.company }}</p>
    </a>
  </mat-nav-list>
</mat-card>

Sa structure est très simple : la seule nouveauté est l'utilisation de  mat-nav-list  plutôt que  mat-list  pour afficher une liste d'éléments cliquables.

Pour pouvoir émettre des candidats, il faut aller les chercher !

Vous allez créer une méthode de service qui sera appelée depuis le template pour hydrater l'Observable candidates$  .

getCandidatesFromServer() {
    this.setLoadingStatus(true);
    this.http.get<Candidate[]>(`${environment.apiUrl}/candidates`).pipe(
      delay(1000),
      tap(candidates => {
        this._candidates$.next(candidates);
        this.setLoadingStatus(false);
      })
    ).subscribe();
}

Vous émettez que le chargement a commencé ; vous récupérez les données, les émettez, puis émettez que le chargement est terminé.

Maintenant, depuis le  ngOnInit  du template :

ngOnInit(): void {
    this.initObservables();
    this.candidatesService.getCandidatesFromServer();
}

Là où l'ordre est essentiel en utilisant un Subject, l'ordre de ces deux méthodes n'a aucune importance avec les BehaviorSubjects.

Pourquoi ? Parce que même si on souscrit après l'émission, le BehaviorSubject nous réémet sa dernière émission. Plutôt pratique !

On a donc :

Des candidats !
Des candidats !

Avec en plus un spinner lors du chargement des données !

Mais comment faire pour envoyer une requête seulement après cinq minutes ?

Je vous propose d'ajouter une variable au service, qui stockera un timestamp du dernier chargement :

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, delay, Observable, tap } from 'rxjs';
import { Candidate } from '../models/candidate.model';
import { environment } from '../../../environments/environment';

@Injectable()
export class CandidatesService {
  constructor(private http: HttpClient) {}

  private _loading$ = new BehaviorSubject<boolean>(false);
  get loading$(): Observable<boolean> {
    return this._loading$.asObservable();
  }

  private _candidates$ = new BehaviorSubject<Candidate[]>([]);
  get candidates$(): Observable<Candidate[]> {
    return this._candidates$.asObservable();
  }

  private lastCandidatesLoad = 0;

  private setLoadingStatus(loading: boolean) {
    this._loading$.next(loading);
  }

  getCandidatesFromServer() {
    if (Date.now() - this.lastCandidatesLoad <= 300000) {
      return;
    }
    this.setLoadingStatus(true);
    this.http.get<Candidate[]>(`${environment.apiUrl}/candidates`).pipe(
      delay(1000),
      tap(candidates => {
        this.lastCandidatesLoad = Date.now();
        this._candidates$.next(candidates);
        this.setLoadingStatus(false);
      })
    ).subscribe();
  }
}

Ici :

  • si la différence entre  Date.now()  et  lastCandidatesLoaded  est inférieure à 300.000 (5 minutes en millisecondes), vous ne faites rien ;

  • si ça fait plus de 5 minutes, vous lancez la logique de rechargement, et vous stockez la  Date.now()  du moment de la réception des données.

Le fait que les dernières données récupérées soient réémises par le BehaviorSubject nous permet cette facilité d'implémentation, tout en conservant la stratégie OnPush !

Passons à l'affichage d'un seul candidat.

Récupérez un seul candidat

Comme pour CandidateListComponent, vous allez ajouter deux Observables à SingleCandidateComponent :

loading$!: Observable<boolean>;
candidate$!: Observable<Candidate>;

constructor(private candidatesService: CandidatesService) { }

ngOnInit(): void {
    this.initObservables();
}

private initObservables() {
    this.loading$ = this.candidatesService.loading$;
}

Avant d'implémenter la méthode de service qui va permettre de récupérer un candidat par son ID, je vous invite à créer le template et ajouter les styles ci-dessous :

<mat-card>
  <ng-container *ngIf="candidate$ | async as candidate">
    <img [src]="candidate.imageUrl" [alt]="candidate.firstName + ' ' + candidate.lastName">
    <div class="employee-info">
      <h1>{{ candidate.firstName }} {{ candidate.lastName }}</h1>
      <h2>{{ candidate.job }}, {{ candidate.department }} chez {{ candidate.company }}</h2>
      <h3>Contact : {{ candidate.email }}</h3>
    </div>
  </ng-container>
  <mat-card-actions>
    <mat-spinner *ngIf="loading$ | async; else buttons"></mat-spinner>
    <ng-template #buttons>
      <div class="action-buttons">
        <button mat-flat-button color="accent" (click)="onHire()">EMBAUCHER</button>
        <button mat-flat-button color="warn" (click)="onRefuse()">REFUSER</button>
      </div>
      <button mat-flat-button color="primary" (click)="onGoBack()">RETOUR</button>
    </ng-template>
  </mat-card-actions>
</mat-card>
mat-card {
  text-align: center;
}

img {
  border-radius: 50%;
}

button {
  margin: 5px;
}

Créez les trois méthodes vides  onHire  ,  onRefuse  et  onGoBack  pour éviter les erreurs de compilation.

Il vous faut maintenant une méthode de service qui retourne un Observable de Candidate par son ID :

getCandidateById(id: number): Observable<Candidate> {
    return this.candidates$.pipe(
        map(candidates => candidates.filter(candidate => candidate.id === id)[0])
    );
}

Vous retournez un Observable qui dépend directement de l'Observable  candidates$ . Du coup, si cet Observable réémet, l'Observable retourné ici réémettra également.

Dans SingleCandidateComponent, vous pouvez donc injecter ActivatedRoute pour récupérer le paramètre  :id  de la route, pour ensuite demander l'Observable du bon candidat :

this.candidate$ = this.route.params.pipe(
    switchMap(params => this.candidatesService.getCandidateById(+params['id']))
);

Avec ça :

Un candidat
Un candidat

Vous pouvez afficher les candidats un par un, et sans requête au serveur !

Pourquoi ? Eh bien parce que pour l'instant, si vous ne passez pas d'abord par CandidateListComponent, les candidats ne sont jamais récupérés depuis le serveur !

Pour régler ce souci, je vous propose l'ajout suivant :

getCandidateById(id: number): Observable<Candidate> {
    if (!this.lastCandidatesLoad) {
        this.getCandidatesFromServer();
    }
    return this.candidates$.pipe(
        map(candidates => candidates.filter(candidate => candidate.id === id)[0])
    );
}

Comme ça, si on arrive directement sur SingleCandidateComponent (et donc  lastCandidatesLoad = 0  ), on demandera le chargement des utilisateurs.

Profitez-en pour implémenter  onGoBack  pour permettre un retour à la liste des candidats :

constructor(private candidatesService: CandidatesService,
              private route: ActivatedRoute,
              private router: Router) { }

// ...

onGoBack() {
    this.router.navigateByUrl('/reactive-state/candidates');
}

Avec ça, l'implémentation de la consultation des candidats est terminée !

En résumé

  • Le state management réactif, une fois implémenté, facilite largement l'implémentation de nouvelles fonctionnalités, car les components affichent simplement ce que le service leur envoie.

  • Vous pouvez calculer la différence entre  Date.now()  et un timestamp pour connaître le temps depuis une dernière requête.

  • ng-container  permet d'attribuer des directives structurelles sans modifier la structure du document HTML.

  • *ngIf  accepte une référence à un  ng-template  en  else  qui sera affiché si la condition n'est pas remplie.

  • les  ng-template  ne sont pas insérés dans le DOM par défaut, et permettent de faire référence à des blocs de HTML.

Vous voilà avec une liste des candidats ! Dans le prochain chapitre, vous découvrirez comment ajouter la recherche en temps réel.

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