Angular/mon-projet-angular

De WikiSys
Aller à : navigation, rechercher
 cd ~/Angular/my-project
 ng test --help

Préparez le projet

su
cd ~/Angular
ng new mon-projet-angular --style=scss --skip-tests
cd mon-projet-angular
npm install bootstrap@3.3.7 --save
npm audit fix
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.8 (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.8: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

bootstrap

npm install bootstrap@3.3.7 --save

Cette commande téléchargera Bootstrap et l'intégrera dans le package.json du projet. Il vous reste une dernière étape pour l'intégrer à votre projet. Ouvrez le fichier angular.json du dossier source de votre projet.

Dans "architect/build/options", modifiez l'array styles comme suit :

"styles": [
    "./node_modules/bootstrap/dist/css/bootstrap.css",
    "styles.scss"
]
Error
ERROR in multi ./node_modules/bootstrap/dist/css/bootstrap.css ./styles.css
Module not found: Error: Can't resolve '/home/alain/Angular/propositions/styles.css' in '/home/alain/Angular/propositions'
Cure
"styles": [
    "./node_modules/bootstrap/dist/css/bootstrap.css",
    "src/styles.css"
]

ng serve

ng serve &
firefox http://localhost:4200

La structure des components d'une application Angular

9384317693 Screen Shot 2018-02-23 at 12.11.04.png

Tout d'abord, notre AppComponent est notre component principal : tous les autres components de notre application seront emboîtés ou "nested" dans celui-ci.

Pour cette structure, on peut imaginer

  • un component pour la barre de menu,
  • un autre pour la partie contenu
  • et un dernier pour le menu à droite.

Il n'y a pas de règle d'or : je vous montrerai au cours des chapitres suivants comment réfléchir à votre structure afin de trouver la séparation des components la plus pertinente

Découvrez la structure du code

Dans ~/Angular/mon-projet-angular

  • Le dossier e2e est généré pour les tests end-to-end,
  • Le dossier node_modules contient toutes les dépendances pour votre application : les fichiers source Angular et TypeScript, par exemple.
  • Le dossier src contient tous les fichiers sources pour votre application.

src

  • index.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>MonProjetAngular</title>
    <base href="/">
    
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
    <app-root></app-root>
</body>
</html>

au lieu d'y voir tout le contenu que nous voyons dans le navigateur, il n'y a que cette balise vide <app-root>  : il s'agit d'une balise Angular.

src/app/

  • app.component.html le module principal de l'application et les trois fichiers du component principal AppComponent  : son template en HTML, sa feuille de styles en SCSS, et son fichier TypeScript, qui contiendra sa logique.

les trois fichiers du component principal AppComponent  :

  • app.component.html le template en HTML, code HTML correspondant à ce que vous voyez dans votre navigateur. Donc comment fait Angular pour injecter ce code dans la balise <app-root> ?
  • app.component.scss feuille de styles en SCSS,
  • app.component.ts fichier TypeScript, qui contient sa logique : à l'intérieur du décorateur @Component() , vous trouvez un objet qui contient les éléments suivants :
    • selector  : il s'agit du nom qu'on utilisera comme balise HTML pour afficher ce component, comme vous l'avez vu avec <app-root>. Ce nom doit être unique et ne doit pas être un nom réservé HTML de type <div>,<body> etc. On utilisera donc très souvent un préfixe comme app-.
    • templateUrl  : le chemin vers le code HTML à injecter ;
    • styleUrls  : un array contenant un ou plusieurs chemins vers les feuilles de styles qui concernent ce component;

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'app';
}

Quand Angular (ng) rencontre la balise <app-root> dans le document HTML src/index.html, il sait qu'il doit en remplacer le contenu par celui du template app.component.html , en appliquant les styles app.component.scss , le tout géré par la logique du fichier app.component.ts .

modifier la variable title dans app.component.ts , enregistrer le fichier, et regarder le résultat dans votre navigateur.

Créez un component

Vous allez maintenant créer un nouveau component à l'aide du CLI d'Angular.

cd ~/Angular/mon-projet-angular
ng generate component mon-premier 

ng a créé un nouveau sous-dossier ~/Angular/mon-projet-angular/src/app/mon-premier

avec :

-rw-r--r-- 1 achadde users  30 avril 17 10:32 mon-premier.component.html
-rw-r--r-- 1 achadde users   0 avril 17 10:32 mon-premier.component.scss
-rw-r--r-- 1 achadde users 289 avril 17 10:32 mon-premier.component.ts

mis à jour du fichier app.module.ts

ng a ajouté MonPremierComponent à l'array declarations

