Débutant

Angular 4 : Pas à pas – Partie 2

Lors de l’article précédent (que je vous conseille très fortement de lire avant de commencer celui-ci), nous avons créé une application Angular from scratch et appris à faire du data binding. Maintenant, poursuivons notre découverte d’Angular et intéressons-nous au tout nouveau HttpClient (sorti pour la version 4.3 d’Angular) afin d’interroger un WebAPI et de récupérer des données backend.
Pour ce qui est de la navigation, nous allons nous familiariser avec les différentes techniques de routage et à la fin de ce tutoriel nous créerons notre premier module Angular.

Services

Les services permettent de factoriser le code, pour respecter SRP (Single Responsability Principle). Le service sera donc injecté et utilisé dans ces composants.

Angular CLI nous permet de générer un service grâce à la commande ng generate service products/service/product --flat -m app.module…. Je vous vois ébahis, vos yeux grands écarquillés… Détaillons cette ligne de commande :

  • ng generate service products/service/product : permet de générer le fichier product.service.ts dans le dossier products/service/ ; notez que l’on peut remplacer ng generate service par ng g s
  • --flat : nous voulons que product.service.ts soit à la racine du dossier products/service/. Sans cette option nous aurions eu products/service/product/product.service.ts
  • -m app.module : permet de déclarer le service dans le module spécifié, nous évite de le faire manuellement.

Regardons cela plus en détail :

La commande nous génère le service ainsi que sa classe de test (on peut désactiver la génération du fichier .spec.ts associé via l’option --spec) et met à jour AppModule en déclarant ProductService dans le tableau des providers, le mettant à disposition de tous les composants de ce module.

Attention : Il faut se rappeler qu’une dépendance est par défaut un singleton dans la portée (scope) de l’injecteur. Lorsque nous déclarons notre service comme provider du module AppModule, il est donc un singleton pour toute l’application.

Voici à quoi ressemble notre service :


import { Injectable } from '@angular/core';

@Injectable()
export class ProductService {
  constructor() { }
}

C’est une classe avec un décorateur spécifique @Injectable() qui permet de faire savoir à Angular que cette classe peut être utilisée avec l’injection de dépendances.

Le service que nous venons de créer nous permettra de récupérer la liste des produits. On fera appel à notre service depuis ProductComponent pour récupérer cette liste.

Nous allons créer la méthode getProducts() : IProduct[] dans notre service :

import { Injectable } from "@angular/core";

@Injectable()
export class ProductService {
  getProducts(): IProduct[] {
    return [
      {
        productId: 1,
        productName: "Leaf Rake",
        productCode: "GDN-0011",
        releaseDate: "March 19, 2016",
        description: "Leaf rake with 48-inch wooden handle.",
        price: 19.95,
        starRating: 3.2,
        imageUrl: "http://openclipart.org/image/300px/svg_to_png/26215/Anonymous_Leaf_Rake.png"
      },
      {
        productId: 2,
        productName: "Garden Cart",
        productCode: "GDN-0023",
        releaseDate: "March 18, 2016",
        description: "15 gallon capacity rolling garden cart",
        price: 32.99,
        starRating: 4.2,
        imageUrl: "http://openclipart.org/image/300px/svg_to_png/58471/garden_cart.png"
      }
    ];
  }
}

Afin de l’utiliser dans notre composant, il suffit de le déclarer en private dans le constructor. En effet, TypeScript permet de créer des propriétés directement depuis les arguments du constructeur, en indiquant leur portée (public, protected ou private). Mais on peut aussi déclarer la propriété en dehors du constructeur et affecter la valeur dans le constructeur.
Il est également conseillé de les déclarer en readonly pour indiquer que l’on ne compte pas changer sa valeur. Cela favorise la construction d’un objet complet, dans un état totalement défini, ce qui n’est pas le cas si l’on doit attendre que d’autres propriétés soient définies en dehors du constructeur :

  constructor(private readonly _productService: ProductService) {
    this.products = this._productService.getProducts();
    this.filteredProducts = this.products;
  }

