Vos applications Angular auront quasiment toutes besoin de récupérer des données pour les afficher aux utilisateurs.
Très souvent, on appellera une méthode de service dans le ngOnInit
d'un component. Cette méthode retournera un Observable qui correspond à une requête HTTP. On souscrit à cet Observable, la requête est envoyée, et quand le serveur répond, on affiche les données dans le template. La souscription se fait souvent avec le pipe async
.
Mais saviez-vous qu'il existe une autre approche ?
Les resolvers font partie du routing d'une application Angular. Quand un utilisateur navigue vers une route qui a besoin de récupérer des données, le resolver effectue la requête, et la navigation ne se termine que lorsque les données sont arrivées.
Cela vous permet, entre autres, d'éviter d'afficher un template avec des trous, ou un component où il manque des éléments : tout peut s'afficher d'un seul coup.
Découvrons ensemble la puissance des resolvers.
Installez le backend
Je vous invite à cloner ce repo, à effectuer un npm install
, et puis à exécuter npm run start
. Cela lancera un backend extrêmement simple qui vous servira pour tout le reste de ce cours, que vous trouverez sur ce repository GitHub.
Préparez la requête
Si vous pointez votre navigateur sur http://localhost:3000/posts, vous verrez le JSON que renverra le backend :
Visiblement, vous avez deux modèles à créer : le modèle Post et le modèle Comment.
Là où SocialMediaModule sera le seul module à consommer des Posts, le component pour les Comments que vous créerez par la suite sera partagé par toute l'application. Le modèle Post sera donc enregistré dans le dossier social-media
, et le modèle Comment dans le dossier core
.
Dans core
, créez un dossier models
, et un fichier comment.model.ts
:
export class Comment {
}
Pour faciliter la création du modèle à partir du JSON retourné par le serveur, vous pouvez utiliser le site JSON2TS :
Ce site permet de générer une interface TypeScript (que nous transformerons en classe) à partir de données JSON.
Sur la page retournée par http://localhost:3000/posts, sélectionnez tout (Ctrl+A sur PC, Cmd+A sur Mac), copiez, et collez dans JSON2TS. En appuyant sur generate TypeScript, vous aurez :
declare module namespace {
export interface Comment {
id: number;
userId: number;
comment: string;
createdDate: Date;
}
export interface RootObject {
id: number;
userId: number;
title: string;
createdDate: Date;
imageUrl: string;
content: string;
comments: Comment[];
}
}
Le site a réussi à générer les deux types ( RootObject
correspond à Post – le convertisseur ne peut pas deviner le nom). Ignorons le namespace
et transformons le tout en classe.
Pensez à changer le type de createdDate
en string
pour les deux modèles : même si la string correspond à une Date, ce n'est pas un objet Date.
Vous aurez donc core/models/comment.model.ts
:
export class Comment {
id!: number;
userId!: number;
comment!: string;
createdDate!: string;
}
Et social-media/models/post.model.ts
:
import { Comment } from '../../core/models/comment.model';
export class Post {
id!: number;
userId!: number;
title!: string;
createdDate!: string;
imageUrl!: string;
content!: string;
comments!: Comment[];
}
Les modèles sont prêts : passons au service, très simple ici, mais avec deux nuances.
Créez un dossier services
dans social-media
et créez-y posts.service.ts
:
import { Injectable } from '@angular/core';
@Injectable()
export class PostsService {
}
Comme vous pouvez le constater, vous n'ajoutez pas { providedIn: 'root' }
au décorateur @Injectable()
.
Puisque SocialMediaModule est lazy-loaded et que PostsService ne sert qu'à l'intérieur de SocialMediaModule, ça ne nous intéresse pas que ce service soit chargé à la racine de l'application. On voudrait qu'il soit lié uniquement au module où il sert.
Pour cela, vous allez ajouter un tableau providers
à SocialMediaModule :
@NgModule({
declarations: [],
imports: [
CommonModule,
SocialMediaRoutingModule
],
providers: [
PostsService
]
})
Cette manière de provide le service nous fournit le résultat souhaité !
Vous aurez besoin du HttpClient pour envoyer des requêtes au serveur. Puisque HttpClientModule ne sera importé qu'une fois dans l'application, c'est un candidat parfait pour être importé dans CoreModule :
@NgModule({
declarations: [
HeaderComponent
],
imports: [
CommonModule,
SharedModule,
RouterModule,
HttpClientModule
],
exports: [
HeaderComponent
]
})
Maintenant vous pouvez créer la méthode getPosts()
qui retournera la requête pour récupérer tous les Posts du backend :
@Injectable()
export class PostsService {
constructor(private http: HttpClient) {}
getPosts(): Observable<Post[]> {
return this.http.get<Post[]>('http://localhost:3000/posts');
}
}
Vous pourriez vous arrêter là, avec l'URL codée en dur, mais je vous propose une autre approche : enregistrer la racine de l'URL du backend dans une variable environnement.
Les variables environnement permettent à des variables de votre application – notamment pour la configuration – de prendre des valeurs différentes selon l'environnement, comme le développement vs. la production.
Ce sera très souvent le cas, par exemple, avec les URL de backend. Vous n'utiliserez certainement pas la même API pour le développement que pour la production.
Dans une application Angular, vous avez un dossier environments
qui contient les fichiers environment.ts
et environment.prod.ts
, pour le développement et la production respectivement. Dans l'objet qui s'y trouve, je vous propose d'ajouter une paire clé-valeur :
environment.ts
:
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
};
environment.prod.ts
:
export const environment = {
production: true,
apiUrl: 'http://localhost:3000',
};
Comme ça, dans le service, la méthode devient :
// autres imports
// ...
import { environment } from '../../../environments/environment';
@Injectable()
export class PostsService {
constructor(private http: HttpClient) {}
getPosts(): Observable<Post[]> {
return this.http.get<Post[]>(`${environment.apiUrl}/posts`);
}
}
Avec ce montage, lorsque vous ferez le build de votre application, Angular injectera les valeurs selon la configuration choisie.
Tout est en place : vous pouvez maintenant créer le resolver qui appellera cette méthode et les components qui afficheront les données.
Résolvez les données
Tout d'abord, on importe HttpClientModule
dans core.module.ts
.
Puis, dans le dossier social-media
, créez un dossier resolvers
et créez-y un fichier posts.resolver.ts
:
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Post } from '../models/post.model';
import { PostsService } from '../services/posts.service';
import { Observable } from 'rxjs';
@Injectable()
export class PostsResolver implements Resolve<Post[]> {
constructor(private postsService: PostsService) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Post[]> {
return this.postsService.getPosts();
}
}
Ici, vous :
avez une classe
@Injectable
, comme un service, qui implémente l'interfaceResolve
– vous y passez le type des données qui seront récupérées par ce resolver ;injectez PostsService pour avoir accès à ses méthodes ;
implémentez une méthode
resolve
qui :accepte des
snapshot
de la route active et de son état – vous ne vous en servirez pas ici, mais sachez que chaque resolver reçoit automatiquement ces arguments,retourne les données récupérées – elles peuvent être retournées dans un Observable, une Promise, ou "en vrac". Ici on préférera l'Observable pour sa flexibilité.
Vous remarquerez que le resolver retourne l'Observable sans y souscrire. C'est Angular qui effectuera la souscription quand il le faudra.
Il faut ajouter le nouveau resolver aux providers
de SocialMediaModule pour qu'il soit utilisable :
@NgModule({
// ...
providers: [
PostsService,
PostsResolver
]
})
export class SocialMediaModule { }
Puisque le resolver s'enregistre dans le routing de l'application, il vous faut créer une route dans SocialMediaRoutingModule, et pour créer une route, il vous faut un component :
ng g c social-media/components/post-list
Cela vous permet d'ajouter une route à SocialMediaRoutingModule :
const routes: Routes = [
{ path: '', component: PostListComponent, resolve: { posts: PostsResolver } }
];
L'objet resolve
que vous passez à la configuration de la route lie les données récupérées par le resolver à une clé posts
qui permettra à PostListComponent d'y accéder.
Si vous enregistrez le tout, et allez regarder dans l'application, vous devriez voir :
Et si vous allez creuser dans l'onglet Network/Réseau de vos DevTools, vous y verrez la requête du resolver !
Vous voyez donc que sans avoir souscrit à quoi que ce soit, le simple fait d'enregistrer ce resolver sur la route a généré la requête souhaitée.
Mais comment faire pour profiter de ces données ?
Accédez aux données
Les données du resolver sont sous forme d'Observable, donc vous allez créer un Observable correspondant dans PostListComponent :
export class PostListComponent implements OnInit {
posts$!: Observable<Post[]>;
}
Puisque le resolver fait partie du routing, les données qu'il récupère sont à disposition sur l'Observable data
dansActivatedRoute
. Injectez-le dans PostListComponent et initialisez posts$
:
export class PostListComponent implements OnInit {
posts$!: Observable<Post[]>;
constructor(private route: ActivatedRoute) { }
ngOnInit(): void {
this.posts$ = this.route.data.pipe(
map(data => data['posts'])
);
}
}
L'Observable data
émet l'objet créé dans la configuration de route, et donc vous récupérez les données du resolver avec la clé posts
.
Pour implémenter l'architecture container/presenter, vous allez maintenant créer le component enfant :
ng g c social-media/components/post-list-item
Ce component aura un @Input
post
de type Post et affichera, pour l'instant, le titre du Post dans un paragraphe dans son template.
post-list-item.component.ts
:
export class PostListItemComponent implements OnInit {
@Input() post!: Post;
}
post-list-item.component.html
:
<p>{{ post.title | titlecase }}</p>
Comme ça, dans le template de PostListComponent :
<h2>Posts</h2>
<app-post-list-item *ngFor="let post of posts$ | async" [post]="post"></app-post-list-item>
Avec quelques styles simples dans post-list.component.scss
:
h2 {
color: white;
}
Vous pouvez aussi wrap le router-outlet
dans AppComponent dans une div
de classe main-content
et ajouter les styles suivants à app.component.scss
pour limiter la largeur du contenu de l'application :
.main-content {
box-sizing: border-box;
padding: 20px;
max-width: 800px;
margin: auto;
}
Tout cela donne :
Vous avez donc implémenté le resolver correctement, et votre component a accès aux données !
En résumé
Un resolver est un outil de routing qui est appelé lorsqu'un utilisateur cherche à accéder à la route où il est placé.
Le resolver récupère des données avant d'afficher la route souhaitée via sa méthode
resolve()
.Cette méthode retourne les données sous forme soit d'Observable, soit de Promise, ou "en vrac".
Le resolver est enregistré au niveau de la configuration de routing, et est associé à une clé d'objet.
Le component cible de la route utilise ensuite l'Observable
data
de ActivatedRoute pour récupérer les données via cette même clé.
Dans le prochain chapitre, vous découvrirez l'une des grandes forces d'Angular : la possibilité de créer des components réutilisables. Vous êtes prêt ?