Angular Router

Gonzalo Ruiz de Villa @gruizdevilla

CTO @gft_es

 

Angular Madrid Meetup

https://github.com/domenic/zones

¿Qué es un Router?

The Angular Router enables navigation from one view to the next as users perform application tasks.

Principales características

  • Navegación por URLs
  • Routing flexible:
    • rutas hijas
    • rutas auxiliares
  • Guardias en la navegación
  • Lazy loading & preloading
  • Resolve

https://leanpub.com/router

Angular 2 Router

by Victor Savkin

Bootstrap

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>AmRouter</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
  <app-root>Loading...</app-root>
</body>
</html>

index.html

Bootstrap

import './polyfills.ts';

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

main.ts

Plantillas

<md-toolbar color="accent">¡Bienvenido!</md-toolbar>
<md-card>
  <md-card-content>
    <h2 class="example-h2">Estado actual</h2>
    <md-slide-toggle
        [checked]="authService.logged"
        (change)="authService.logged=!authService.logged">
      Logado
    </md-slide-toggle>
    <!-- ... -->
  </md-card-content>
</md-card>

bienvenido/bienvenido.component.html

Componentes

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

@Component({
  selector: 'am-bienvenido',
  templateUrl: './bienvenido.component.html',
  styleUrls: ['./bienvenido.component.css']
})
export class BienvenidoComponent implements OnInit {

  constructor(private authService: AuthService) { }

  ngOnInit() {
  }

}

bienvenido/bienvenido.component.ts

Servicios

import { Injectable } from '@angular/core';
import { Router, CanActivate} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/of';

@Injectable()
export class AuthService  implements CanActivate {
  logged:boolean;
  constructor() {
    this.logged = false;
  }
  canActivate(){
    return Observable.of(this.logged);
  }
}

auth.service.ts

Servicios

import { Injectable } from '@angular/core';
import { Song } from './entities/song';
import { Album } from './entities/album';

@Injectable()
export class MusicService {

  //...

  selectedSongs(): Song[] { return playList; }

  albums(): Album[]{ return albumList; }