@NgModule({
  declarations: [
    AppComponent,
    MonPremierComponent
  ],

fichier ~/Angular/mon-projet-angular/src/app/mon-premier/mon-premier.component.ts ng a créé un sélecteur : app-mon-premier

@Component({
  selector: 'app-mon-premier',
  templateUrl: './mon-premier.component.html',
  styleUrls: ['./mon-premier.component.scss']
})

dans app.component.html introduire <app-mon-premier></app-mon-premier> après </div du titre. Dans le navigateur, on voit le même titre qu'avant et, sur une deuxième ligne, le texte mon-premier works. Il s'agit du texte par défaut créé par le CLI que vous trouverez dans mon-premier.component.html

<p>
  mon-premier works!
</p>

Gérez des données dynamiques

Angular gére le DOM (Document Object Model : les éléments HTML affichés par le navigateur) de manière dynamique, et pour cela, il faut utiliser la liaison de données, ou "databinding".

Le databinding, c'est la communication entre votre code TypeScript et le template HTML qui est montré à l'utilisateur. Cette communication est divisée en deux directions :

  • les informations venant de votre code qui doivent être affichées dans le navigateur, comme par exemple des informations que votre code a calculées ou récupérées sur un serveur. Les deux principales méthodes pour cela sont le string interpolation et le property binding ;
  • les informations venant du template qui doivent être gérées par le code : l'utilisateur a rempli un formulaire ou cliqué sur un bouton, et il faut réagir et gérer ces événements. On parlera de event binding pour cela.

Il existe également des situations comme les formulaires, par exemple, où l'on voudra une communication à double sens : on parle donc de two-way binding.

String interpolation

L'interpolation est la manière la plus basique d'émettre des données issues de votre code TypeScript.

Imaginez une application qui vérifie l'état de vos appareils électriques à la maison pour voir s'ils sont allumés ou non. Créez maintenant un nouveau component AppareilComponent avec la commande suivante :

cd ~/Angular/mon-projet-angular/src/app
ng generate component appareil

Ouvrez ensuite appareil.component.html (dans le nouveau dossier appareil créé par le CLI), supprimez le contenu, et entrez le code ci-dessous :

<li class="list-group-item">
  <h4>Ceci est dans AppareilComponent</h4>
</li>

Ensuite, ouvrez app.component.html , et remplacez tout le contenu comme suit :

<div class="container">
  <div class="row">
    <div class="col-xs-12">
      <h2>Mes appareils</h2>
      <ul class="list-group">
        <app-appareil></app-appareil>
        <app-appareil></app-appareil>
        <app-appareil></app-appareil>
      </ul>
    </div>
  </div>
</div>

utiliser l'interpolation pour commencer à dynamiser vos données. Modifiez appareil.component.html ainsi :

<li class="list-group-item">
  <h4>Appareil : {{ appareilName }}</h4>
</li>

Ici, vous trouvez la syntaxe pour l'interpolation : les doubles accolades {{ }} . Ce qui se trouve entre les doubles accolades correspond à l'expression TypeScript que nous voulons afficher, l'expression la plus simple étant une variable. D'ailleurs, puisque la variable appareilName n'existe pas encore, votre navigateur n'affiche rien à cet endroit pour l'instant. Ouvrez maintenant appareil.component.ts  :


Pour l'instant, les valeurs sont codées "en dur", mais à terme, ces valeurs peuvent être calculées par votre code ou récupérées sur un serveur, par exemple. Ajoutez maintenant une nouvelle variable dans votre AppareilComponent

export class AppareilComponent implements OnInit {
  appareilName: string = 'Machine à laver';
  constructor() { }

Pour l'instant, les valeurs sont codées "en dur", mais à terme, ces valeurs peuvent être calculées par votre code ou récupérées sur un serveur, par exemple. Ajoutez maintenant une nouvelle variable dans votre AppareilComponent

appareilStatus: string = 'éteint';

intégrez cette variable dans le template :

<h4>Appareil : {{ appareilName }} -- Statut : {{ appareilStatus }}</h4>

On peut utiliser toute expression TypeScript valable pour l'interpolation. Pour démontrer cela, ajouter une méthode au fichier AppareilComponent  :

 getStatus() {
    return this.appareilStatus;
 }

Alors que cette méthode ne fait que retourner la même valeur qu'avant, on peut imaginer une situation où elle ferait un appel API, par exemple, qui retournerait le statut de l'appareil électrique.

Modifiez maintenant le template pour prendre en compte ce changement :

 <h4>Appareil : {{ appareilName }} -- Statut : {{ getStatus() }}</h4>

Property binding

La liaison par propriété ou property binding est une autre façon de créer de la communication dynamique entre votre TypeScript et votre template : plutôt que d'afficher simplement le contenu d'une variable, vous pouvez modifier dynamiquement les propriétés d'un élément du DOM en fonction de données dans votre TypeScript.

Pour votre application des appareils électriques, imaginez que si l'utilisateur est authentifié, on lui laisse la possibilité d'allumer tous les appareils de la maison. Puisque l'authentification est une valeur globale, ajoutez une variable boolean dans AppComponent , votre component de base (vous pouvez supprimer la variable title puisque vous ne l'utilisez plus).

Ajoutez maintenant un bouton au template global app.component.html , en dessous de la liste d'appareils :

 <button class="btn btn-success" disabled>Tout allumer</button>

La propriété disabled permet de désactiver le bouton. Afin de lier cette propriété au TypeScript, il faut le mettre entre crochets [] et l'associer à la variable ainsi :

<button class="btn btn-success" [disabled]="!isAuth">Tout allume</button>

Le point d'exclamation fait que le bouton est désactivé lorsque isAuth === false.

Pour montrer que cette liaison est dynamique, créez une méthode constructor dans AppComponent , dans laquelle vous créerez une timeout qui associe la valeur true à isAuth après 4 secondes (pour simuler, par exemple, le temps d'un appel API) :

export class AppComponent {
  isAuth = false;

  constructor() {
    setTimeout(
      () => {
        this.isAuth = true;
      }, 4000
    );
  }
}
setTimeout 
https://www.w3schools.com/jsref/met_win_settimeout.asp

Pour en voir l'effet, rechargez la page dans votre navigateur et observez comment le bouton s'active au bout de quatre secondes.

Pour l'instant le bouton ne fait rien : vous découvrirez comment exécuter du code lorsque l'utilisateur cliquera dessus avec la liaison des événements, ou "event binding".

Event binding

Avec le string interpolation et le property binding, vous savez communiquer depuis votre code TypeScript vers le template HTML.

Comment réagir dans votre code TypeScript aux événements venant du template HTML.

Actuellement, vous avez un bouton sur votre template qui s'active au bout de 4 secondes. Vous allez maintenant lui ajouter une fonctionnalité liée à l'événement click (déclenché quand l'utilisateur clique dessus).

Ajoutez la liaison suivante à votre bouton dans le template HTML :

<div class="container">
  <div class="row">
    <div class="col-xs-12">
      <h2>Mes appareils</h2>
      <ul class="list-group">
        <app-appareil></app-appareil>
        <app-appareil></app-appareil>
        <app-appareil></app-appareil>
      </ul>
      <button class="btn btn-success" 
              [disabled]="!isAuth" 
              (click)="onAllumer()">Tout allumer</button>
    </div>
  </div>
</div>

Comme vous pouvez le constater, on utilise les parenthèses () pour créer une liaison à un événement. Pour l'instant, la méthode onAllumer() n'existe pas, donc je vous propose de la créer maintenant dans app.component.ts, en dessous du constructeur.

La méthode affichera simplement un message dans la console dans un premier temps :

app.component.ts
onAllumer() {
    console.log('On allume tout !');
}

Enregistrez le fichier, et ouvrez la console dans votre navigateur.

Lorsque le bouton s'active, cliquez dessus, et vous verrez votre message apparaître dans la console.

Cela montre comment lier une fonction TypeScript à un événement venant du template.

De manière générale, vous pouvez lier du code à n'importe quelle propriété ou événement des éléments du DOM. Pour plus d'informations, vous pouvez consulter le Mozilla Developer Network ou W3Schools, par exemple.

Two-way binding

La liaison à double sens (ou two-way binding) utilise la liaison par propriété et la liaison par événement en même temps ; on l'utilise, par exemple, pour les formulaires, afin de pouvoir déclarer et de récupérer le contenu des champs, entre autres.

Pour pouvoir utiliser le two-way binding, il vous faut importer FormsModule depuis @angular/forms dans votre application.

Vous pouvez accomplir cela en l'ajoutant à l'array imports de votre AppModule (sans oublier d'ajouter le statement import correspondant en haut du fichier) :


import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';


import { AppComponent } from './app.component';
import { MonPremierComponent } from './mon-premier/mon-premier.component';
import { AppareilComponent } from './appareil/appareil.component';
import { FormsModule } from '@angular/forms';


@NgModule({
  declarations: [
    AppComponent,
    MonPremierComponent,
    AppareilComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Le two-way binding emploie le mélange des syntaxes de property binding et d'event binding : des crochets et des parenthèses [()] .

Pour une première démonstration, ajoutez un <input> dans votre template appareil.component.html et liez-le à la variable appareilName en utilisant la directive ngModel  :

<li class="list-group-item">
    <h4>Appareil:{{appareilName}} -- Statut : {{ getStatus() }}</h4>
    <input type="text" class="form-control" [(ngModel)]="appareilName">
</li>

Dans la page web il y a un <input> par appareil.

Le nom de l'appareil y est déjà indiqué. Si on le modifie, le contenu du <h4> est simultanément modifié.

Chaque instance du component AppareilComponent est entièrement indépendante une fois créée : le fait d'en modifier une ne change rien aux autres.

Ce concept est très important, et il s'agit de l'une des plus grandes utilités d'Angular.

Propriétés personnalisées

Il est possible de créer des propriétés personnalisées dans un component afin de pouvoir lui transmettre des données depuis l'extérieur.

il serait intéressant de faire en sorte que chaque instance d' AppareilComponent ait un nom différent qu'on puisse régler depuis l'extérieur du code.

Pour ce faire, il faut utiliser le décorateur @Input() en remplaçant la déclaration de la variable appareilName  :

appareil.component.ts

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

@Component({
  selector: 'app-appareil',
  templateUrl: './appareil.component.html',
  styleUrls: ['./appareil.component.scss']
})
export class AppareilComponent implements OnInit {

  @Input() appareilName: string;
  
  appareilStatus: string = 'éteint';

  constructor() { }

  ngOnInit() {
  }

  getStatus() {
    return this.appareilStatus;
  }

}

Le décorateur @Input (), crée une propriété appareilName que l'on peut fixer depuis la balise <app-appareil>  : ~/Angular/mon-projet-angular/src/app/app.component.html

<div class="container">
  <div class="row">
    <div class="col-xs-12">
      <h2>Mes appareils</h2>
      <ul class="list-group">
        <app-appareil appareilName="Machine à laver"></app-appareil>
        <app-appareil appareilName="Frigo"></app-appareil>
        <app-appareil appareilName="Ordinateur"></app-appareil>
      </ul>
      <button class="btn btn-success"
              [disabled]="!isAuth"
              (click)="onAllumer()">Tout allumer</button>
    </div>
  </div>
</div>

C'est une première étape intéressante, mais ce serait encore plus dynamique de pouvoir passer des variables depuis AppComponent pour nommer les appareils (on peut imaginer une autre partie de l'application qui récupérerait ces noms depuis un serveur, par exemple).

Créez d'abord vos trois variables dans AppComponent  : app.component.ts

export class AppComponent {
  isAuth = false;
  
  appareilOne = 'Machine à laver';
  appareilTwo = 'Frigo';
  appareilThree = 'Ordinateur';

  constructor() {

Maintenant, utilisez les crochets [] pour lier le contenu de ces variables à la propriété du component :

	    <ul class="list-group">
		<app-appareil [appareilName]="appareilOne"></app-appareil>
		<app-appareil [appareilName]="appareilTwo"></app-appareil>
		<app-appareil [appareilName]="appareilThree"></app-appareil>
	    </ul>

Vous pouvez également créer une propriété pour régler l'état de l'appareil :

app.component.ts

 @Input() appareilStatus: string;

app.component.html

<ul class="list-group">
    <app-appareil [appareilName]="appareilOne" [appareilStatus]="'éteint'"></app-appareil>
    <app-appareil [appareilName]="appareilTwo" [appareilStatus]="'allumé'"></app-appareil>
    <app-appareil [appareilName]="appareilThree" [appareilStatus]="'éteint'"></app-appareil>
</ul>

Notez bien que si vous employez les crochets pour le property binding et que vous souhaitez y passer un string directement, il faut le mettre entre apostrophes, car entre les guillemets, il doit y avoir un statement de TypeScript valable. Si vous omettez les apostrophes, vous essayez d'y passer une variable nommée allumé ou éteint et l'application ne compilera pas.

Structurez le document avec des Directives

Les directives sont des instructions intégrées dans le DOM que vous utiliserez presque systématiquement quand vous créerez des applications Angular. Quand Angular lit votre template et rencontre une directive qu'il reconnait, il suit les instructions correspondantes. Vous pouvez créer vos propres directives, mais dans le cadre de ce cours, nous allons uniquement aborder certaines directives qui sont fournies avec Angular et qui sont extrêmement utiles.

Il existe deux types principaux de directive : les directives structurelles et les directives par attribut.

Les directives structurelles

Ce sont des directives qui, comme leur nom l'indique, modifient la structure du document. Dans ce chapitre, vous allez en découvrir deux (il en existe d'autres) : *ngIf , pour afficher des données de façon conditionnelle, et *ngFor , pour itérer des données dans un array, par exemple.

*ngIf

Un component auquel on ajoute la directive *ngIf="condition" ne s'affichera que si la condition est "truthy" (elle retourne la valeur true où la variable mentionnée est définie et non-nulle), comme un statement if classique.

Pour une démonstration simple, ajoutez une
rouge qui ne s'affichera que si l'appareil est éteint :

appareil.component.html

<div style="width:20px;height:20px;background-color:red;" 
       *ngIf="appareilStatus === 'éteint'"></div>

*ngFor

Lorsque l'on ajoute la directive *ngFor="let obj of myArray" à un component, Angular itérera l'array myArray et affichera un component par objet obj . Pour en comprendre l'utilisation, je vous propose de modifier la façon dont votre application génère des appareils électriques.

On peut imaginer que votre application récupère, depuis un serveur, un array contenant tous les appareils et leurs états. Pour l'instant, créez cet array directement dans AppComponent  :

app.component.ts

export class AppComponent {
  isAuth = false;

  appareils = [
    {
      name: 'Machine à laver',
      status: 'éteint'
    },
    {
      name: 'Frigo',
      status: 'allumé'
    },
    {
      name: 'Ordinateur',
      status: 'éteint'
    }
  ];

  constructor() {

Vous avez un array avec trois objets, chaque objet ayant une propriété name et une propriété status . Vous pourriez même créer une interface ou une class TypeScript Appareil , mais dans ce cas simple ce n'est pas nécessaire.

app.component.html

Maintenant la magie  *ngFor  :
	    <ul class="list-group">
		<app-appareil  *ngFor="let appareil of appareils"
			       [appareilName]="appareil.name"
			       [appareilStatus]="appareil.status"></app-appareil>
	    </ul>
	    

Le statement let appareil of appareils , comme dans une for loop classique, itère pour chaque élément appareil (nom arbitraire) de l'array appareils .

Après cette directive, vous pouvez maintenant utiliser l'objet appareil , dont vous connaissez la forme, à l'intérieur de cette balise HTML. Vous pouvez donc utiliser le property binding, et y passer les propriétés name et status de cet objet.

N'oubliez pas l'astérisque devant ces directives, qui signifie à Angular de les traiter comme directives structurelles !

Les directives par attribut

À la différence des directives structurelles, les directives par attribut modifient le comportement d'un objet déjà existant.

Vous avez déjà utilisé une directive de ce type sans le savoir : la directive ngModel que vous avez employée pour le two-way binding, qui modifie la valeur du <input> et répond à tout changement qu'on lui apporte. Je vais vous montrer deux autres exemples très utiles : ngStyle et ngClass , qui permettent d'attribuer des styles ou des classes de manière dynamique.

ngStyle

Cette directive permet d'appliquer des styles à un objet du DOM de manière dynamique. Imaginez que, pour l'application des appareils électriques, vous souhaitiez modifier la couleur du texte selon si l'appareil est allumé ou non, disons vert pour allumé, rouge pour éteint. ngStyle vous permet de faire cela :

<h4 [ngStyle]="{color: getColor()}">Appareil : {{ appareilName }} -- Statut : {{ getStatus() }}</h4>

ngStyle prend un objet JS de type clé-valeur, avec comme clé le style à modifier, et comme valeur la valeur souhaitée pour ce style.

Ici, vous faites appel à une fonction getColor() dans AppareilComponent que vous allez maintenant créer

appareil.component.ts

getColor() {
    if(this.appareilStatus === 'allumé') {
      return 'green';
    } else if(this.appareilStatus === 'éteint') {
      return 'red';
    }
}

Cette fonction retourne la valeur 'green' si l'appareil est allumé, et 'red' s'il est éteint, modifiant ainsi la couleur du texte dans le template.

ngClass

Au-delà de modifier des styles directement, il peut être très utile d'ajouter des classes CSS à un élément de manière dynamique. Comme ngStyle , ngClass prend un objet clé-valeur, mais cette fois avec la classe à appliquer en clé, et la condition en valeur.

Pour cet exemple, je vous propose d'appliquer des classes Bootstrap à la balise <li> en fonction du statut de l'appareil : appareil.component.html

<li [ngClass]="{'list-group-item': true,
                'list-group-item-success': appareilStatus === 'allumé',
                'list-group-item-danger': appareilStatus === 'éteint'}">
  <div style="width:20px;height:20px;background-color:red;"
       *ngIf="appareilStatus === 'éteint'"></div>
  <h4 [ngStyle]="{color: getColor()}">Appareil : {{ appareilName }} -- Statut : {{ getStatus() }}</h4>
  <input type="text" class="form-control" [(ngModel)]="appareilName">
</li>

Angular appliquera donc systématiquement la classe list-group-item , et selon le contenu de la variable appareilStatus , appliquera l'une ou l'autre des deux autres classes. Vous pouvez bien évidemment créer vos propres classes et les utiliser ; j'ai simplement choisi des classes Bootstrap pour simplifier l'explication.

Routing

Créez des routes

Il s'agit des instructions d'affichage à suivre pour chaque URL, c'est-à-dire quel(s) component(s) il faut afficher à quel(s) endroit(s) pour un URL donné.

Puisque le routing d'une application est fondamentale pour son fonctionnement, on déclare les routes dans app.module.ts .

Il est possible d'avoir un fichier séparé pour le routing, mais en termes de fonctionnalité, cela ne change rien : c'est juste une question d'organisation du code.

import { Routes } from '@angular/router';

const appRoutes: Routes = [
  { path: 'appareils', component: AppareilViewComponent },
  { path: 'auth', component: AuthComponent },
  { path: '', component: AppareilViewComponent }
];

Le path correspond au string qui viendra après le / dans l'URL : sur votre serveur local, le premier path ici correspond donc à localhost:4200/appareils .

Ensuite, le component correspond au component que l'on veut afficher lorsque l'utilisateur navigue au path choisi. J'ai ajouté un path vide, qui correspond tout simplement à localhost:4200 (ou à la racine de l'application seule), car si on ne traite pas le path vide, chaque refresh de l'application la fera planter.

Les routes sont maintenant créées, mais il faut les enregistrer dans votre application. Pour cela, vous allez importer RouterModule depuis @angular/router et vous allez l'ajouter à l'array imports de votre AppModule , tout en lui appelant la méthode forRoot() en lui passant l'array de routes que vous venez de créer :

dans app.module.ts:

imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot(appRoutes)
],

Maintenant que les routes sont enregistrées, il ne reste plus qu'à dire à Angular où vous souhaitez afficher les components dans le template lorsque l'utilisateur navigue vers la route en question. On utilise la balise <router-outlet>  :

Dans app.component.html

<div class="container">
  <div class="row">
    <div class="col-xs-12">
      <router-outlet></router-outlet>
    </div>
  </div>
</div>

Lorsque vous changez de route (pour l'instant, en modifiant l'URL directement dans la barre d'adresse du navigateur), la page n'est pas rechargée, mais le contenu sous la barre de navigation change.

Naviguez avec les routerLink

Afin que l'utilisateur puisse naviguer à l'intérieur de votre application, il est nécessaire de créer des liens ou des boutons qui naviguent vers les routes que vous avez créées. Dans le chapitre précédent, vous avez créé des liens typiques dans la barre de navigation, mais qui ne font rien pour l'instant.

Vous pourriez vous dire qu'il suffirait de marquer le path de vos routes directement dans l'attribut href , et techniquement, cela permet d'atteindre les routes que vous avez créées.

Alors pourquoi on ne fait pas comme ça ?

Tout simplement parce que, si vous regardez bien, en employant cette technique, la page est rechargée à chaque clic ! On perd totalement l'intérêt d'une Single Page App !

Du coup, on retire l'attribut href et on le remplace par l'attribut routerLink  : Ainsi, les routes sont chargées instantanément : on conserve l'ergonomie d'une SPA.

Pour finaliser cette étape, il serait intéressant que la classe active ne s'applique qu'au lien du component réellement actif. Heureusement, Angular fournit un attribut pour cela qui peut être ajouté au lien directement ou à son élément parent :

<ul class="nav navbar-nav">
    <li routerLinkActive="active"><a routerLink="auth">Authentification</a></li>
    <li routerLinkActive="active"><a routerLink="appareils">Appareils</a></li>
</ul>

Maintenant, les liens s'activent visuellement.

Naviguez avec le Router

Il peut y avoir des cas où vous aurez besoin d'exécuter du code avant une navigation.

Par exemple, on peut avoir besoin d'authentifier un utilisateur et, si l'authentification fonctionne, de naviguer vers la page que l'utilisateur souhaite voir.

Je vous propose d'intégrer cette fonctionnalité à l'application des appareils électriques (l'authentification elle-même restera simulée pour l'instant).

Tout d'abord, créez un nouveau fichier auth.service.ts dans le dossier services pour gérer l'authentification (n'oubliez pas de l'ajouter également dans l'array providers dans app.module.ts ) :

Dans service/auth.service.ts :

export class AuthService {

  isAuth = false;

  signIn() {
    return new Promise(
      (resolve, reject) => {
        setTimeout(
          () => {
            this.isAuth = true;
            resolve(true);
          }, 
          2000
        );
      }
    );
  }

  signOut() {
    this.isAuth = false;
  }
}

La variable isAuth donne l'état d'authentification de l'utilisateur.

  • La méthode signOut() "déconnecte" l'utilisateur,
  • et la méthode signIn() authentifie automatiquement l'utilisateur au bout de 2 secondes, simulant le délai de communication avec un serveur.

Dans le component AuthComponent , vous allez simplement créer deux boutons et les méthodes correspondantes pour se connecter et se déconnecter (qui s'afficheront de manière contextuelle : le bouton "se connecter" ne s'affichera que si l'utilisateur est déconnecté et vice versa) :

auth.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../services/auth.service';

@Component({
  selector: 'app-auth',
  templateUrl: './auth.component.html',
  styleUrls: ['./auth.component.scss']
})
export class AuthComponent implements OnInit {

  authStatus: boolean;

  constructor(private authService: AuthService) { }

  ngOnInit() {
    this.authStatus = this.authService.isAuth;
  }

  onSignIn() {
    this.authService.signIn().then(
      () => {
        console.log('Sign in successful!');
        this.authStatus = this.authService.isAuth;
      }
    );
  }

  onSignOut() {
    this.authService.signOut();
    this.authStatus = this.authService.isAuth;
  }
}

Puisque la méthode signIn() du service retourne une Promise, on peut employer une fonction callback asynchrone avec .then() pour exécuter du code une fois la Promise résolue.

Ajoutez simplement les boutons, et tout sera prêt pour intégrer la navigation :

auth.component.html
<h2>Authentification</h2>
<button class="btn btn-success" *ngIf="!authStatus" (click)="onSignIn()">Se connecter</button>
<button class="btn btn-danger" *ngIf="authStatus" (click)="onSignOut()">Se déconnecter</button>

Le comportement recherché serait qu'une fois l'utilisateur authentifié, l'application navigue automatiquement vers la view des appareils.

Pour cela, il faut injecter le Router (importé depuis @angular/router ) pour accéder à la méthode navigate()  :

auth.component.ts
constructor(private authService: AuthService, private router: Router) { }

Dans auth.component.ts :

onSignIn() {
    this.authService.signIn().then(
      () => {
        console.log('Sign in successful!');
        this.authStatus = this.authService.isAuth;
        this.router.navigate(['appareils']);
      }
    );
}

La fonction navigate prend comme argument un array d'éléments (ce qui permet de créer des chemins à partir de variables, par exemple) qui, dans ce cas, n'a qu'un seul membre : le path souhaité.

Le chemin appareils est toujours accessible actuellement, même sans authentification : dans un chapitre ultérieur, vous apprendrez à le sécuriser totalement. Avant cela, vous allez apprendre à ajouter des paramètres à vos routes.

Paramètres de routes

Imaginez qu'on souhaite pouvoir cliquer sur un appareil dans la liste d'appareils afin d'afficher une page avec plus d'informations sur cet appareil : on peut imaginer un système de routing de type appareils/nom-de-l'appareil , par exemple.

Si on n'avait que deux ou trois appareils, on pourrait être tenté de créer une route par appareil, mais imaginez un cas de figure où l'on aurait 30 appareils, ou 300.

Imaginez qu'on laisse l'utilisateur créer de nouveaux appareils ; l'approche de créer une route par appareil n'est pas adaptée. Dans ce genre de cas, on choisira plutôt de créer une route avec paramètre.

Tout d'abord, vous allez créer la route dans AppModule  :

Dans app.modules.ts

const appRoutes: Routes = [
  { path: 'appareils', component: AppareilViewComponent },
  { path: 'appareils/:id', component: SingleAppareilComponent },
  { path: 'auth', component: AuthComponent },
  { path: '', component: AppareilViewComponent }
];

L'utilisation des deux-points  : avant un fragment de route déclare ce fragment comme étant un paramètre : tous les chemins de type appareils/* seront renvoyés vers SingleAppareilComponent , que vous allez maintenant créer :

Dans single-appareil.component.ts

import { Component, OnInit } from '@angular/core';
import { AppareilService } from '../services/appareil.service';

@Component({
  selector: 'app-single-appareil',
  templateUrl: './single-appareil.component.html',
  styleUrls: ['./single-appareil.component.scss']
})
export class SingleAppareilComponent implements OnInit {

  name: string = 'Appareil';
  status: string = 'Statut';

  constructor(private appareilService: AppareilService) { }

  ngOnInit() {
  }

}

Dans single-appareil.component.html

<h2>{{ name }}</h2>
<p>Statut : {{ status }}</p>
<a routerLink="/appareils">Retour à la liste</a>

Pour l'instant, si vous naviguez vers /appareils/nom , peu importe le nom que vous choisissez, vous avez accès à SingleAppareilComponent. Maintenant, vous allez y injecter ActivatedRoute , importé depuis @angular/router , afin de récupérer le fragment id de l'URL :

Dans single-appareil.component.ts

constructor(private appareilService: AppareilService,
            private route: ActivatedRoute) { }

Puis, dans ngOnInit() , vous allez utiliser l'objet snapshot qui contient les paramètres de l'URL et, pour l'instant, attribuer le paramètre id à la variable name  :

Dans single-appareil.component.ts

ngOnInit() {
    this.name = this.route.snapshot.params['id'];
}

Ainsi, le fragment que vous tapez dans la barre d'adresse après appareils/ s'affichera dans le template, mais ce n'est pas le comportement recherché.

Pour atteindre l'objectif souhaité, commencez par ajouter, dans AppareilService , un identifiant unique pour chaque appareil et une méthode qui rendra l'appareil correspondant à un identifiant :

Dans services/appareil.service.ts:

appareils = [
    {
      id: 1,
      name: 'Machine à laver',
      status: 'éteint'
    },
    {
      id: 2,
      name: 'Frigo',
      status: 'allumé'
    },
    {
      id: 3,
      name: 'Ordinateur',
      status: 'éteint'
    }
];

Et en utilisant find sur le tableau appareils :

getAppareilById(id: number) {
    const appareil = this.appareils.find(
      (s) => {
        return s.id === id;
      }
    );
    return appareil;
}

Maintenant, dans SingleAppareilComponent , vous allez récupérer l'identifiant de l'URL et l'utiliser pour récupérer l'appareil correspondant : Dans single-appareil.component.ts

ngOnInit() {
    const id = this.route.snapshot.params['id'];
    this.name = this.appareilService.getAppareilById(+id).name;
    this.status = this.appareilService.getAppareilById(+id).status;
}

Puisqu'un fragment d'URL est forcément de type string , et que la méthode getAppareilById() prend un number comme argument, il ne faut pas oublier d'utiliser + avant id dans l'appel pour caster la variable comme number.

Vous pouvez naviguer manuellement vers /appareils/2 , par exemple, mais cela recharge encore la page, et vous perdez l'état des appareils (si vous en allumez ou éteignez par exemple).

Pour finaliser cette fonctionnalité, intégrez l'identifiant unique dans AppareilComponent et dans AppareilViewComponent , puis créez un routerLink pour chaque appareil qui permet d'en regarder le détail :

Dans appareil/appareil.component.ts

@Input() appareilName: string;
@Input() appareilStatus: string;
@Input() index: number;
@Input() id: number;

Dans appareil-view/appareil-view.component.html

<ul class="list-group">
  <app-appareil  *ngFor="let appareil of appareils; let i = index"
                 [appareilName]="appareil.name"
                 [appareilStatus]="appareil.status"
                 [index]="i" 
                 [id]="appareil.id"></app-appareil>
</ul>

Dans single-appareil.component.html

<h4 [ngStyle]="{color: getColor()}">Appareil : {{ appareilName }} -- Statut : {{ getStatus() }}</h4>
<a [routerLink]="[id]">Détail</a>

Ici, vous utilisez le format array pour routerLink en property binding afin d'accéder à la variable id .

Ça y est ! Vous pouvez maintenant accéder à la page Détail pour chaque appareil, et les informations de statut qui s'y trouvent sont automatiquement à jour grâce à l'utilisation du service.

Redirection

Il peut y avoir des cas de figure où l'on souhaiterait rediriger un utilisateur, par exemple pour afficher une page 404 lorsqu'il entre une URL qui n'existe pas.

Pour l'application des appareils électriques, commencez par créer un component 404 très simple, appelé four-oh-four.component.ts  :

Dans four-oh-four.component.html

<h2>Erreur 404</h2>
<p>La page que vous cherchez n'existe pas !</p>

Ensuite, vous allez ajouter la route "directe" vers cette page, ainsi qu'une route "wildcard", qui redirigera toute route inconnue vers la page d'erreur :

Dans app.module.ts :

const appRoutes: Routes = [
  { path: 'appareils', component: AppareilViewComponent },
  { path: 'appareils/:id', component: SingleAppareilComponent },
  { path: 'auth', component: AuthComponent },
  { path: '', component: AppareilViewComponent },
  { path: 'not-found', component: FourOhFourComponent },
  { path: '**', redirectTo: 'not-found' }
];

Ainsi, quand vous entrez un chemin dans la barre de navigation qui n'est pas directement pris en charge par votre application, vous êtes redirigé vers /not-found et donc le component 404.

Guards

Il peut y avoir des cas de figure où vous souhaiterez exécuter du code avant qu'un utilisateur puisse accéder à une route ; par exemple, vous pouvez souhaiter vérifier son authentification ou son identité. Techniquement, ce serait possible en insérant du code dans la méthode ngOnInit() de chaque component, mais cela deviendrait lourd, avec du code répété et une multiplication des erreurs potentielles. Ce serait donc mieux d'avoir une façon de centraliser ce genre de fonctionnalité. Pour cela, il existe la guard canActivate .

Une guard est en effet un service qu'Angular exécutera au moment où l'utilisateur essaye de naviguer vers la route sélectionnée.

Ce service implémente l'interface canActivate , et donc doit contenir une méthode du même nom qui prend les arguments ActivatedRouteSnapshot et RouterStateSnapshot (qui lui seront fournis par Angular au moment de l'exécution) et retourne une valeur booléenne, soit de manière synchrone (boolean), soit de manière asynchrone (sous forme de Promise ou d'Observable) :

Dans auth-guard.service.ts :

import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';

export class AuthGuard implements CanActivate {
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    
  }
}

Ensuite, il faut injecter le service AuthService dans ce nouveau service. Pour injecter un service dans un autre service, il faut que le service dans lequel on injecte un autre ait le décorateur @Injectable , à importer depuis @angular/core  :


import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { AuthService } from './auth.service';
import { Injectable } from '@angular/core';

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private authService: AuthService) { }

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {

  }
}

À l'intérieur de la méthode canActivate() , vous allez vérifier l'état de l'authentification dans AuthService . Si l'utilisateur est authentifié, la méthode renverra true , permettant l'accès à la route protégée. Sinon, vous pourriez retourner false , mais cela empêchera simplement l'accès sans autre fonctionnalité. Il serait intéressant de rediriger l'utilisateur vers la page d'authentification, le poussant à s'identifier :

import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { AuthService } from './auth.service';
import { Injectable } from '@angular/core';

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private authService: AuthService,
              private router: Router) { }

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    if(this.authService.isAuth) {
      return true;
    } else {
      this.router.navigate(['/auth']);
    }
  }
}

Pour appliquer cette garde à la route /appareils et à toutes ses routes enfants, il faut l'ajouter dans AppModule . N'oubliez pas d'ajouter AuthGuard à l'array providers , puisqu'il s'agit d'un service :

const appRoutes: Routes = [
  { path: 'appareils', canActivate: [AuthGuard], component: AppareilViewComponent },
  { path: 'appareils/:id', canActivate: [AuthGuard], component: SingleAppareilComponent },
  { path: 'auth', component: AuthComponent },
  { path: '', component: AppareilViewComponent },
  { path: 'not-found', component: FourOhFourComponent },
  { path: '**', redirectTo: 'not-found' }
];

Maintenant, si vous essayez d'accéder à /appareils sans être authentifié, vous êtes automatiquement redirigé vers /auth . Si vous cliquez sur "Se connecter", vous pouvez accéder à la liste d'appareils sans problème.

La route /appareils étant protégée, vous pouvez retirer les liaisons disabled des boutons "Tout allumer" et "Tout éteindre", car vous pouvez être certain que tout utilisateur accédant à cette route sera authentifié.

Dans ce chapitre, vous avez appris à gérer le routing de votre application avec des routerLink et de manière programmatique (avec router.navigate() ). Vous avez également vu comment rediriger un utilisateur, et comment protéger des routes de votre application avec les guards.

Écoutez l'utilisateur avec les Forms - méthode template

Jusqu'ici une application dynamique permettait d'échanger des informations entre components à l'aide des services et des Observables, qui change l'affichage selon le routing et qui réagit à certains événements provenant de l'utilisateur, comme des clics sur un bouton, par exemple.

Coutes les données ont été codées "en dur" dans l'application, c'est-à-dire fournies ni par l'utilisateur, ni par un serveur.

Dans les chapitres suivants, vous allez apprendre à interagir avec l'utilisateur et avec les serveurs afin de créer une application totalement dynamique.

En Angular, il y a deux grandes méthodes pour créer des formulaires :

  • la méthode template : vous créez votre formulaire dans le template, et Angular l'analyse pour comprendre les différents inputs et pour en mettre à disposition le contenu ;
  • la méthode réactive : vous créez votre formulaire en TypeScript et dans le template, puis vous en faites la liaison manuellement — cette approche est plus complexe, mais elle permet beaucoup plus de contrôle et une approche dynamique.

N'oubliez pas d'importer FormsModule dans AppModule si ce n'est pas déjà fait !

Dans la suite de ce chapitre nous allons découvrir la méthode template.

Créez le formulaire

Pour comprendre et pratiquer cette méthode, vous allez créer un nouveau component EditAppareilComponent qui permettra à l'utilisateur d'enregistrer un nouvel appareil électrique :

Dans edit-appareil.component.html

<div class="row">
  <div class="col-sm-8 col-sm-offset-2">
    <form>
      <div class="form-group">
        <label for="name">
          Nom de l'appareil
        </label>
        <input type="text" id="name" class="form-control">
      </div>
      <div class="form-group">
        <label for="status">
          État de l'appareil
        </label>
        <select id="status" class="form-control">
          <option value="allumé">Allumé</option>
          <option value="éteint">Éteint</option>
        </select>
      </div>
      <button class="btn btn-primary">Enregistrer</button>
    </form>
  </div>
</div>

Angular parcourt votre template et trouve la balise <form> , créant ainsi un objet qui sera utilisable depuis votre code TypeScript.

Avant de passer à la soumission du formulaire, il faut signaler à Angular quels inputs correspondront à des controls , c'est-à-dire des champs dont le contenu est à soumettre.

Pour cela, il suffit d'ajouter deux attributs aux inputs en question : un attribut name, qui correspondra à la clef de la paire clef-valeur qui sera rendu, et l'attribut ngModel , sans parenthèses ni crochets. Ce deuxième attribut signale à Angular que vous souhaitez enregistrer ce contrôle :

Dans edit-appareil.component.html :

<div class="form-group">
    <label for="name">
      Nom de l'appareil
    </label>
    <input type="text" id="name" class="form-control" name="name" ngModel>
</div>
<div class="form-group">
    <label for="status">
      État de l'appareil
    </label>
    <select id="status" class="form-control" name="status" ngModel>
      <option value="allumé">Allumé</option>
      <option value="éteint">Éteint</option>
    </select>
</div>

Il faut maintenant préparer la gestion de la soumission de ce formulaire. Dans votre template, au lieu d'ajouter un événement sur le bouton, vous allez déclarer le bouton de type submit , et ajouter le code suivant dans la balise <form>  :

Dans edit-appareil.component.html :

<form (ngSubmit)="onSubmit(f)" #f="ngForm">
<button class="btn btn-primary" type="submit">Enregistrer</button>

Déclarer le bouton de type submit à l'intérieur du <form> déclenche le comportement de soumission classique de HTML.

En ajoutant l'attribut (ngSubmit) , vous recevez cette soumission et exécutez la méthode onSubmit() (que vous n'avez pas encore créée).

L'attribut #f est ce qu'on appelle une référence locale.

Vous donnez simplement un nom à l'objet sur lequel vous ajoutez cet attribut ; ce nom sera ensuite utilisable par Angular. C'est d'ailleurs le cas ici : on appelle le formulaire f pour ensuite le passer comme argument à la méthode de soumission.

De manière générale, on ne donne pas de valeur à une référence locale : on écrit simplement #f ou #my-name . Dans ce cas précis d'un formulaire en méthode template, on y attribue la valeur ngForm pour avoir accès à l'objet créé par Angular.

Pour récapituler : quand l'utilisateur clique sur le bouton de type submit, la méthode que vous attribuez à (ngSubmit) est exécutée, et grâce à la référence locale #f="ngForm" , vous pouvez passer l'objet à la méthode (et donc la récupérer dans votre code TypeScript).

Créez maintenant la méthode onSubmit() afin de recevoir les informations venant du formulaire. Pour l'instant, vous allez simplement les afficher dans la console :

onSubmit(form: NgForm) {
  console.log(form.value);
}

Ici vous utilisez la propriété value du NgForm . L'objet NgForm (à importer depuis @angular/forms ) comporte beaucoup de propriétés très intéressantes ; je n'en expliquerai que certaines dans ce cours, mais vous pouvez en trouver la liste complète dans la documentation officielle.

Pour avoir accès au formulaire, créez une nouvelle route dans AppModule et un routerLink correspondant dans la barre de menu :

Dans app.module.ts

const appRoutes: Routes = [
  { path: 'appareils', canActivate: [AuthGuard], component: AppareilViewComponent },
  { path: 'appareils/:id', canActivate: [AuthGuard], component: SingleAppareilComponent },
  { path: 'edit', canActivate: [AuthGuard], component: EditAppareilComponent },
  { path: 'auth', component: AuthComponent },
  { path: '', component: AppareilViewComponent },
  { path: 'not-found', component: FourOhFourComponent },
  { path: '**', redirectTo: 'not-found' }
];

Dans app.component.html

<ul class="nav navbar-nav">
    <li routerLinkActive="active"><a routerLink="auth">Authentification</a></li>
    <li routerLinkActive="active"><a routerLink="appareils">Appareils</a></li>
    <li routerLinkActive="active"><a routerLink="edit">Nouvel appareil</a></li>
</ul>

Naviguez vers cette nouvelle page. Si vous tapez "Télévision" comme nom et choisissez "Éteint" et vous cliquez sur "Enregistrer", dans la console vous avez : ...

Ça y est, vous avez accès aux informations du formulaire !

Avant de les traiter et de créer un nouvel appareil, vous allez d'abord ajouter une validation du formulaire, afin que l'utilisateur ne puisse pas le soumettre sans rentrer des données valables.

Validez les données

Pour le formulaire de cette application, on peut dire que le nom de l'appareil est un champ obligatoire. Pour ajouter cette obligation, ajoutez simplement la directive suivante :

<input type="text" id="name" class="form-control" name="name" ngModel required>

Cet attribut ressemble à un attribut HTML5 classique, mais sachez qu'Angular désactive par défaut le comportement de validation HTML5.

Le champ est obligatoire, mais le formulaire peut encore être soumis, car vous n'avez pas encore intégré l'état de validation du formulaire. Pour accomplir cela, vous allez lier la propriété disabled du bouton à la propriété invalid du formulaire, qui est mise à disposition par Angular :

Dans edit-appareil.component.html

<button class="btn btn-primary" type="submit" [disabled]="f.invalid">Enregistrer</button>

Vous employez la référence locale f pour avoir accès à l'objet NgForm , et donc à sa propriété invalid , désactivant le bouton si elle retourne true .

Il serait intéressant de déclarer l'appareil éteint par défaut afin de ne pas laisser ce champ vide.

Pour déclarer une réponse par défaut, vous allez créer une variable TypeScript et la lier à la propriété ngModel du contrôle :

Dans edit-appareil.component.ts :

export class EditAppareilComponent implements OnInit {

  defaultOnOff = 'éteint';

  constructor() { }

Dans edit-appareil.component.ts :

<select id="status" class="form-control" name="status" [ngModel]="defaultOnOff">
      <option value="allumé">Allumé</option>
      <option value="éteint">Éteint</option>
</select>

Ainsi, le formulaire ne pourra être soumis que si le champ "Nom de l'appareil" n'est pas vide, et l'option choisie par défaut empêche le deuxième champ d'être vide également.

Il ne reste plus qu'à exploiter ces données et en créer un nouvel appareil !

Exploitez les données

Dans la méthode onSubmit() , vous allez récupérer les données et les attribuer à deux constantes pour les envoyer à AppareilService  :

Dans edit-appareil.component.ts :

onSubmit(form: NgForm) {
    const name = form.value['name'];
    const status = form.value['status'];
}

Puisque vous savez que le formulaire comportera forcément un champ name et un champ status , vous savez que vous pouvez utiliser cette syntaxe sans problème.

Créez maintenant la méthode dans AppareilService qui générera le nouvel appareil :

Dans appareil.service.ts :

addAppareil(name: string, status: string) {
    const appareilObject = {
      id: 0,
      name: '',
      status: ''
    };
    appareilObject.name = name;
    appareilObject.status = status;
    appareilObject.id = this.appareils[(this.appareils.length - 1)].id + 1;
    this.appareils.push(appareilObject);
    this.emitAppareilSubject();
}

Cette méthode crée un objet du bon format, et attribue le nom et le statut qui lui sont passés comme arguments.

La ligne pour l'id prend l'id du dernier élément actuel de l'array et ajoute 1.

Ensuite, l'objet complété est ajouté à l'array et le Subject est déclenché pour tout garder à jour.

Il ne reste plus qu'à intégrer cette fonction dans EditAppareilComponent .

Il serait intéressant qu'une fois l'appareil créé, l'utilisateur soit redirigé vers la liste des appareils. N'oubliez pas d'importer et d'injecter le router pour cela, ainsi qu' AppareilService  :

Dans edit-appareil.component.ts :

export class EditAppareilComponent implements OnInit {

  defaultOnOff = 'éteint';

  constructor(private appareilService: AppareilService,
              private router: Router) { }

  ngOnInit() {
  }

  onSubmit(form: NgForm) {
    const name = form.value['name'];
    const status = form.value['status'];
    this.appareilService.addAppareil(name, status);
    this.router.navigate(['/appareils']);
  }

}

Et voilà ! Maintenant, grâce à un formulaire simple géré par la méthode template, l'utilisateur peut créer un nouvel appareil pour la liste.

Écoutez l'utilisateur avec les Forms - méthode réactive

Préparez le terrain

À la différence de la méthode template où Angular crée l'objet du formulaire, pour la méthode réactive, vous devez le créer vous-même et le relier à votre template. Même si cela a l'air plus complexe, cela vous permet de gérer votre formulaire en détail, notamment avec la création programmatique de contrôles (permettant, par exemple, à l'utilisateur d'ajouter des champs).

Pour illustrer la méthode réactive, vous allez créer une nouvelle section dans l'application des appareils électriques : vous allez permettre aux utilisateurs de créer un profil utilisateur simple. Cette démonstration utilisera des compétences que vous avez apprises tout au long de ce cours, et vous allez également créer votre premier modèle de données sous forme d'une classe TypeScript.

Commencez par le modèle User  ; créez un nouveau dossier models , et dedans un fichier User.model.ts  :

Dans models/User.model.ts :

export class User {
    constructor(
	public firstName: string,
	public lastName: string,
	public email: string,
	public drinkPreference: string,
	public hobbies?: string[]
    ) {}
}
Ce modèle pourra donc être utilisé dans le reste de l'application en l'important dans les components où vous en avez besoin.

Cette syntaxe de constructeur permet l'utilisation du mot-clé new , et les arguments passés seront attribués à des variables qui portent les noms choisis ici, par exemple

const user = new User('James', 'Smith', 'james@james.com', 'jus d\'orange', ['football', 'lecture'])

créera un nouvel objet User avec ces valeurs attribuées aux variables user.firstName , user.lastName etc.

Ensuite, créez un UserService simple qui stockera la liste des objets User et qui comportera une méthode permettant d'ajouter un utilisateur à la liste :

Dans services/user.service.ts :

 
import { User } from '../models/User.model';
import { Subject } from 'rxjs/Subject';

export class UserService {
    private users: User[] = [
	{
	    firstName: 'William',
	    lastName: 'Jones',
	    email: 'william.jones@gmx.com',
	    drinkPreference: 'jus d\'orange',
	    hobbies : ['coder', 'boire du café']
	}
    ];
    userSubject = new Subject<User[]>();

    emitUsers() {
	this.userSubject.next(this.users.slice());
    }

    addUser(user: User) {
	this.users.push(user);
	this.emitUsers();
    }
}

Ce service contient un array privé d'objets de type personnalisé User et un Subject pour émettre cet array. La méthode emitUsers() déclenche ce Subject et la méthode addUser() ajoute un objet User à l'array, puis déclenche le Subject.

L'étape suivante est de créer UserListComponent — pour simplifier cet exemple, vous n'allez pas créer un component pour les utilisateurs individuels :

Dans user-list/user-list.component.ts :

import { Component, OnDestroy, OnInit } from '@angular/core';
import { User } from '../models/User.model';
import { Subscription } from 'rxjs/Subscription';
import { UserService } from '../services/user.service';

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  styleUrls: ['./user-list.component.scss']
})
export class UserListComponent implements OnInit, OnDestroy {

  users: User[];
  userSubscription: Subscription;

  constructor(private userService: UserService) { }

  ngOnInit() {
    this.userSubscription = this.userService.userSubject.subscribe(
      (users: User[]) => {
        this.users = users;
      }
    );
    this.userService.emitUsers();
  }

  ngOnDestroy() {
    this.userSubscription.unsubscribe();
  }

}

Ce component très simple souscrit au Subject dans UserService et le déclenche pour en récupérer les informations et les rendre disponibles au template (que vous allez maintenant créer) :

Dans user-list/user-list.component.html:

<ul class="list-group">
    <li class="list-group-item" *ngFor="let user of users">
	<h3>{{ user.firstName }} {{ user.lastName }}</h3>
	<p>son email est {{ user.email }}</p>
	<p>Cette persone préfère le {{ user.drinkPreference }}</p>
	<p *ngIf="user.hobbies && user.hobbies.length > 0">
	    Cette personne a des hobbies :
	    <span *ngFor="let hobby of user.hobbies">{{ hobby }} - </span>
	</p>
    </li>
    <a routerLink="/new-user">Nouvel utilisateur</a>
</ul>

Ici, vous appliquez des directives *ngFor et *ngIf pour afficher la liste des utilisateurs et leurs hobbies, s'ils en ont.

Afin de pouvoir visualiser ce nouveau component, ajoutez une route users dans AppModule , et créez un routerLink . Ajoutez également un objet User codé en dur dans le service pour voir les résultats :

Dans app.modules.ts

const appRoutes: Routes = [
    { path: 'axiomes', canActivate: [AuthGuard], component: AxiomeViewComponent },
    { path: 'axiomes/:id', canActivate: [AuthGuard], component: SingleAxiomeComponent },
    { path: 'edit', canActivate: [AuthGuard], component: EditAxiomeComponent },
    { path: 'auth', component: AuthComponent },
    { path: 'users', component: UserListComponent },
    { path: 'new-user', component: NewUserComponent },
    { path: '', component: AxiomeViewComponent },
    { path: 'not-found', component: FourOhFourComponent },
    { path: '**', redirectTo: 'not-found' }
];

Dans app.component.html :

<li routerLinkActive="active"><a routerLink="users">Utilisateurs</a></li>

Dans services/user.service.ts :

private users: User[] = [
	{
	    firstName: 'William',
	    lastName: 'Jones',
	    email: 'william.jones@gmx.com',
	    drinkPreference: 'jus d\'orange',
	    hobbies : ['coder', 'boire du café']
	}
    ];

Dernière étape : il faut ajouter ReactiveFormsModule , importé depuis @angular/forms , à l'array imports de votre AppModule  :

Dans app.module.ts :

    imports: [
	BrowserModule,
	FormsModule,
	ReactiveFormsModule,
	RouterModule.forRoot(appRoutes)
    ],

Maintenant que tout est prêt, vous allez créer NewUserComponent qui contiendra votre formulaire réactif.

Construisez un formulaire avec FormBuilder

Dans la méthode template, l'objet formulaire mis à disposition par Angular était de type NgForm , mais ce n'est pas le cas pour les formulaires réactifs.

Un formulaire réactif est de type FormGroup , et il regroupe plusieurs FormControl (tous les deux importés depuis @angular/forms ).

Vous commencez d'abord, donc, par créer l'objet dans votre nouveau component NewUserComponent  :

Dans new-user.component.ts

import { Component, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
  selector: 'app-new-user',
  templateUrl: './new-user.component.html',
  styleUrls: ['./new-user.component.scss']
})
export class NewUserComponent implements OnInit {

  userForm: FormGroup;

  constructor() { }

  ngOnInit() {
  }

}

Ensuite, vous allez créer une méthode qui sera appelée dans le constructeur pour la population de cet objet, et vous allez également injecter FormBuilder , importé depuis @angular/forms  :

Dans new-user.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-new-user',
  templateUrl: './new-user.component.html',
  styleUrls: ['./new-user.component.scss']
})
export class NewUserComponent implements OnInit {

