L'un des énormes avantages d'utiliser Angular est de pouvoir créer des "single page application" (SPA). Sur le Web, ces applications sont rapides et lisses : il n'y a qu'un seul chargement de page au début, et même si les données mettent parfois du temps à arriver, la sensation pour l'utilisateur est celle d'une application native. Au lieu de charger une nouvelle page à chaque clic ou à chaque changement d'URL, on remplace le contenu ou une partie du contenu de la page : on modifie les components qui y sont affichés, ou le contenu de ces components. On accomplit tout cela avec le "routing", où l'application lit le contenu de l'URL pour afficher le ou les components requis.
L'application des appareils électriques n'a que la view des appareils à afficher pour le moment ; je vous propose de créer un component pour l'authentification (qui restera simulée pour l'instant) et vous créerez un menu permettant de naviguer entre les views.
Tout d'abord, créez le component avec le CLI :
ng g c auth
Vous allez également devoir modifier un peu l'organisation actuelle afin d'intégrer plus facilement le routing : vous allez créer un component qui contiendra toute la view actuelle et qui s'appellera AppareilViewComponent
:
ng g c appareil-view
Ensuite, coupez tout le contenu de la colonne dans app.component.html
, enregistrez-le dans appareil-view.component.html
, et remplacez-le par la nouvelle balise <app-appareil-view>
:
class="container"
class="row"
class="col-xs-12"
Il faudra également déménager la logique de cette view pour que tout re-marche : injectez AppareilService
, créez l'array appareils
, intégrez la logique ngOnInit
et déplacez les fonctions onAllumer()
et onEteindre()
:
import { Component, OnInit } from '@angular/core';
import { AppareilService } from '../services/appareil.service';
@Component({
selector: 'app-appareil-view',
templateUrl: './appareil-view.component.html',
styleUrls: ['./appareil-view.component.scss']
})
export class AppareilViewComponent implements OnInit {
appareils: any[];
lastUpdate = new Promise((resolve, reject) => {
const date = new Date();
setTimeout(
() => {
resolve(date);
}, 2000
);
});
constructor(private appareilService: AppareilService) { }
ngOnInit() {
this.appareils = this.appareilService.appareils;
}
onAllumer() {
this.appareilService.switchOnAll();
}
onEteindre() {
if(confirm('Etes-vous sûr de vouloir éteindre tous vos appareils ?')) {
this.appareilService.switchOffAll();
} else {
return null;
}
}
}
Vous pouvez faire le ménage dans AppComponent
, en retirant tout ce qui n'y sert plus. Créez également une boolean isAuth
dans AppareilViewComponent
, et déclarez-la comme false
, car vous allez intégrer un service d'authentification pour la suite.
Ajoutez la barre de navigation suivante à AppComponent
:
class="navbar navbar-default"
class="container-fluid"
class="navbar-collapse"
class="nav navbar-nav"
href="#"Authentification
class="active" href="#"Appareils
Maintenant, tout est prêt pour créer le routing de l'application.
Créez des routes
Tout d'abord, qu'est-ce qu'une route dans une application Angular ?
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
.
On crée une constante de type Routes
(qu'on importe depuis @angular/router
) qui est un array d'objets JS qui prennent une certaine forme :
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';
import { AppareilService } from './services/appareil.service';
import { AuthComponent } from './auth/auth.component';
import { AppareilViewComponent } from './appareil-view/appareil-view.component';
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. Je vous montrerai d'autres façons de gérer cela dans les chapitres suivants.
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 :
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>
:
class="container"
class="row"
class="col-xs-12"
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. Dans le chapitre suivant, vous allez intégrer les liens de la barre de navigation afin que l'utilisateur puisse naviguer facilement.
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
:
class="navbar navbar-default"
class="container-fluid"
class="navbar-collapse"
class="nav navbar-nav"
routerLink="auth"Authentification
class="active" routerLink="appareils"Appareils
class="container"
class="row"
class="col-xs-12"
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 :
class="nav navbar-nav"
routerLinkActive="active" routerLink="auth"Authentification
routerLinkActive="active" routerLink="appareils"Appareils
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 AppModule
) :
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) :
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 :
Authentification
class="btn btn-success" *ngIf="!authStatus" (click)="onSignIn()"Se connecter
class="btn btn-danger" *ngIf="authStatus" (click)="onSignOut()"Se déconnecter
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()
:
constructor(private authService: AuthService, private router: Router) { }
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
:
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 :
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() {
}
}
{{ name }}
Statut : {{ status }}
routerLink="/appareils"Retour à la liste
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 :
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
:
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 :
appareils = [
{
id: 1,
name: 'Machine à laver',
status: 'éteint'
},
{
id: 2,
name: 'Frigo',
status: 'allumé'
},
{
id: 3,
name: 'Ordinateur',
status: 'éteint'
}
];
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 :
ngOnInit() {
const id = this.route.snapshot.params['id'];
this.name = this.appareilService.getAppareilById(+id).name;
this.status = this.appareilService.getAppareilById(+id).status;
}
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 :
@Input() appareilName: string;
@Input() appareilStatus: string;
@Input() index: number;
@Input() id: number;
class="list-group"
*ngFor="let appareil of appareils; let i = index"
[appareilName]="appareil.name"
[appareilStatus]="appareil.status"
[index]="i"
[id]="appareil.id"
[ngStyle]="{color: getColor()}"Appareil : {{ appareilName }} -- Statut : {{ getStatus() }}
[routerLink]="[id]"Détail
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
:
Erreur 404
La page que vous cherchez n'existe pas !
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 :
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) :
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 {
}
}
Sauvegardez ce fichier dans le dossier services
sous le nom auth-guard.service.ts
.
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.