Votre code est censé continuer de fonctionner. Nous avons déclaré une liste de produits en dur dans le service, qui sera récupérée dans notre composant afin d’être affichée, mais que faire si l’on souhaite récupérer nos données d’un fichier JSON ou bien d’une API ? Pas de panique, Angular nous met à disposition le module HttpClient et coup de bol incroyable, c’est ce que nous allons voir tout de suite ^^.

HttpClient

Le module HttpClient est un module optionnel d’Angular qui vous permet de requêter vos API à l’aide du protocole HTTP. HttpClient supporte les verbes HTTP lors de l’exécution de vos requêtes. Le service HttpClient propose ainsi les méthodes get, post, put, delete, head et patch.

Je vous ai mis à disposition une liste de produits dans ‘api/product/product.json’. Nous allons utiliser l’HttpClient pour faire un appel http et récupérer les données de ce json.
Nous allons commencer par déclarer le HttpClientModule dans notre AppModule comme suit :

Ensuite nous allons injecter l’HttpClient dans notre service et faire notre appel http :

import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx";

import { IProduct } from "../product";

@Injectable()
export class ProductService {
  private _productUrl = "./api/products/products.json";

  constructor(private readonly _httpClient: HttpClient) {}

  getProducts(): Observable<IProduct[]> {
    return this._httpClient.get<IProduct[]>(this._productUrl);
  }
}

Note : Il n’est pas nécessaire de déclarer le type de retour de getProducts() Observable<IProduct[]>. L’inférence du type à partir de httpClient.get<IProduct[]>() suffit. Nous l’avons gardé pour faciliter la lecture du code.

Maintenant, il nous faut adapter notre ProductComponent pour souscrire à notre Observable :

    constructor(private readonly _productService: ProductService) {
        this._productService.getProducts().subscribe(
            products => {
                this.products = products;
                this.filteredProducts = this.products;
            }
        );
    }

Et là tout devrait fonctionner… Non ? Ah oui c’est vrai, nous devons dire à Angular de prendre en compte le dossier “api” en l’ajoutant comme assets dans angular-cli.json :

Il faudra killer la commande ng serve et la relancer pour que le changement soit pris en compte.

Le routage

L’un des rôles d’une application Web est de pouvoir rediriger vers des pages HTML au travers de liens HyperText. La balise responsable de ce routage est <a href="…">. Lorsque l’on clique sur un lien HyperText, le navigateur intercepte cette action et va charger la nouvelle page. Il en résulte la perte de tout contexte JavaScript et un manque de fluidité de l’application.

Pour une application Angular, il n’est pas souhaitable de perdre tout son contexte à chaque changement de page. Angular a donc besoin d’un système de routage qui permet la navigation à travers différentes vues de l’application. Pour réaliser ce routage, Angular propose le module RouterModule disponible dans la librairie @angular/router.

À chaque clic sur un lien ou à chaque changement d’url du navigateur, Angular router effectue ces sept étapes :

  1. Analyse l’url
  2. Applique les redirections
  3. Identifie les états du routeur
  4. Exécute les guards
  5. Détermine les données
  6. Active tous les composants nécessaires pour afficher la page
  7. Gère la navigation

Pour plus d’informations sur le fonctionnement du routage, je vous conseille de lire cet article.

Notre premier routage

Nous allons donc mettre en place un routage pour notre application :

Nous avons importé deux éléments de la librairie @angular/router, à savoir Routes et RouterModule:

  • Routes est un tableau contenant la déclaration des routes
  • RouterModule est un module regroupant les directives et les services paramétrables permettant de remplir la fonctionnalité de routage.

Nous avons créé une constante contenant nos routes et avons fait appel à la fonction statique forRoot() qui prend en paramètre un tableau de Route.

L’ordre des éléments du tableau est important: si l’on met le wildcard ('**') en premier, toutes les URL seront redirigées vers la page d’accueil.

Lier les routes aux actions

Contrairement à une application Web classique, il est fastidieux d’utiliser l’attribut href dans nos balises. Angular nous fournit donc la directive : routerLink. Cette directive prend en paramètre un tableau contenant un path et des paramètres optionnels (query parameters, fragment, etc.).
Une fois nos liens déclarés, il est nécessaire d’indiquer à Angular où charger le contenu du lien. Cela se fait à l’aide de la directive router-outlet qui va accueillir le composant associé à la route.