  userForm: FormGroup;

  constructor(private formBuilder: FormBuilder) { }

  ngOnInit() {
    this.initForm();
  }

  initForm() {
    
  }

}

FormBuilder est une classe qui vous met à disposition des méthodes facilitant la création d'objet FormGroup .

Vous allez maintenant utiliser la méthode group à l'intérieur de votre méthode initForm() pour commencer à créer le formulaire :

Dans new-user.component.ts

initForm() {
	this.userForm = this.formBuilder.group({
	    firstName: ['', Validators.required],
	    lastName: ['', Validators.required],
	    email: ['', [Validators.required, Validators.email]],
	    drinkPreference: ['', Validators.required]
	});
    }

La méthode group prend comme argument un objet où les clés correspondent aux noms des contrôles souhaités et les valeurs correspondent aux valeurs par défaut de ces champs.

Puisque l'objectif est d'avoir des champs vides au départ, chaque valeur ici correspond au string vide.

Les contrôles correspondants aux hobbies seront ajoutés par la suite avec une autre méthode.

Il faut maintenant créer le template du formulaire et lier ce template à l'objet userForm que vous venez de créer :

Dans new-user/new-user.component.html :

<div class="col-sm-8 col-sm-offset-2">
    <form [formGroup]="userForm" (ngSubmit)="onSubmitForm()">
	<div class="form-group">
	    <label for="firstName">Prénom</label>
	    <input type="text" id="firstName" class="form-control" formControlName="firstName">
	</div>
	<div class="form-group">
	    <label for="lastName">Nom</label>
	    <input type="text" id="lastName" class="form-control" formControlName="lastName">
	</div>
	<div class="form-group">
	    <label for="email">Adresse e-mail</label>
	    <input type="text" id="email" class="form-control" formControlName="email">
	</div>
	<div class="form-group">
	    <label for="drinkPreference">Quelle boisson préférez-vous ?</label>
	    <select id="drinkPreference" class="form-control" formControlName="drinkPreference">
		<option value="jus d\'orange">Jus d'orange</option>
		<option value="jus de mangue">Jus de mangue</option>
	    </select>
	</div>
	<button type="submit" class="btn btn-primary" [disabled]="userForm.invalid">Soumettre</button>
    </form>
