Angular routing

De Banane Atomic
Aller à la navigationAller à la recherche

Routing

app/app.module.ts
import { RouterModule } from '@angular/router';

// la première correspondance est utilisée
const routes = [
  { path: 'welcome', component: WelcomeComponent },
  { path: '', redirectTo: 'welcome', pathMatch: 'full' },  // les redirections ne s'enchainent pas, une seule est possible
  { path: 'items', component: ItemsComponent },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
    imports: [
        RouterModule.forRoot(routes, {
            useHash: true,
            enableTracing: true  // afficher dans la console les evts de routing
        }),
        ItemModule  // ce module contient ses propres règles de routage
    ]
})
export class AppModule { }
app/app.component.html
<router-outlet></router-outlet>
Html.svg
<a routerLink="/path"></a>
Typescript.svg
import { Router } from '@angular/router';

export class MyComponent {

    contructor(private router: Router) { }

    onClick() {
        this.router.navigate("/path");
    }

app-routing.module.ts

Permet d'isoler le routing dans un fichier à part.

app/app.module.ts
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  imports: [
    AppRoutingModule
  ],
})
export class AppModule { }
app/app-routing.module.ts
import { RouterModule } from '@angular/router';

// la première correspondance est utilisée
let routes = [
    { path: "welcome", component: WelcomeComponent },
    { path: "", redirectTo: "welcome", pathMatch: "full" },  // les redirections ne s'enchainent pas, une seule est possible
    { path: "items", component: ItemsComponent },
    { path: "**", component: PageNotFoundComponent }
];

@NgModule({
    imports: [
        RouterModule.forRoot(routes,
            {
                useHash: true,
                enableTracing: false  // to debug routing
            }),
        ItemModule  // ce module contient ses propres règles de routage
    ],
    exports: [ RouterModule ]
})
export class AppRoutingModule { }

Child Route

ClientApp/src/app/module1/module1.module.ts
import { RouterModule } from '@angular/router';

let routes = [
  { path: "component1", component: Component1Component }
];

@NgModule({
  imports: [
    RouterModule.forChild(routes)
  ]
})
export class Module1Module { }

Hash-based urls

Par défaut les urls sont de style HTML 5: .../welcome

  • nécessite une réécriture des urls côté serveur

Pour utiliser les urls de style hash-based: .../#/welcome il faut ajouter useHash: true dans les options du RouterModule

Noms des routes

Item List items
Item Detail items/:id
Item Edit items/:id/edit

Paramètres de routage

app/app.module.ts
{ path: "items/:id", component: ItemDetailComponent }
Html.svg
<a [routerLink]="['/items', item.id]">{{ item.name }}</a>
Typescript.svg
import { ActivatedRoute } from '@angular/router';

constructor(private route: ActivatedRoute) { }

Snapshot

Typescript.svg
import { OnInit } from '@angular/core';