Nous allons donc modifier le Template du composant principal AppComponent :


<div>
  <nav class='navbar navbar-default'>
      <div class='container-fluid'>
          <a class='navbar-brand'>{{pageTitle}}</a>
          <ul class='nav navbar-nav'>
              <li><a [routerLink]="['welcome']">Home</a></li>
              <li><a [routerLink]="['products']">Product List</a></li>
          </ul>
      </div>
  </nav>
  <div class='container'>
      <router-outlet></router-outlet>
  </div>
</div>


Ce qui nous donne le résultat suivant :

Utilisation de paramètres de route

Que faire si l’on souhaite voir le détail d’un produit ? Il nous faut configurer une route qui prend l’id du produit comme paramètre et qui nous redirige vers la page contenant le détail du produit.
Tout d’abord, nous allons créer ProductDetailComponent (en utilisant Angular CLI ng g c products/product-detail) et ensuite nous allons modifier les routes :

Nous avons ajouté le paramètre “id” en ajoutant les ‘:’ devant son nom. Le chargement de cette route doit nous afficher le produit correspondant à l’identifiant dans l’url. Nous devons donc créer une méthode qui prend en paramètre un “id” et qui nous renvoie le produit correspondant. Cela se fait dans le service :

import "rxjs/add/operator/map";

import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Rx";

import { IProduct } from "../product";

@Injectable()
export class ProductService {
  private _productUrl = "./api/products/products.json";

  constructor(private _httpClient: HttpClient) {}

  getProducts(): Observable<IProduct[]> {
    return this._httpClient.get<IProduct[]>(this._productUrl);
  }

  getProduct(id: number): Observable<IProduct> {
    return this.getProducts().map((products: IProduct[]) =>
      products.find(p => p.productId === id)
    );
  }
}

Mettons un peu d’HTML dans product-detail.component.html :


<div class='panel panel-primary' *ngIf='product'>
  <div class='panel-heading'>
    {{pageTitle + ': ' + product.productName}}
  </div>

  <div class='panel-body'>
    <div class='row'>
      <div class='col-md-6'>
        <div class='row'>
          <div class='col-md-3'>Name:</div>
          <div class='col-md-6'>{{product.productName}}</div>
        </div>
        <div class='row'>
          <div class='col-md-3'>Code:</div>
          <div class='col-md-6'>{{product.productCode | lowercase}}</div>
        </div>
        <div class='row'>
          <div class='col-md-3'>Description:</div>
          <div class='col-md-6'>{{product.description}}</div>
        </div>
        <div class='row'>
          <div class='col-md-3'>Availability:</div>
          <div class='col-md-6'>{{product.releaseDate}}</div>
        </div>
        <div class='row'>
          <div class='col-md-3'>Price:</div>
          <div class='col-md-6'>{{product.price|currency:'USD':true}}</div>
        </div>
      </div>

      <div class='col-md-6'>
        <img class='center-block img-responsive' [style.width.px]='200' [style.margin.px]='2' [src]='product.imageUrl' [title]='product.productName'>
      </div>
    </div>
  </div>

  <div class='panel-footer'>
    <a class='btn btn-default' (click)='onBack()' style='width:80px'>
      <i class='glyphicon glyphicon-chevron-left'></i> Back
    </a>
  </div>
</div>

Maintenant, il nous faut ajouter du code dans notre composant ProductDetailComponent :

import { Component, OnInit } from "@angular/core";

import { IProduct } from "../product";
import { ProductService } from "../service/product.service";

@Component({
  templateUrl: "./product-detail.component.html",
  styleUrls: ["./product-detail.component.css"]
})
export class ProductDetailComponent implements OnInit {
  pageTitle: string = "Product Detail";
  errorMessage: string;
  product: IProduct;

  constructor(
    private _route: ActivatedRoute,
    private _router: Router,
    private _productService: ProductService
  ) {}

  ngOnInit() {}

  getProduct(id: number) {
    this._productService
      .getProduct(id)
      .subscribe(
        product => (this.product = product),
        error => (this.errorMessage = <any>error)
      );
  }