</div>

Analysez le template :

  • Sur la balise <form> , vous utilisez le property binding pour lier l'objet userForm à l'attribut formGroup du formulaire, créant la liaison pour Angular entre le template et le TypeScript.
  • Également dans la balise <form> , vous avez toujours une méthode onSubmitForm() liée à ngSubmit, mais vous n'avez plus besoin de passer le formulaire comme argument puisque vous y avez déjà accès par l'objet userForm que vous avez créé.
  • Sur chaque <input> qui correspond à un control du formulaire, vous ajoutez l'attribut formControlName où vous passez un string correspondant au nom du control dans l'objet TypeScript.
  • Le bouton de type submit déclenche l'événement ngSubmit , déclenchant ainsi la méthode onSubmitForm() , que vous allez créer dans votre TypeScript.

Pour tout mettre ensemble, injectez UserService et Router (sans oublier de les importer) dans le constructeur du component, et créez la méthode onSubmitForm()  :

Dans new-user/new-user.component.ts :

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UserService } from '../services/user.service';
import { Router } from '@angular/router';
import { User } from '../models/User.model';

@Component({
    selector: 'app-new-user',
    templateUrl: './new-user.component.html',
    styleUrls: ['./new-user.component.scss']
})

export class NewUserComponent implements OnInit {

