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 :
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()
etlastCandidatesLoaded
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 :
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 à unng-template
enelse
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.