  onBack(): void {}
}

On crée notre variable product de type IProduct ; on instancie notre service dans le constructeur puis dans notre méthode getProduct ; on souscrit (subscribe) à notre Observable.

Nous allons utiliser (encore une fois) la directive routerLink sur la liste des produits, mais cette fois nous lui passerons l’id du produit comme paramètre :


            <td>
              <a [routerLink]="['/products', product.productId]">
                {{ product.productName }}
              </a>
            </td>

Il nous faudra récupérer l’id du produit de la route pour l’utiliser dans notre méthode. Pour cela, Angular nous met à disposition ActivatedRoute qui contient les informations des routes associées à notre composant. Cela nous permettra de récupérer le productId :


import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

import { IProduct } from '../product';
import { ProductService } from '../service/product.service';

@Component({
  templateUrl: './product-detail.component.html',
  styleUrls: ['./product-detail.component.css']
})
export class ProductDetailComponent implements OnInit {
  pageTitle: string = 'Product Detail';
  errorMessage: string;
  product: IProduct;

  constructor(private _route: ActivatedRoute,
    private _router: Router,
    private _productService: ProductService) {
  }

  ngOnInit() {
    const id = +this._route.snapshot.paramMap.get('id');
    this.getProduct(id);
  }

getProduct(id: number) {
  this._productService.getProduct(id).subscribe(
    product =< this.product = product,
    error =< this.errorMessage = <any<error);
}

  onBack(): void {
  }

}


On récupère l’id grâce à la méthode this._route.snapshot.paramMap.get('id') ; le + sert à convertir une chaîne de caractères en entier.
La méthode ngOnInit fait partie du cycle de vie d’un composant Angular. Elle est appelée juste après la création du composant. C’est pour cela que nous y avons mis le code nécessaire à la récupération de notre produit, afin de charger le contenu à l’initialisation du composant.

Il nous reste la méthode onBack() que nous n’avons pas encore implémentée… Je vous laisse le faire : vous aurez besoin d’utiliser la classe Router.

Et voilà :

Les modules

Un module Angular représente une classe contenant le décorateur @NgModule. Son but est de mieux organiser les parties de l’application en les rangeant dans des blocs fonctionnels. Jetons un coup d’œil à l’architecture de notre application en utilisant compodoc :

Voilà à quoi ressemble notre application : il n’y a qu’un seul module pour tous les services et les composants. Afin de mieux organiser notre code et de le rendre plus gérable, il convient de créer des modules définis par leur fonctionnalité. Dans notre cas nous aurons un module ProductModule en plus pour la partie produit. Il regroupera tous les composants ayant la même fonctionnalité logique.

Création de notre premier module

Nous allons utiliser Angular CLI afin de créer notre ProductModule : la commande est la suivante : ng g m products/product --flat -m app.module

Et voilà ! La coquille vide de notre nouveau module :


import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: []
})
export class ProductModule { }


Maintenant il nous faut :
* Importer FormsModule et RouterModule
* Déclarer ProductListComponent et ProductDetailComponent
* Ajouter ProductService dans la liste des providers
* Créer les routes


import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';

import { ProductDetailComponent } from './product-detail/product-detail.component';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductService } from './service/product.service';

const routes: Routes = [
  { path: 'products', component: ProductListComponent },
  { path: 'products/:id', component: ProductDetailComponent }
];

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    RouterModule.forChild(routes),
  ],
  declarations: [
    ProductListComponent,
    ProductDetailComponent
  ],
  providers: [ProductService],
})
export class ProductModule { }

Et l’on met à jour AppModule en supprimant ce que l’on a ajouté dans ProductModule :


import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule, Routes } from '@angular/router';

import { AppComponent } from './app.component';
import { WelcomeComponent } from './home/welcome.component';
import { ProductModule } from './products/product.module';

const routes: Routes = [
  { path: 'welcome', component: WelcomeComponent },
  { path: '', redirectTo: 'welcome', pathMatch: 'full' },
  { path: '**', redirectTo: 'welcome', pathMatch: 'full' }
];