    userForm: FormGroup;

    constructor(private formBuilder: FormBuilder,
		private userService: UserService,
		private router: Router) { }

    ngOnInit() {
	this.initForm();
    }

    initForm() {
	this.userForm = this.formBuilder.group({
	    firstName: ['', Validators.required],
	    lastName: ['', Validators.required],
	    email: ['', [Validators.required, Validators.email]],
	    drinkPreference: ['', Validators.required]
	});
    }

    onSubmitForm() {
	const formValue = this.userForm.value;
	const newUser = new User(
	    formValue['firstName'],
	    formValue['lastName'],
	    formValue['email'],
	    formValue['drinkPreference']
	);
	this.userService.addUser(newUser);
	this.router.navigate(['/users']);
    }

}

La méthode onSubmitForm() récupère la value du formulaire, et crée un nouvel objet User (à importer en haut) à partir de la valeur des controls du formulaire. Ensuite, elle ajoute le nouvel utilisateur au service et navigue vers /users pour en montrer le résultat.

Il ne reste plus qu'à ajouter un lien dans UserListComponent qui permet d'accéder à NewUserComponent et de créer la route correspondante new-user dans AppModule  :

Dans user-list/user-list.component.html :

<ul class="list-group">
    <li class="list-group-item" *ngFor="let user of users">
	<h3>{{ user.firstName }} {{ user.lastName }}</h3>
	<p>son email est {{ user.email }}</p>
	<p>Cette persone préfère le {{ user.drinkPreference }}</p>
	<p *ngIf="user.hobbies && user.hobbies.length > 0">
	    Cette personne a des hobbies :
	    <span *ngFor="let hobby of user.hobbies">{{ hobby }} - </span>
	</p>
    </li>
    <a routerLink="/new-user">Nouvel utilisateur</a>