  songs(): Song[] { return songList; }

music.service.ts

Configuración del módulo

@NgModule({
  declarations: [
    AppComponent,
    PlayerComponent,
    SonglistComponent,
    AlbumgridComponent,
    BienvenidoComponent,
    //...
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    AppRoutingModule,
    MaterialModule.forRoot()
  ],
  entryComponents: [
    DeactivatedialogComponent
  ],
  providers: [AuthService, DeactivateService, MusicService],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.module.ts

Setup

import { NgModule } from '@angular/core';
import { AppRoutingModule }     from './app-routing.module';
//...

@NgModule({
  declarations: [
    //...
  ],
  imports: [
    //...
    AppRoutingModule,
    //...
  ],
  entryComponents: [//... ],
  providers: [//...],
  bootstrap: [AppComponent]
})
export class AppModule { }

Incluir módulo router

RouterModule.forRoot(routes)


RouterModule.forChild(routes)

Definiendo las rutas

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BienvenidoComponent }  from './bienvenido/bienvenido.component';

const routes: Routes = [
  { path: '', redirectTo: '/bienvenido', pathMatch: 'full' },
  { path: 'bienvenido',  component: BienvenidoComponent }
];


@NgModule({
  imports: [ RouterModule.forRoot(routes) ],
  exports: [ RouterModule ]
})
export class AppRoutingModule {}

app-routing.module.ts

Pintamos en outlets

<md-sidenav-layout>

  <md-sidenav #sidenav mode="side" class="app-sidenav"></md-sidenav>

  <md-toolbar color="primary">
    Angular Router Example App
  </md-toolbar>

  <div class="app-content">
    <router-outlet></router-outlet>
  </div>

</md-sidenav-layout>

app.component.html

Rutas jerárquicas

const routes: Routes = [
  { path: '', redirectTo: '/bienvenido', pathMatch: 'full' },
  { path: 'bienvenido',  component: BienvenidoComponent },
  { path: 'albumes',  component: AlbumgridComponent},
  { path: 'albumes/:albumTitle', component: AlbumdetailComponent}
];

app-routing.module.ts

Rutas jerárquicas (refactor)

const routes: Routes = [
  { path: '', redirectTo: '/bienvenido', pathMatch: 'full' },
  { path: 'bienvenido',  component: BienvenidoComponent },
  { path: 'albumes',  
      children: [
        {path:'', component: AlbumgridComponent},
        {path:':albumTitle', component: AlbumdetailComponent}
      ]
  }
];

app-routing.module.ts

Navegando

Con directiva routerLink:

  • routerLink="/licencia"
  • [routerLink]="['/albumes', album.title]"
  • [routerLink]="['/',{ outlets: { 'side': ['player'] } }]"

Navegando

Programáticamente:

  • this.router.navigate(['bienvenido']);
  • this.router.navigateByUrl('/bienvenido');

routerLink

  <md-card-content>
    <h2 class="example-h2">Estado actual</h2>
    <md-slide-toggle [checked]="authService.logged" (change)="authService.logged=!authService.logged">
      Logado
    </md-slide-toggle>
    <p>Debes estar logado para acceder al listado de álbumes</p>
    <p><button md-raised-button color="primary" routerLink="/albumes">Ver Álbumes</button></p>
    <p>Puedes consultar la <a routerLink="/licencia">licencia</a></p>
    <p>Puedes ver algo <a routerLink="/lazy">lazy</a></p>
  </md-card-content>

bienvenido.component.html

routerLink


<p>
  <button md-raised-button
    [routerLinkActive]="['active']" 
    [routerLink]="['/',{ outlets: { 'side': ['player'] } }]" 
    [queryParams]="{ play: 1 }">Play</button>
  <button md-raised-button
    [routerLinkActive]="['active']" 
    [routerLink]="['/',{ outlets: { 'side': ['player'] } }]" 
    [queryParams]="{ play: 0 }">Stop</button>
</p>

albumgrid.component.html

routerLink


<!-- params and queryParams -->
<a href="#/albumes/Blue-again;flag=true?q=1">Blue again</a>
<a [routerLink]="['albumes', 'Blue again', {flag: true}]" 
   [queryParams]="{q: 1}">Blue again</a>

más opciones

Para acceder a la url usamos Router

@Component({
  selector: 'am-albumdetail'
})
export class AlbumdetailComponent implements OnInit { 
  constructor(private router: Router){ }

  ngOnInit() {
    console.log(this.router.url); // /albumes/Blue-again
  }
}

Accediendo a los datos de la url

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

@Component({
  selector: 'am-albumdetail',
})
export class AlbumdetailComponent implements OnInit {

  constructor(
    private musicService:MusicService, 
    private route: ActivatedRoute
  ) {}

  ngOnInit() {
    let title = this.route.snapshot.params['albumTitle'];
    //...
  }

}

albumdetail.component.ts

Observando los datos de la url 

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

@Component({
  selector: 'am-navmusic'
})
export class NavmusicComponent implements OnInit, OnDestroy {
  play: string;

  constructor( private route: ActivatedRoute) { }

  ngOnInit() {
    this.sub = this.route
      .queryParams
      .subscribe(params => {
        this.play = params["play"];
       //...
      });
  }

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

navmusic.component.ts

Guardias: preguntar al salir

import { LicenciaComponent }    from './licencia/licencia.component';
import { DeactivateService }    from './deactivate.service';


const routes: Routes = [
  { path: '', redirectTo: '/bienvenido', pathMatch: 'full' },
  { path: 'bienvenido',  component: BienvenidoComponent },
  { 
    path: 'licencia',  
    component: LicenciaComponent, 
    canDeactivate:[DeactivateService]
  },
  
];

app-routing.module.ts

import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { MdDialog} from '@angular/material';
import { DeactivatedialogComponent } from './deactivatedialog/deactivatedialog.component';
import { LicenciaComponent } from './licencia/licencia.component';


@Injectable()
export class DeactivateService implements CanDeactivate<LicenciaComponent>  {
  constructor(private dialog: MdDialog) { }

  canDeactivate(target: LicenciaComponent) {
    return this.dialog.open(DeactivatedialogComponent).afterClosed();
  }

}

deactivate.service.ts

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


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

  constructor(public dialogRef: MdDialogRef<DeactivatedialogComponent>) { }

  ngOnInit() {
  }

}

deactivatedialog.component.ts

<h1 md-dialog-title>¿Estas seguro?</h1>
<div md-dialog-content>
  <p>¿Seguro que quieres salir? Si es la mar de interesante.</p>
</div>
<div md-dialog-actions>
  <button md-raised-button
    color="primary"
    (click)="dialogRef.close(true)">Por supuesto</button>
  <button md-raised-button
    color="warn"
    (click)="dialogRef.close(false)">Bueno... me quedo</button>
</div>

deactivatedialog.component.html

Restringiendo accesos: validando al entrar

const routes: Routes = [
  { path: '', redirectTo: '/bienvenido', pathMatch: 'full' },
  { path: 'bienvenido',  component: BienvenidoComponent },
  { path: 'licencia',  component: LicenciaComponent, canDeactivate:[DeactivateService] },
  { path: 'albumes',  canActivate:[AuthService] ,
      children: [
        {path:'', component: AlbumgridComponent},
        {path:':albumTitle', component: AlbumdetailComponent}
      ]
  }
];

app-routing.module.ts

import { Injectable } from '@angular/core';
import { Router, CanActivate} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/of';

@Injectable()
export class AuthService  implements CanActivate {
  logged:boolean;
  constructor() {
    this.logged = false;
  }
  canActivate(){
    return Observable.of(this.logged);
  }
}

auth.service.ts

Rutas auxiliares

const routes: Routes = [
  { path: '', redirectTo: '/bienvenido', pathMatch: 'full' },
  { path: 'bienvenido',  component: BienvenidoComponent },
  { path: 'licencia',  component: LicenciaComponent, canDeactivate:[DeactivateService] },
  { path: 'albumes',  canActivate:[AuthService] ,
      children: [
        {path:'', component: AlbumgridComponent},
        {path:':albumTitle', component: AlbumdetailComponent}
      ]
  },
  { path: 'player', component: NavmusicComponent, outlet: 'side'}
]

app-routing.module.ts

  <md-sidenav #sidenav mode="side" class="app-sidenav">

    <router-outlet name="side"></router-outlet>

    <p>
        <button md-raised-button color="primary" 
            [routerLinkActive]="['hidebutton']" 
            [routerLink]="[{ outlets: { 'side': ['player'] } }]"
        >Load player</button>
    </p>

  </md-sidenav>

app.component.html

<p>
  <button md-raised-button
    [routerLinkActive]="['active']" 
    [routerLink]="['/',{ outlets: { 'side': ['player'] } }]" 
    [queryParams]="{ play: 1 }">Play</button>
  <button md-raised-button
    [routerLinkActive]="['active']" 
    [routerLink]="['/',{ outlets: { 'side': ['player'] } }]" 
    [queryParams]="{ play: 0 }">Stop</button>
</p>

albumgrid.component.html

albumgrid.component.css

.active{font-style:italic;}

Lazy loading

Lazy loading

const routes: Routes = [
  //...
  { path: 'lazy', loadChildren: 'app/lazy/lazy.module#LazyModule' }
];

app-routing.module.ts

Lazy loading

import { NgModule } from '@angular/core';
import { MaterialModule } from '@angular/material';

import { LazyComponent }   from './lazy.component';
import { routing } from './lazy.routing';

@NgModule({
  imports: [routing,   MaterialModule.forRoot()],
  declarations: [LazyComponent]
})
export class LazyModule {}

lazy.module.ts

Lazy loading

import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { LazyComponent } from './lazy.component';

const routes: Routes = [
  { path: '', component: LazyComponent }
];

export const routing: ModuleWithProviders = RouterModule.forChild(routes);

lazy.routing.ts

Lazy loading

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

@Component({
  template: `<md-card>
    <md-card-content>
      <p>¡Esto es lazy!</p>
    </md-card-content>
  </md-card>`
})
export class LazyComponent {}

lazy.component.ts

Estrategias de Lazy Loading

@NgModule({
  bootstrap: [AppCmp],
  imports: [
    RouterModule.forRoot(ROUTES, 
    {preloadingStrategy: PreloadAllModules})
  ]
})
class AppModule {}

Preloading

@NgModule({
  bootstrap: [AppCmp],
  imports: [
    RouterModule.forRoot(ROUTES, 
    {preloadingStrategy: NoPreloading})
  ]
})
class AppModule {}

Sin preloading

Estrategias a medida de Lazy Loading

[
  {
    path: 'moduleA',
    loadChildren: './moduleA.module',
    data: {preload: true}
  },
  {
    path: 'moduleB',
    loadChildren: './moduleB.module'
  }
]

Agregamos un nuevo atributo a los datos de las rutas

Estrategias a medida de Lazy Loading

export class CustomPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: Function): Observable<any> {
    return route.data && route.data.preload ? load() : of(null);
  }
}

Implementamos la estrategia usando la interfaz PreloadingStrategy

Configuramos el módulo con la nueva estrategia

@NgModule({
  bootstrap: [AppCmp],
  providers: [CustomPreloadingStrategy],
  imports: [
    RouterModule.forRoot(
        ROUTES, 
        {preloadingStrategy: CustomPreloadingStrategy}
    )
  ]
})
class AppModule {}

Revisión de características

  • Navegación por URLs
  • Routing flexible:
    • rutas hijas
    • rutas auxiliares
  • Guardias en la navegación
  • Lazy loading & preloading

Pendiente para agregar al ejemplo

  • Resolve
  • Inner outlets

¡Gracias!

 

@gruizdevilla

@gft_es

Made with Slides.com