Dans le dernier chapitre, vous avez implémenté des requêtes GET pour récupérer la liste des FaceSnaps et les FaceSnaps spécifiques. Cependant, il reste deux fonctionnalités à implémenter : le "snap" et la création de nouveaux FaceSnaps.
Pour ces deux fonctionnalités, vous allez utiliser des requêtes composées.
Pour le "snap"
Vous allez utiliser l'
id
que vous recevez du component pour GET le FaceSnap en entier.Après avoir modifié le nombre de snaps du FaceSnap, vous enverrez le FaceSnap modifié via une requête PUT.
Pour la création du nouveau FaceSnap
Vous allez GET la liste entière de FaceSnaps pour vous assurer de créer un
id
unique valable pour le FaceSnap créé.Vous enverrez ensuite le nouveau FaceSnap via une requête POST.
Commençons par le "snap".
Laissez "snap" vos utilisateurs
Si vous regardez l'implémentation actuelle de la méthode onSnap()
, vous vous rendrez vite compte qu'il va falloir changer d'approche. On n'a plus de FaceSnap statique dans le component, donc on ne peut pas accéder à son id
, et souscrire de nouveau à faceSnap$
relancerait la requête HTTP. Il nous faut donc trouver une autre solution.
La solution que je vous propose (qui n'est pas la seule possible !) est de passer l' id
du FaceSnap comme argument à onSnap()
. Grâce au pipe async
, le template contient une référence à la dernière émission, donc cette modification est simple.
Dans le TypeScript :
// ...
onSnap(faceSnapId: number) {
if (this.buttonText === 'Oh Snap!') {
this.faceSnapsService.snapFaceSnapById(faceSnapId, 'snap');
this.buttonText = 'Oops, unSnap!';
} else {
this.faceSnapsService.snapFaceSnapById(faceSnapId, 'unsnap');
this.buttonText = 'Oh Snap!';
}
}
// ...
Et dans le template :
<!-- ... -->
(click)="onSnap(faceSnap.id)"{{ buttonText }}
<!-- ... -->
La méthode onSnap()
sait maintenant de quel FaceSnap on parle.
Il faut modifier l'implémentation de snapFaceSnapById()
pour implémenter la requête composée. Ce sera également votre premier vrai Observable haut niveau.
Dans ce cas, vous allez créer un Observable pour la requête GET qui, avec la réponse du serveur, va envoyer une requête PUT avec le FaceSnap modifié.
Voilà l'implémentation que je vous propose :
snapFaceSnapById(faceSnapId: number, snapType: 'snap' | 'unsnap'): Observable<FaceSnap> {
return this.getFaceSnapById(faceSnapId).pipe(
map(faceSnap => ({
...faceSnap,
snaps: faceSnap.snaps + (snapType === 'snap' ? 1 : -1)
})),
switchMap(updatedFaceSnap => this.http.put<FaceSnap>(
`http://localhost:3000/facesnaps/${faceSnapId}`,
updatedFaceSnap)
)
);
}
Regardons cette méthode ligne par ligne :
D'abord, constatez que le type de retour de cette méthode est Observable<FaceSnap>
parce que la requête PUT finale renvoie le FaceSnap modifié comme confirmation.
Puisqu'il existe déjà une méthode qui permet de récupérer un FaceSnap par son ID, vous n'allez pas vous priver de l'utiliser ! N'oubliez pas que cette méthode retourne l'Observable de la requête, donc vous pouvez très bien lui ajouter un pipe()
pour y ajouter des opérateurs afin de créer l'Observable final dont vous avez besoin.
Le premier opérateur map()
vous permet de prendre le FaceSnap retourné par le serveur, et de le transformer en un FaceSnap avec un snap de plus ou de moins, selon que le snapType
est 'snap'
ou 'unsnap'
.
Le second opérateur switchMap()
prend le FaceSnap modifié, et en génère une requête PUT avec la méthode put
de HttpClient. put
prend l'URL comme premier argument et le corps de la requête à envoyer comme deuxième argument, et retourne l'Observable qui correspond à cette requête.
Vous finissez donc avec un Observable qui enverra deux requêtes, avec la deuxième qui dépend du retour de la première.
La méthode de service est prête : passons au component.
Vous pourriez être tenté de faire quelque chose comme ça :
onSnap(faceSnapId: number) {
if (this.buttonText === 'Oh Snap!') {
this.faceSnapsService.snapFaceSnapById(faceSnapId, 'snap').subscribe();
this.buttonText = 'Oops, unSnap!';
} else {
this.faceSnapsService.snapFaceSnapById(faceSnapId, 'unsnap').subscribe();
this.buttonText = 'Oh Snap!';
}
}
Cette approche mettra bien à jour le serveur, mais le FaceSnap affiché ne sera pas mis à jour, car il ne sait pas que le FaceSnap a été modifié. Même si, en fin de méthode, vous ajoutez :
this.faceSnap$ = this.faceSnapsService.getFaceSnapById(faceSnapId);
… vous verrez que ça ne fonctionne pas, et que ça donne même l'impression de fonctionner à l'envers !
Mais pourquoi ??!!
Eh bien, c'est parce que ces requêtes restent asynchrones, même si elles sont quasiment instantanées avec le serveur qui tourne en local. L'ordre de traitement des requêtes n'est donc pas assuré dans l'exemple ci-dessus.
Pour vous assurer que le PUT soit traité avant de rappeler le GET, il faut de nouveau ajouter un pipe()
:
onSnap(faceSnapId: number) {
if (this.buttonText === 'Oh Snap!') {
this.faceSnapsService.snapFaceSnapById(faceSnapId, 'snap').pipe(
tap(() => {
this.faceSnap$ = this.faceSnapsService.getFaceSnapById(faceSnapId);
this.buttonText = 'Oops, unSnap!';
})
).subscribe();
} else {
this.faceSnapsService.snapFaceSnapById(faceSnapId, 'unsnap').pipe(
tap(() => {
this.faceSnap$ = this.faceSnapsService.getFaceSnapById(faceSnapId);
this.buttonText = 'Oh Snap!';
})
).subscribe();
}
}
Ici, vous profitez du fait que l'Observable du PUT émette au moment de la réponse positive du serveur, pour ajouter un tap()
qui vient renouveler la requête GET du FaceSnap simple et mettre à jour le texte du bouton !
On va même améliorer cette implémentation : puisque la requête PUT renvoie le FaceSnap modifié, on peut simplifier et faire :
onSnap(faceSnapId: number) {
if (this.buttonText === 'Oh Snap!') {
this.faceSnap$ = this.faceSnapsService.snapFaceSnapById(faceSnapId, 'snap').pipe(
tap(() => this.buttonText = 'Oops, unSnap!')
);
} else {
this.faceSnap$ = this.faceSnapsService.snapFaceSnapById(faceSnapId, 'unsnap').pipe(
tap(() => this.buttonText = 'Oh Snap!')
);
}
}
Avec cette implémentation, il n'y a même pas besoin d'appeler .subscribe()
car le pipe async
du template souscrit pour nous !
Et voilà ! Vos utilisateurs peuvent de nouveau "snap" les FaceSnaps !
Du coup, il ne reste plus que la création de nouveaux FaceSnaps !
Ajoutez de nouveaux FaceSnaps
À vous de jouer !
Je vous invite à profiter de cette fonctionnalité pour essayer de l'implémenter vous-même avant de lire mon approche. L'objectif ici sera de :
récupérer la liste entière de FaceSnaps à jour du serveur ;
créer un FaceSnap à partir des données fournies dans le formulaire, qui aura comme
id
le prochainid
valable ;
Par exemple si le FaceSnap précédent a un
id
de 16, le nouveau doit avoir unid
de 17.
envoyer ce nouveau FaceSnap au serveur via une requête POST.
Voici les quelques informations dont vous aurez besoin :
l'URL pour cette requête sera http://localhost:3000/facesnaps ;
une requête POST à cet URL retourne le FaceSnap créé ;
la méthode
post()
de HttpClient fonctionne comme sa méthodeput()
en termes d'arguments ;la redirection de l'utilisateur ne doit avoir lieu qu'une fois le nouveau FaceSnap enregistré.
Voilà, vous avez tout ce qu'il faut pour ajouter de nouveaux FaceSnaps par vous-même !
Suivez mon approche
D'abord, la méthode du service :
addFaceSnap(formValue: { title: string, description: string, imageUrl: string, location?: string }): Observable<FaceSnap> {
return this.getAllFaceSnaps().pipe(
map(facesnaps => [...facesnaps].sort((a,b) => a.id - b.id)),
map(sortedFacesnaps => sortedFacesnaps[sortedFacesnaps.length - 1]),
map(previousFacesnap => ({
...formValue,
snaps: 0,
createdDate: new Date(),
id: previousFacesnap.id + 1
})),
switchMap(newFacesnap => this.http.post<FaceSnap>(
'http://localhost:3000/facesnaps',
newFacesnap)
)
);
}
Trois fois l'opérateur map()
??? Vraiment ???
Il est tout à fait possible d'effectuer ces trois étapes dans un seul opérateur. Je voulais simplement vous montrer la lisibilité qui peut être gagnée en séparant les étapes :
On retourne un tableau trié par ID pour s'assurer que le dernier élément du tableau possède l'ID le plus élevé.
On retourne ensuite le dernier élément de ce tableau.
On retourne le nouveau FaceSnap avec son ID valable.
Le dernier opérateur, switchMap()
, génère la requête POST finale.
Il ne reste plus qu'à modifier onSubmitForm()
:
onSubmitForm() {
this.faceSnapsService.addFaceSnap(this.snapForm.value).pipe(
tap(() => this.router.navigateByUrl('/facesnaps'))
).subscribe();
}
Comme avec le "snap" précédemment implémenté, on profite du retour de l'Observable pour déclencher la "prochaine étape" – ici, la redirection de l'utilisateur.
Avec cette dernière étape, votre application est entièrement dynamique ! Elle dépend entièrement du serveur auquel elle se connecte, et elle interagit avec correctement.
En résumé
Les méthodes
put()
etpost()
de HttpClient prennent l'URL de la requête comme premier argument, et le corps à envoyer comme deuxième argument ;Vous créez une requête composée lorsque la réponse d'une requête est utilisée pour en créer une autre ;
Attention à l'asynchrone ! Si une action doit être effectuée après une requête, utilisez des opérateurs comme
tap()
dans lepipe
de la requête ;Quand une méthode de service génère une requête, le best practice est de retourner l'Observable et d'y souscrire depuis le component.
Maintenant que vous savez envoyer des requêtes HTTP composées, dans le prochain chapitre, vous verrez comment utiliser les intercepteurs pour agir sur toutes les requêtes de votre application !