</ul>

Puisque ce routerLink se trouve à l'intérieur du router-outlet , il faut ajouter un / au début de l'URL pour naviguer vers localhost:4200/new-user . Si vous ne mettez pas le / , ce lien naviguera vers localhost:4200/users/new-user

{ path: 'users', component: UserListComponent },
{ path: 'new-user', component: NewUserComponent },

Validators

Comme pour la méthode template, il existe un outil pour la validation de données dans la méthode réactive : les Validators .

Pour ajouter la validation, vous allez modifier légèrement votre exécution de FormBuilder.group  :

Dans new-user/new-user.component.ts :

    initForm() {
	this.userForm = this.formBuilder.group({
	    firstName: ['', Validators.required],
	    lastName: ['', Validators.required],
	    email: ['', [Validators.required, Validators.email]],
	    drinkPreference: ['', Validators.required]
	});
    }

Plutôt qu'un string simple, vous passez un array à chaque control , avec comme premier élément la valeur par défaut souhaitée, et comme deuxième élément le ou les Validators (dans un array s'il y en a plusieurs) souhaités. Il faut également importer Validators depuis @angular/forms . Dans ce cas de figure, tous les champs sont requis et la valeur du champ email doit être sous un format valable d'adresse mail (la validité de l'adresse elle-même n'est forcément pas évaluée).

