Skip to content

Instantly share code, notes, and snippets.

@bhubr
Last active February 8, 2022 13:36
Show Gist options
  • Save bhubr/98f4043f3b1464c36166dd37f41a078d to your computer and use it in GitHub Desktop.
Save bhubr/98f4043f3b1464c36166dd37f41a078d to your computer and use it in GitHub Desktop.
Angular M1 CP IL - 7 janvier 2022
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript Bind</title>
</head>
<body>
<button id="btn" type="button">click me</button>
<script>
const person = {
name: 'Joe',
sayName() {
console.log(this)
console.log(`Hello, I am ${this.name}`)
}
}
// this.handleError = this.handleError.bind(this);
person.sayName = person.sayName.bind(person);
// appeler méthode directement sur l'objet -> ça marche
person.sayName();
// on pourrait se dire :
// "en cliquant sur le bouton, ça va appeler person.sayName()"
// et afficher le même message...
// mais non !
document.querySelector('#btn').addEventListener('click', person.sayName);
</script>
</body>
</html>

Angular M1 CP IL - 7 janvier 2022 - Plan

:info: Doc de l'API : https://album-api.benoithubert.me

État des lieux

La dernière fois, nous avions :

  • Créé des formulaires de register et de login

  • Abordé la notion d'observable et vu quelques exemples avec la bibliothèque RxJS, utilisée par Angular.

  • Créé un AuthenticationService permettant :

    • de stocker l'envoi des données des deux formulaires vers l'API,
    • de gérer l'état de l'authentification (connecté ou non) via un observable : plus précisément, l'attribut currentUserSubject, un type d'observable particulier, le BehaviorSubject, qui contrairement aux observables ordinaires, permet de stocker la dernière valeur.
    • de "persister" cette authentification dans deux clés du "local storage" : currentUser pour les données de l'utilisateur proprement dites, token pour le JWT renvoyé par le serveur sur une requête de login réussie
  • Modifié la méthode createPost du PostService, de façon à envoyer le JWT dans le header Authorization, sous la forme : Bearer JWT.

Il nous reste beaucoup de choses à voir !

"Petit" oubli à corriger

Quand on appelle la méthode logout de AuthenticationService, on devrait réinitialiser l'attribut token à une chaîne vide.

Guards

Une des problématiques évoquées la dernière fois, mais qu'on n'a pas eu le temps de traiter : la "protection" de certaines pages contre des accès non-autorisés.