export class ItemDetailComponent implements OnInit {
    // on utilise OnInit pour éviter de mettre du code asychrone dans le ctor 
    ngOnInit(): void {
        // obtenir la valeur d'un paramètre
        let id = +this.route.snapshot.paramMap.get('id');
        // + : cast string → int
Récupération des paramètres à l'initialisation du composant, si les paramètres sont modifiés mais pas l'url alors le composant n'est pas réinitialisé et on ne récupère pas les nouvelles valeurs des paramètres.

Observable

Typescript.svg
import { OnInit } from '@angular/core';

export class ItemDetailComponent implements OnInit {
    // on utilise OnInit pour éviter de mettre du code asychrone dans le ctor 
    ngOnInit(): void {
        // exécuté à chaque modification des paramètres
        this.route.paramMap.subscribe(
            params => {
                let id = +params.get('id');
            }
        );

Paramètres optionnels

Html.svg
<a [routerLink]="['/items', { name: itemName, id: itemId }]"></a>

Query parameters

Html.svg
<a [routerLink]="['/items']"
   [queryParams]="{ filterBy: 'text', showImage: true }">
</a>

<a [routerLink]="['/items']"
   [preserveQueryParams]="true">  <!-- conserve les query params déjà définit -->
    Back
</a>
Typescript.svg
this.router.navigate(['/items'],
    {
        queryParams: { filterBy: 'text', showImage: true }
    }
);

// conserve les query params déjà définit
this.router.navigate(['/items'],
    { preserveQueryParams: true }
);

Child routes

Permet de définir des règles de routages dans les modules et ainsi définir une hiérarchie dans les routes.
component-less routes module: module n'activant pas de composants, contenant juste des child routes.
Il définit son propre <router-outlet> qui contiendra le contenu des onglets par exemple.
Les child routes permettent une navigation master/detail ou avec des onglets.
child routes sont nécessaires pour le lazy loading.

item.component.ts
// component-less route: pas de component définit pour le path parent
const routes = [
  { path: 'items/:id',
    children: [
      { path: '', redirectTo: 'info' },
      { path: 'info', component: ItemComponent },
      { path: 'edit', component: ItemEditComponent },
    ]
  }
];

// naviguer vers info
this.router.navigate(['info'], { relativeTo: this.route });
item.component.html
<!-- menu -->
<div class="wizard">
    <a [routerLink]="['info']">Info</a>
    <a [routerLink]="['edit']">Edit</a>
</div>
<!-- définit l'espace d'affichage pour le contenu des child component -->
<router-outlet></router-outlet>

Style

Html.svg
<a [routerLink]="['/']" routerLinkActive="active">Items</a>

<!-- dans une liste il faut ajouter routerLinkActive à li -->
<ul>
    <!-- [routerLinkActiveOptions]='{ exact: true }' 
         ne colore le lien que s'il y a correspondance exacte.
         Par défaut, colore tous les liens qui correspondent -->
    <li routerLinkActive="active" [routerLinkActiveOptions]='{ exact: true }'>
        <a routerLink="/" (click)='collapse()'>
            <span class='glyphicon glyphicon-list'></span> Items

<!-- afficher une icône d'erreur à côté du lien -->
<a [routerLink]="['/']" routerLinkActive="active">
    Items <span [ngClass]="{'glyphicon glyphicon-list': hasError()}"></span>
</a>

Animation CSS de navigation

ClientApp/src/styles.css
/* Slide transition */
app-root div.panel.panel-primary {
    animation-duration: .5s;
    animation-name: slideIn;
    animation-fill-mode: forwards;
    transition: transform .5s ease;
}

@keyframes slideIn {
  from {
    transform: translate(-200px);
  }

  to {
    transform: translate(0px);
  }
}

/* Fade transition */
app-root div {
    animation-duration: .5s;
    opacity: 0;
    animation-name: fadeIn;
    animation-fill-mode: forwards;
    transition: transform .5s ease;
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

Spinner pour le temps d'attente lors du chargement avant navigation

Fonctionne avec un resolver sinon la page s'affiche directement.

app/app.component.ts
import { Router, Event, NavigationStart, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router';

loading = true;

constructor(private router: Router) {
  router.events.subscribe((routerEvent: Event) => this.checkRouterEvent(routerEvent));
}

checkRouterEvent(routerEvent: Event): void {
  if (routerEvent instanceof NavigationStart) {
    this.loading = true;
  } else if (routerEvent instanceof NavigationEnd || 
    routerEvent instanceof NavigationError ||
    routerEvent instanceof NavigationCancel) {
      this.loading = false;
  }
}
app/app.component.html
<!-- à la première ligne -->
<span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="loading"></span>
style.css
.spinner {
  font-size: 300%;
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 10;
}

.glyphicon-spin {
    -webkit-animation: spin 1000ms infinite linear;
    animation: spin 1000ms infinite linear;
}
@-webkit-keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}
@keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}

Spinner pour le temps d'attente lors du chargement avant navigation

Fonctionne avec un resolver sinon la page s'affiche directement.

app/app.component.ts
import { Router, Event, NavigationStart, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router';

loading = true;

constructor(private router: Router) {
  router.events.subscribe((routerEvent: Event) => this.checkRouterEvent(routerEvent));
}

checkRouterEvent(routerEvent: Event): void {
  if (routerEvent instanceof NavigationStart) {
    this.loading = true;
  } else if (routerEvent instanceof NavigationEnd || 
    routerEvent instanceof NavigationError ||
    routerEvent instanceof NavigationCancel) {
      this.loading = false;
  }
}
app/app.component.html
<!-- à la première ligne -->
<span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="loading"></span>
style.css
.spinner {
  font-size: 300%;
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 10;
}

.glyphicon-spin {
    -webkit-animation: spin 1000ms infinite linear;
    animation: spin 1000ms infinite linear;
}
@-webkit-keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}
@keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}

Secondary Routes

Dans le cas où un sous-composent a son propre menu, il faut donc gérer les routes du menu principal et les routes du menu du sous-composant.

Route Guards

  • contrôler les droits d'accès aux composants
  • empêcher la sortie d'un composant (ex: s'il manque des données à un formulaire)

Route Guards

  • contrôler les droits d'accès aux composants (ex: administrateur ou utilisateur loggué)
  • empêcher la sortie d'un composant (ex: s'il manque des données à un formulaire)
  • récupérer les données avant d'afficher la page (resolver)

Récupérer les données avec un Route Resolver

Sans Route Resolver, le template est affiché sans donnée, puis une requête récupère les données et les affiche.
Avec Route Resolver, les données sont récupérées puis le template et les données sont affichés en même temps.

app.module.ts
import { ItemResolver } from 'ClientApp/app/components/items/items-resolver.service';

let routes = [
    { path: "items", component: ItemsComponent, resolve: { items: ItemResolver } },
];
	
@NgModule({
    providers: [
        ItemResolver
    ],
items-resolver.service.ts
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

import { Observable } from 'rxjs/Observable';

@Injectable()
export class ItemResolver implements Resolve<Item[]> {

    constructor(private dataService: DataService) { }

    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Item[] | Observable<Item[]> | Promise<Item[]> {
        return this.dataService.loadItems();
    }
items.component.ts
import { ActivatedRoute } from '@angular/router';

constructor(private route: ActivatedRoute) {
    this.items = this.route.snapshot.data['items'];  // items → resolve: { items: ItemResolver }
}

Lazy loading modules

  • définir sur un module, ainsi tous les composants de ce module seront chargés de manière asynchrone
  • configurer dans le routing dans le parent
  • ne pas importer le module asynchrone dans un autre module
app.module.ts
const routes = [
  { path: 'home', component: HomeComponent },
  { path: 'images', loadChildren: 'app/images/image.module#ImageModule' }
];

CanLoad Guard: ne pas télécharger le module si l'utilisateur n'y a pas accès.