Même si les Validators sont des fonctions, il ne faut pas ajouter les parenthèses () en les déclarant ici. Les déclarations de Validators dans FormBuilder informent Angular de la validation souhaitée : Angular s'occupe ensuite d'exécuter ces fonctions au bon moment.

En liant la validité de userForm à la propriété disabled du bouton submit , vous intégrez la validation de données :

Dans new-user/new-user.component.html :

<button type="submit" class="btn btn-primary" [disabled]="userForm.invalid">Soumettre</button>

Dans ce chapitre, vous avez vu les Validators required et email  : il en existe d'autres, et vous avez également la possibilité de créer des Validators personnalisés. Pour plus d'informations, référez-vous à la section correspondante de la documentation Angular.

Ajoutez dynamiquement des FormControl

Pour l'instant, vous n'avez pas encore laissé la possibilité à l'utilisateur d'ajouter ses hobbies.

Il serait intéressant de lui laisser la possibilité d'en ajouter autant qu'il veut, et pour cela, vous allez utiliser un FormArray . Un FormArray est un array de plusieurs FormControl , et permet, par exemple, d'ajouter des nouveaux controls à un formulaire.

Vous allez utiliser cette méthode pour permettre à l'utilisateur d'ajouter ses hobbies.

Modifiez d'abord initForm() pour ajouter un FormArray vide qui s'appellera hobbies avec la méthode array  :

Dans new-user/new-user.component.ts :

initForm() {
    this.userForm = this.formBuilder.group({
      firstName: ['', Validators.required],
      lastName: ['', Validators.required],
      email: ['', [Validators.required, Validators.email]],
      drinkPreference: ['', Validators.required],
      hobbies: this.formBuilder.array([])
    });
}

Modifiez ensuite onSubmitForm() pour récupérer les valeurs, si elles existent (sinon, retournez un array vide) :

Dans new-user/new-user.component.ts :

onSubmitForm() {
    const formValue = this.userForm.value;
    const newUser = new User(
      formValue['firstName'],
      formValue['lastName'],
      formValue['email'],
      formValue['drinkPreference'],
      formValue['hobbies'] ? formValue['hobbies'] : []
    );
    this.userService.addUser(newUser);
    this.router.navigate(['/users']);
}

Afin d'avoir accès aux controls à l'intérieur de l'array, pour des raisons de typage strict liées à TypeScript, il faut créer une méthode qui retourne hobbies par la méthode get() sous forme de FormArray ( FormArray s'importe depuis @angular/forms )  :

Dans new-user/new-user.component.ts :

getHobbies(): FormArray {
    return this.userForm.get('hobbies') as FormArray;
}

Ensuite, vous allez créer la méthode qui permet d'ajouter un FormControl à hobbies , permettant ainsi à l'utilisateur d'en ajouter autant qu'il veut.

Vous allez également rendre le nouveau champ requis, afin de ne pas avoir un array de hobbies avec des string vides :

Dans new-user/new-user.component.ts :

onAddHobby() {
    const newHobbyControl = this.formBuilder.control(null, Validators.required);
    this.getHobbies().push(newHobbyControl);
}

Cette méthode crée un control avec la méthode FormBuilder.control() , et l'ajoute au FormArray rendu disponible par la méthode getHobbies() .

Enfin, il faut ajouter une section au template qui permet d'ajouter des hobbies en ajoutant des <input>  :

Dans new-user/new-user.component.html :

<div formArrayName="hobbies">
      <h3>Vos hobbies</h3>
      <div class="form-group" *ngFor="let hobbyControl of getHobbies().controls; let i = index">
        <input type="text" class="form-control" [formControlName]="i">
      </div>
      <button type="button" class="btn btn-success" (click)="onAddHobby()">Ajouter un hobby</button>