La plupart des pages de notre application ont vocation à être librement accessibles, sans authentification :

  • la page d'accueil,
  • la page de détails d'un post (accessible en cliquant "En savoir plus"),
  • les pages About et Contact (bien qu'assez inutiles pour le moment, il faut le reconnaître !)

Par contre, certaines pages ne devraient pas être accessibles ainsi :

  • le formulaire d'ajout d'un post,
  • éventuellement, si on ajoute ce genre de fonctionnalité, un formulaire permettant à l'utilisateur connecté de modifier ses informations (email, avatar, etc.)

Résumé de la procédure à suivre

Dans un premier temps :

  • Créer un guard : ng generate guard auth
  • Dans ce guard, renvoyer true/false suivant qu'un utilisateur est connecté ou non
  • Pour cela, s'inspirer de la doc sur CanActivate : pour simplifier, la méthode canActivate renvoie un booléen (ou un Observable émettant un booléen, ou une Promise résolvant en booléen)
  • La valeur booléenne à renvoyer va être déterminée par la valeur courante stockée par l'observable currentUserSubject.
  • Un getter existe déjà pour simplifier l'accès à cette valeur depuis du code extérieur à AuthenticationService : currentUserValue.
  • Ajouter un champ canActivate dans la route ayant le path add-post, pour utiliser le guard

Après ces modifs, l'utilisateur sera directement renvoyé vers la page de login s'il essaie d'accéder à la page /add-post.

Dans un second temps, améliorer. Il serait intéressant, quand on redirige l'utilisateur vers la page d'accueil, de se souvenir de quelle page il souhaitait visiter. Ainsi, on redirigera vers la page de login, en indiquant dans l'URL un paramètre contenant le chemin de cette page. Une fois connecté, l'utilisateur sera redirigé automatiquement vers cette page.

Gestion des erreurs

Nous avions, par souci de temps, concentré nos efforts sur l'ajout des fonctionnalités, sans évoquer l'aspect de la gestion des erreurs.

Il est grand temps de s'y pencher, car sans une gestion d'erreurs appropriée, les utilisateurs seront confrontés à des problèmes inacceptables dans une application web moderne.

Par exemple :

  • Si le serveur de l'API est en panne, nos visiteurs ne verront que les parties navbar/header et footer de l'application. On aurait intérêt à les informer d'un dysfonctionnement, et à les inviter à revenir plus tard.
  • Si on essaie de se register avec une adresse email déjà inscrite dans la base de données sur le serveur, on aurait tout intérêt à en informer l'utilisateur.
  • Si on soumet des identifiants incorrects lors d'une requête de login, on devrait le signifier à l'utilisateur.

En l'état actuel des choses, aucun de ces cas d'erreur ne se matérialise dans l'interface.

Quelques sources à consulter:

Identifier les cas d'erreur à gérer

Voici une petite liste des endroits où on pourrait (et devrait) implémenter une gestion d'erreurs.

  • AuthenticationService, dans les méthodes register et login
  • PostService, dans les méthodes getAllPosts, getPost, createPost, deletePost (et éventuellement likePost, qu'on n'a pas implémentée… voire bonus)

Procédure à suivre

  • On peut commencer par prendre le code décrit dans la doc officielle d'Angular (1ère ressource)
  • Des explications plus complètes sur le fonctionnement de ce code sont données dans la 3ème ressource, sous la section intitulée "The Catch and Rethrow Strategy".
  • L'idée est, dans la méthode handleError, d'initialiser une variable errorMessage au tout début, contenant une chaîne vide.
  • Dans la 1ère "branche" du if, changer la valeur d'errorMessage avec une chaîne indiquant qu'il s'agit d'une erreur réseau.
  • Dans la 2nde branche, autrement dit dans le else, changer la valeur en se servant des données renvoyées par le serveur. Celles-ci consistent en un tableau d'erreurs accessible via error.error.errors (!!!). On peut par exemple faire un map pour récupérer uniquement la clé msg de chaque erreur, puis faire un join pour créer une string à partir de ce tableau de messages.
  • Enfin, on va passer cette valeur errorMessage au throwError à la fin.
Solution...

private handleError(error: HttpErrorResponse) {
    let errorMessage = '';
    if (error.status === 0) {
      errorMessage = 'Network error! Please check your connection or come back later!'
    } else {
      errorMessage = error.error.errors.map((err: { msg: string }) => err.msg).join('. ');
    }
    return throwError(errorMessage);
  }

Ceci marche bien pour les register et login.

Pour adapter ceci au PostService, il faudra adapter le code :

  • notamment, supprimer les .toPromise() pour utiliser les observables au lieu des promesses.
  • répercuter cette modification dans AddPostComponent : plus de .then et .catch mais un unique .subscribe, auquel on pourra passer un objet contenant des méthodes next (cas nominal) et error (cas d'erreur)
  • jusqu'ici, on avait passé directement une unique fonction à subscribe, appelée lorsque l'observable émet une nouvelle valeur. Passer un objet permet de gérer les cas d'erreur et de complétion d'un observable.
  • Un exemple est donné dans la section Subscribing de la page Using observables to pass values de la doc Angular.

Par ailleurs, le handleError du PostService devra gérer un problème dont l'AuthenticationService n'avait pas à s'occuper : l'expiration du JWT. Celui-ci n'a (idéalement !) pas une durée infinie. Quand il expire, le serveur renverra une erreur 401.

Dans handleError, on pourra vérifier si error.status vaut 401 pour gérer ce cas. Il faudra alors :

  • Déconnecter l'utilisateur
  • Le rediriger vers la page de login

Intercepteurs

Les intercepteurs sont un mécanisme qui permet d'inspecter et/ou transformer des requêtes et réponses HTTP gérées via le HttpClient.

Ils sont documentés dans la section Intercepting requests and responses du guide HTTP d'Angular, auquel on s'est déjà référé plus haut, quand on a évoqué la gestion des erreurs.

Ils ont plusieurs utilités (voir section Http interceptor use-cases de la même page). Par exemple :

  • Gérer les erreurs de façon plus centralisée les erreurs (éviter la duplication de code des errorHandler implémentés pour l'AuthenticationService et le PostService)
  • Positionner des en-têtes par défaut pour plusieurs requêtes, ce qui s'avèrera utile afin d'éviter de répéter l'insertion du header Authorization pour chaque requête devant être authentifiée.
  • Mettre en "cache" le résultat de certaines requêtes, de façon à éviter de requêter le backend plus que nécessaire

Le tuto sur l'authentification dont nous nous étions inspiré présente deux intercepteurs : l'un pour gérer les erreurs, l'autre pour ajouter le JWT à chaque requête. On peut s'en inspirer, moyennant des petites adaptations (nous ne stockons pas le token exactement comme le montre ce tuto).

Consigne :

  • metre en place un intercepteur pour envoyer automatiquement le JWT avec chaque requête
  • centraliser la gestion des erreurs 401 et 403 dans un 2ème intercepteur

Le tuto sur l'authentification dont nous nous étions inspiré présente deux intercepteurs : l'un pour gérer les erreurs, l'autre pour ajouter le JWT à chaque requête. On peut s'en inspirer, moyennant des petites adaptations (nous ne stockons pas le token exactement comme le montre ce tuto).

Bonus

Si vous avez tout fini, tout compris, etc. 😁

Implémenter la validation de champs de formulaires

Voir la doc : Validating form input. Attention à regarder plus attentivement ce qui concerne les "reactive forms" (ce que nous avons utilisé) plutôt que ce qui concerne les "template-driven forms".

Implémenter un vrai compteur de likes

C'est à dire, incrémenter les likes stockés dans la base de données du serveur pour chaque post. Le code serveur est en place, vous n'avez pas à vous en préoccuper. Dans la méthode like de CardComponent :

  • au lieu de stocker les likes dans l'attribut likes, appeler la méthode likePost de PostService, en lui passant l'id du post.
  • Cette méthode likePost à implémenter devra envoyer une requête sur l'URL /api/v2/posts/<id>/like, ou <id> sera remplacé par l'id reçu en paramètre.
  • On devra également passer le JWT dans les headers, en s'inspirant de ce qu'on avait fait pour createPost.
  • Pour afficher ce compteur de likes, il suffit d'afficher post.likes au lieu de likes dans le template card.component.html.
  • Cet attribut likes doit être ajouté à la définition du type Post.

Implémenter la mise à jour du profil

Vous pouvez créer un composant my-profile. Il sera accessible via un lien de la navbar, et "protégé" comme on l'a fait pour add-post.

Le formulaire comportera :

  • un champ login permettant de modifier le login/email de l'utilisateur actuellement connecté (il faut initialiser sa valeur en utilisant celle stockée dans le currentUserSubject du service d'authentification)
  • Un champ input file. Vous pouvez suivre ce tuto en supprimant le champ name du formulaire qui y est décrit.
  • Lorsque vous "uploaderez" les données, ce sera via une requête PUT vers le endpoint /api/v2/users/me. Les données devront contenir login (l'email) et un champ avatar qui contiendra les données du champ imgSrc du formulaire.
  • Donc, indice par rapport au tuto, où on envoie en "brut" this.uploadForm.value : il vous faudra envoyer le champ login et le champ imgSrc en renommant ce dernier avatar.
  • Le serveur vous répondra avec l'utilisateur mis à jour, dont le champ avatar pointant vers le chemin relatif de l'avatar par rapport au serveur.
  • Pour afficher cette image dans un tag img, il faudra concaténer ce chemin relatif à l'URL de base du serveur.

Ajouter des tags sur les posts

Outre les attributs title, description et picture d'un nouveau post, l'API accepte, sur la route POST vers /api/v2/posts, un champ tags qui doit comprendre des tags séparés par des virgules. Par exemple : iceland,mountains,lake. Exemple complet de "payload" (corps de requête) :

{
  "title": "Landmannalaugar - Iceland",
  "description": "In Landmannalaugar there are rhyolite mountains.",
  "picture": "https://i.imgur.com/WCzfMMt.jpeg",
  "tags": "iceland,mountains"
}

Côté Angular, une solution simple peut être d'ajouter un simple champ input (avec éventuellement un placeholder et un label suggérant le format de la valeur acceptée).

Une solution plus poussée peut être d'utiliser la bibliothèque ng-select (GitHub) dont la doc présente un exemple qui serait assez facilement adaptable : Tags. Si vous choisissez cette solution, le mieux est de prendre l'exemple "Custom tags" et l'adapter. C'est-à-dire qu'au final, lorsque vous soumettrez le formulaire, il faudra convertir le tableau de tags saisi en une chaîne, en vue de l'envoyer au backend.

Les tags sont renvoyés avec les posts sur les requêtes GET

Utiliser Bootstrap ou Angular Material

Notre application, en l'état, est moche spartiate. On avait un minimum vital de CSS en place sur le template d'origine, mais on ne s'en est pas préoccupé depuis.

Il y aurait plusieurs approches pour y remédier :

  • Ajouter du CSS à la main. C'est faisable mais potentiellement fastidieux.
  • Utiliser une bibliothèque CSS comme Bootstrap, Bulma, Tailwind, etc. Cela nous allégerait déjà considérablement la tâche.
  • Enfin, utiliser une bibliothèque de composants. C'est-à-dire un ensemble de composants développés spécifiquement pour Angular. Outre l'aspect esthétique, ces composants gèrent l'aspect dynamique de l'affichage (afficher/cacher un menu, des notifications, etc.).

La dernière option est intéressante et largement adoptée, notamment pour des applications d'entreprise où l'on s'efforce d'obtenir une esthétique cohérente, sans y consacrer d'efforts démesurés.

La bibliothèque de composants la plus connue est Angular Material.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment