• 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ésolvez les données

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 :

Des posts en JSON
Des posts en JSON

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 :

L'interface du site JSON2TS
La page d'accueil du 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'interface Resolve  – 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éthoderesolve 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 :

post-list works!
post-list works!

Et si vous allez creuser dans l'onglet Network/Réseau de vos DevTools, vous y verrez la requête du resolver !

La requête a été envoyée !
La requête a été envoyée !

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 :

Posts !
Posts !

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 ?

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