</div>
<button type="submit" class="btn btn-primary" [disabled]="userForm.invalid">Soumettre</button>

Analysez cette <div> :

  • à la <div> qui englobe toute la partie hobbies , vous ajoutez l'attribut formArrayName , qui correspond au nom choisi dans votre TypeScript ;
  • la <div> de class form-group est ensuite répété pour chaque FormControl dans le FormArray (retourné par getHobbies() , initialement vide, en notant l'index afin de créer un nom unique pour chaque FormControl ;
  • dans cette <div> , vous avec une <input> qui prendra comme formControlName l'index du FormControl ;
  • enfin, vous avez le bouton (de type button pour l'empêcher d'essayer de soumettre le formulaire) qui déclenche onAddHobby() , méthode qui, pour rappel, crée un nouveau FormControl (affichant une nouvelle instance de la <div> de class form-group , et donc créant une nouvelle <input> )

Maintenant, vous savez créer des formulaires par deux méthodes différentes, et comment récupérer les données saisies par l'utilisateur. Pour l'instant, la capacité d'enregistrement et de gestion de ces données a été limitée au service et donc tout est systématiquement remis à zéro à chaque rechargement de l'app. Dans les chapitres suivants, vous allez apprendre à interagir avec un serveur (et puis, plus précisément, avec un backend Firebase) afin de rendre les données permanentes et que votre application soit totalement dynamique.

Interagissez avec un serveur avec HttpClient

Dans une application Angular, vous aurez très souvent besoin de faire des appels à un backend ou à un autre serveur — pour enregistrer ou charger des données, par exemple, ou pour effectuer des calculs ou des modifications de données que vous ne souhaitez pas faire faire par le frontend. Angular met à disposition un service appelé HttpClient qui permet de créer et d'exécuter des appels HTTP (fait par AJAX - Asynchronous JavaScript and XML) et de réagir aux informations retournées par le serveur.

Dans ce chapitre, vous allez configurer un backend avec le service Firebase de Google. Ce service permet la création d'un backend complet sans coder, et node comprend énormément de services, dont l'authentification, une base de données NoSQL et un stockage de fichiers. Dans un chapitre ultérieur, vous apprendrez à utiliser les fonctions mises à disposition par Firebase afin de mieux intégrer le service. Pour ce chapitre, vous allez simplement utiliser l'API HTTP afin de comprendre l'utilisation de HttpClient .

Préparez le backend

Allez à firebase.com, créez un compte Google ou authentifiez-vous si vous en avez déjà un, et créez un nouveau projet Firebase. Vous pouvez le domicilier dans votre pays de résidence et lui donner le nom que vous voulez.

Une fois arrivé sur la console, allez dans Database et choisissez le Realtime Database. Afin d'éviter tout problème d'authentification pour l'instant

  • allez dans la section Règles et définissez read et write en true
  • puis publiez les règles modifiées

Revenez à la section Données et notez l'URL de votre base de données, vous allez en avoir besoin pour configurer les appels HTTP :

'https://http-client-demo-61961.firebaseio.com/axiomes.json'

Le backend est maintenant prêt, et vous allez pouvoir intégrer HttpClient à votre application des appareils électriques.

Envoyez vers le backend

Pour avoir accès au service HttpClient , il faut tout d'abord ajouter HttpClientModule , importé depuis @angular/common/http , à votre AppModule  :

Dans app.module.ts :

imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    HttpClientModule,
    RouterModule.forRoot(appRoutes)
],

Vous allez utiliser HttpClient , dans un premier temps, pour la gestion des données de la liste d'appareils. Vous allez donc l'injecter dans AppareilService , en y ayant auparavant ajouté le décorateur @Injectable() (importé depuis @angular/core )

Dans appareil.service.ts :

import { Subject } from 'rxjs/Subject';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class AppareilService {

  appareilsSubject = new Subject<any[]>();

  private appareils = [
    {
      id: 1,
      name: 'Machine à laver',
      status: 'éteint'
    },
    {
      id: 2,
      name: 'Frigo',
      status: 'allumé'
    },
    {
      id: 3,
      name: 'Ordinateur',
      status: 'éteint'
    }
  ];
  
  constructor(private httpClient: HttpClient) { }

Si votre service n'avait pas encore de constructeur, créez-le pour injecter HttpClient . Vous allez d'abord créer une méthode qui va enregistrer l'array appareils dans la base de données au endpoint /appareils par la méthode POST :

Dans appareil.service.ts :

saveAppareilsToServer() {
    this.httpClient
      .post('https://httpclient-demo-61961.firebaseio.com/appareils.json', this.appareils)
      .subscribe(
        () => {
          console.log('Enregistrement terminé !');
        },
        (error) => {
          console.log('Erreur ! : ' + error);
        }
      );
}

Analysez cette méthode :

  • la méthode post() , qui permet de lancer un appel POST, prend comme premier argument l'URL visée, et comme deuxième argument le corps de l'appel, c'est-à-dire ce qu'il faut envoyer à l'URL ;
  • l'extension .json de l'URL est une spécificité Firebase, pour lui dire que vous lui envoyez des données au format JSON ;
  • la méthode post() retourne un Observable — elle ne fait pas d'appel à elle toute seule. C'est en y souscrivant que l'appel est lancé ;
  • dans la méthode subscribe() , vous prévoyez le cas où tout fonctionne et le cas où le serveur vous renverrait une erreur.

Créez maintenant un bouton dans AppareilViewComponent qui déclenche cette sauvegarde (en passant, si vous ne l'avez pas encore fait, vous pouvez retirer l'activation conditionnelle des boutons "Tout allumer" et "Tout éteindre") :

Dans appareil-view.component.html :

<button class="btn btn-success"
        (click)="onAllumer()">Tout allumer</button>
<button class="btn btn-danger"
        (click)="onEteindre()">Tout éteindre</button>
<button class="btn btn-primary"
        (click)="onSave()">Enregistrer les appareils</button>

Dans appareil-view.component.ts :

onSave() {
    this.appareilService.saveAppareilsToServer();
}

Enregistrez le tout, et cliquez sur le bouton que vous venez de créer : vous devez avoir votre message de réussite qui apparait dans la console. Si vous regardez maintenant la console Firebase :

Firebase a créé un nouveau node sous appareils avec un identifiant unique, et y a enregistré votre array appareils .

Cependant, si vous cliquez plusieurs fois sur ce bouton, Firebase continuera à créer de nouveaux nodes, et dans ce cas de figure, ce n'est pas le comportement souhaité. Il faudrait que chaque enregistrement écrase le précédent : pour cela, utilisez plutôt la méthode put() (il n'y a pas besoin de changer les arguments, car les méthodes put() et post() prennent les deux mêmes premiers arguments) :

Dans appareil.service.ts :

saveAppareilsToServer() {
    this.httpClient
      .put('https://http-client-demo.firebaseio.com/appareils.json', this.appareils)
      .subscribe(
        () => {
          console.log('Enregistrement terminé !');
        },
        (error) => {
          console.log('Erreur ! : ' + error);
        }
      );
}

Maintenant, quand vous enregistrez les données, votre console Firebase montre le node suivant :

...

Ainsi, vous savez envoyer des données vers un serveur avec les méthodes POST et PUT. Pour la suite, vous allez intégrer la requête GET pour récupérer et traiter des données depuis le serveur.

Recevez depuis le backend

Afin de demander la liste des appareils (maintenant stocké au endpoint /appareils ), vous allez créer une nouvelle méthode qui emploie la méthode get() dans AppareilService  :

Dans appareil.service.ts :

getAppareilsFromServer() {
    this.httpClient
      .get<any[]>('https://httpclient-demo.firebaseio.com/appareils.json')
      .subscribe(
        (response) => {
          this.appareils = response;
          this.emitAppareilSubject();
        },
        (error) => {
          console.log('Erreur ! : ' + error);
        }
      );
}

Comme pour post() et put() , la méthode get() retourne un Observable, mais puisqu'ici, vous allez recevoir des données, TypeScript a besoin de savoir de quel type elles seront (l'objet retourné est d'office considéré comme étant un Object). Vous devez donc, dans ce cas précis, ajouter <any[]> pour dire que vous allez recevoir un array de type any , et que donc TypeScript peut traiter cet objet comme un array : si vous ne le faites pas, TypeScript vous dira qu'un array ne peut pas être redéfini comme Object.

Vous pouvez maintenant vider l'array appareils du service et intégrer un bouton au component permettant de déclencher la méthode getAppareilsFromServer()  :

Dans appareil-view.component.html :

<button class="btn btn-primary"
        (click)="onFetch()">Récupérer les appareils</button>

Dans appareil-view.component.ts :

onFetch() {
    this.appareilService.getAppareilsFromServer();
}

Maintenant, vous pouvez ajouter de nouveaux appareils, en modifier l'état et les sauvegarder, puis récupérer la liste sauvegardée.

Il serait également possible de rendre automatique le chargement et l'enregistrement des appareils (par exemple en appelant la méthode getAppareilsFromServer() dans ngOnInit() , et saveAppareilsToServer() après chaque modification), mais j'ai souhaité vous laisser la possibilité de les exécuter manuellement afin de voir le résultat de manière plus concrète. Dans ce chapitre, vous avez appris à passer des appels à un serveur HTTP avec le service HttpClient . Vous avez utilisé un backend Firebase pour cette démonstration : en effet, Firebase propose une API beaucoup plus flexible que des appels HTTP simples afin de profiter pleinement des services proposés. Dans les prochains chapitres de ce cours, vous allez apprendre à intégrer certains des services Firebase dans votre application.

====Recevez depuis le backend====