@NgModule({
  declarations: [
    AppComponent,
    WelcomeComponent,
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    RouterModule.forRoot(routes),
    ProductModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Au niveau du routage, il y a une petite différence entre nos deux modules ; dans AppModule nous avons fait appel à la méthode statique RouterModule.forRoot afin d’enregistrer les routes au niveau de l’application, dans un sous module nous utiliserons RouterModule.forChild

Appelez uniquement RouterModule.forRoot dans la racine AppModule. Dans tout autre module, vous devez appeler la méthode RouterModule.forChild pour enregistrer des routes supplémentaires.

Après ces modifications, voici à quoi ressemble l’architecture de notre application :

Nettement mieux n’est-ce pas ?

Conclusion

Dans cette seconde partie, nous avons appris à créer des services réutilisables et injectables partout, et à utiliser le HttpClient d’Angular afin de faire des appels API. Nous nous sommes également familiarisés avec le routage d’Angular afin de naviguer dans notre SPA, et pour finir nous avons découpé notre application en deux modules distincts fonctionnellement.

Si vous souhaitez aller plus loin, je vous conseille les cours de Deborah Kurata sur pluralsight ainsi que ceux de John Papa

Pour rappel, le code lié à cet article est disponible dans son intégralité sur le GitHub de Soat.

© SOAT
Toute reproduction interdite sans autorisation de l’auteur

Nombre de vue : 781

COMMENTAIRES 9 commentaires

  1. Farid dit :

    Merci pour cet article, simple à comprendre, concis et va à l’essentiel. Bravo !

  2. Lamine BENDIB dit :

    Merci Farid, pour ce commentaire. Cela me fait plaisir et me motive à écrire d’autres articles.

  3. Hamidou dit :

    Très bon article,Lamine

  4. Rabii dit :

    Merci Lamine, franchement rien à dire chapeau 🙂
    en ce moment que ça bouge pas mal angular y’a 5 mois j’ai commencé en angular 2 et là la version 5 est déjà sortie.

  5. Lamine BENDIB dit :

    Je suis vraiment ravi que cet article vous plaise.
    @Rabii c’est vrai qu’il y a eu beaucoup de changement sur la scène Angular. Les versions beta d’Angular 2 était vraiment différentes entre elles, beaucoup trop de changement qui en ont dissuadé plus d’un. Aujourd’hui le framework semble se stabiliser, les changements sont “plus doux”, la base est la même. Donc ne t’inquiète pas, si tu peux coder en Angular 4 tu n’auras pas de soucis à te faire en Angular 5. Si ça peut d’intéresser, voici un peu de [lecture](https://blog.angular.io/version-5-0-0-of-angular-now-available-37e414935ced)

  6. Paris dit :

    tu as utilisé des classes bootstrap. je pense qu’il manque peut être un détail dans cette partie qui permet d’avoir le même resultat au niveau design: c’est d’indiquer dans le fichier index.html :
    1) lien vers bootstrap,
    2) et la class=’container’,

  7. Paris dit :

    au niveau du body

  8. Paris dit :

    Bonjour Lamine,
    le commentaire que j’ai posté concerne la première partie(c’était un détail).
    pour cette deuxième partie rien à dire. merci beaucoup.

  9. Lamine BENDIB dit :

    Hello hello 🙂
    Ah j’aime bien quand il n’y a rien à dire :D. En tout cas merci à toi d’avoir pris le temps de suivre ce tuto et de m’avoir fait part de tes retours. Pour ce qui est de la déclaration de boostrap, je ne l’ai effectivement pas expliqué, en fait tu as plusieurs façons de l’inclure à ton projet. Il faut d’abord rajouter la dépendance à ton “package.json” et ensuite rajouté le path dans le tableau “styles” du fichier de configuration d’Angular CLI qui se trouve à la racine de ton projet “.angular-cli.json” c’est ce que j’ai fait sur le projet qui se trouve sur Github (https://github.com/SoatGroup/angular-project/blob/master/.angular-cli.json).

    Pour voir l’autre façon de faire c’est par ici => https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/include-bootstrap.md

AJOUTER UN COMMENTAIRE