Angular Routing

Lazy Load

Guards

Resolvers

FREDDY MONTES

AUTOR:

💼 Team Lead Frontend Developer @ DotCMS

🐦 @fmontes

📷 @fmontes

🌎 http://fmontes.com

✉️ me@fmontes.com

Freddy Montes

An Angular application is a tree of components. (...) In other words, a router state is an arrangement of application components that defines what is visible on the screen.

  1. Es una librería de routing “extra”
  2. Es mantenida por el core team de angular
  3. Entre sus características principales:
    • Lazy load routes
    • Guards para proteger el acceso a rutas
    • Resolvers para pre-cargar data

@angular/router

Veamos
código

ng new angular-cr-routing

Usando angular-cli

Agrega Angular Routing

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

const routes: Routes = [];

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

src/app/app-routing.module.ts

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

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

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

src/app/app.module.ts

Es una directiva de la librería que se usa como un componente. Actúa como un placeholder que marca el sitio en el template donde el router va a mostrar los componentes.

<router-outlet>

<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
    <h1>Welcome to {{ title }}!</h1>
    <img
        width="300"
        alt="Angular Logo"
        src="data:image/..."
    />
</div>
<h2>Here are some links to help you start:</h2>
<ul>
    <li>
        <h2>
            <a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a>
        </h2>
    </li>
    <li>
        <h2>
            <a target="_blank" rel="noopener" href="https://angular.io/cli">CLI Documentation</a>
        </h2>
    </li>
    <li>
        <h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2>
    </li>
</ul>

<router-outlet></router-outlet>

src/app/app.component.html

<header>
   <h1>Header</h1>
</header>
<router-outlet></router-outlet>
<footer>
   <p>Footer</p>
</footer>

src/app/app.component.html

ng serve

Definamos nuestras primeras rutas

ng g component pages/home
ng g component pages/costa-rica

Creamos componentes

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
import { CostaRicaComponent } from './pages/costa-rica/costa-rica.component';

const routes: Routes = [
    {
        path: '',
        component: HomeComponent
    },
    {
        path: 'cr',
        component: CostaRicaComponent
    }
];

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

Declaramos rutas

src/app/app-routing.module.ts

Es una directiva que se usa en los anchor links <a> y solo tenemos que pasarle el string del path.

RouterLink

<nav>
   <ul>
       <li>
           <a
               routerLink="/"
               routerLinkActive="active"
               [routerLinkActiveOptions]="{ exact: true }"
               >Home</a>
       </li>
       <li>
           <a
               routerLink="/cr"
               routerLinkActive="active"
               >Costa Rica</a>
       </li>
   </ul>
</nav>

src/app/app.component.html

Lazy Load

ng g m pages/beaches --routing

Creando el modulo

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

const routes: Routes = [];

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

src/app/pages/beaches/beaches-routing.module.ts

ng g c pages/beaches

Creando el componente

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

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

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

src/app/pages/beaches/beaches-routing.module.ts

const routes: Routes = [
    {
        path: '',
        component: HomeComponent
    },
    {
        path: 'cr',
        component: CostaRicaComponent,
    },
    {
        path: 'beaches',
        loadChildren: () => import('./pages/beaches/beaches.module').then(m => m.BeachesModule)
    }
];

src/app/app-routing.module.ts

Debugging

@NgModule({
    imports: [RouterModule.forRoot(routes, {
        enableTracing: true
    })],
    exports: [RouterModule]
})

src/app/app-routing.module.ts

Un guard es una clase con métodos que retornan un boolean o un Observable o Promise de boolean para cargar o no una ruta.

Route Guards

Metodos Para mediar
CanActivate en la navegación de una ruta
CanActivateChild en la navegación a una ruta hijo
CanDeactivate cuando se navega fuera de una ruta
CanLoad en la navegación de un módulo que se carga asíncronamente

Metodos

ng g guard guards/auth

Creando un guard

export class AuthGuard implements CanActivate {
   canActivate(
       next: ActivatedRouteSnapshot,
       state: RouterStateSnapshot
   ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
       return true;
   }
}

src/app/guards/auth.guard.ts

import { AuthGuard } from './guards/auth.guard';

const routes: Routes = [
    {
        path: '',
        component: HomeComponent
    },
    {
        path: 'cr',
        component: CostaRicaComponent,
        canActivate: [AuthGuard]
    },
    {
        path: 'beaches',
        loadChildren: './pages/beaches/beaches.module#BeachesModule'
    }
];

src/app/app-routing.module.ts

Guard retornando true

Guard retornando false

export class AuthService {
    private user$: Subject<string> = new Subject();

    login(): void {
        const user = Math.random()
            .toString(36)
            .substr(2);

        localStorage.setItem('user', user);
        this.user$.next(user);
    }

    logout(): void {
        localStorage.removeItem('user');
        this.user$.next(null);
    }

    isAuth(): Observable<string> {
        const user = localStorage.getItem('user');
        return merge(of(user), this.user$.asObservable());
    }
}

AuthService

<div *ngIf="(user$ | async) as user; else loginButton">
    <p>User: {{ user }}</p>
    <button (click)="logout()">Logout</button>
</div>

<ng-template #loginButton>
    <button (click)="login()">Login</button>
</ng-template>

UserComponent

import { AuthService } from '../services/auth.service';

@Injectable({
    providedIn: 'root'
})
export class AuthGuard implements CanActivate {
    constructor(private authService: AuthService) {}

    canActivate(
        next: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
    ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
        return this.authService.isAuth().pipe(map((token: string) => !!token));
    }
}

src/app/guards/auth.guard.ts

Resolvers

export interface Beach {
    id: string;
    name: string;
    desc?: string;
    img?: string;
}

@Injectable({
    providedIn: 'root'
})
export class BeachesService {
    constructor() {}

    getBeaches(): Observable<Beach[]> {
        return from(data).pipe(
            map((beach: Beach) => {
                return {
                    id: beach.id,
                    name: beach.name
                };
            }),
            toArray()
        );
    }

    getBeach(id: string): Observable<Beach> {
        return from(data).pipe(filter((beach: Beach) => beach.id === id), delay(2000));
    }
}

BeachesService

export class BeachesComponent implements OnInit {
   content: Beach[] = [];

   constructor(private beachService: BeachesService) {}

   ngOnInit() {
       this.beachService.getBeaches().subscribe((beaches: Beach[]) => {
           this.content = beaches;
       });
   }
} 
<h1>Beaches</h1>
<ul>
   <li *ngFor="let beach of content">{{beach.name}}</li>
</ul>

BeachesComponent

ng g resolver pages/beaches/resolver/beaches
import { Injectable } from '@angular/core';
import {
  Router, Resolve,
  RouterStateSnapshot,
  ActivatedRouteSnapshot
} from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class BeachesResolverService implements Resolve<boolean> {
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
    return of(true);
  }
}

Crear un resolver

export class BeachesResolverService implements Resolve<boolean> {
    resolve(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
    ): Observable<Beach[]> | Promise<Beach[]> | Beach[] {
        return this.beachService.getBeaches();
    }
}

Implementar resolve

import { BeachesResolverService } from './resolver/beaches-resolver.service';

const routes: Routes = [{
   path: '',
   component: BeachesComponent,
   resolve: {
       beaches: BeachesResolverService
   }
}];

Usar el resolver

src/app/pages/beaches/beaches-routing.module.ts

import { BeachesResolverService } from './resolver/beaches-resolver.service';


@NgModule({
    declarations: [BeachesComponent, DetailComponent],
    imports: [CommonModule, BeachesRoutingModule],
    providers: [BeachesResolverService]
})

Agregar el resolver

src/app/pages/beaches/beaches.module.ts

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

export class BeachesComponent implements OnInit {
   content: Beach[] = [];

   constructor(private router: ActivatedRoute) {}

   ngOnInit() {
       this.router.data.subscribe(data => {
           this.content = data.beaches;
       });
   }
}

Obteniendo data

src/app/pages/beaches/beaches.component.ts

export class BeachesComponent implements OnInit {
   content$: Observable<Beach[]>;

   constructor(private router: ActivatedRoute) {}

   ngOnInit() {
       this.content$ = this.router.data.pipe(pluck('beaches'));
   }
}
<li *ngFor="let beach of content$ | async">{{beach.name}}</li>

Usando async pipe

src/app/pages/beaches/beaches.component.ts

src/app/pages/beaches/beaches.component.html

Parámetros en rutas

ng g c pages/beaches/components/detail

Creamos BeachDeatail

import { DetailComponent } from './components/detail/detail.component';

const routes: Routes = [
   {
       path: '',
       component: BeachesComponent,
       resolve: {
           beaches: BeachesResolverService
       }
   },
   {
       path: ':id',
       component: DetailComponent
   }
];

Definimos la ruta

src/app/pages/beaches/beaches-routing.module.ts

<h1>Beaches</h1>
<ul>
   <li *ngFor="let beach of (content$ | async)">
       <a [routerLink]="beach.id"> {{ beach.name }}</a>
   </li>
</ul>

Links al detalle de beaches

src/app/pages/beaches/beaches.component.html

ng g resolver pages/beaches/resolver/beach-detail

Creamos el detail resolver

import { Beach, BeachesService } from 'src/app/services/beaches.service';

export class BeachDetailResolverService {
   constructor(private beachService: BeachesService) {}

   resolve(
       route: ActivatedRouteSnapshot,
       state: RouterStateSnapshot
   ): Observable<Beach> | Promise<Beach> | Beach {
       return this.beachService.getBeach(route.params.id);
   }
}

Implementar el resolver

src/app/pages/beaches/resolver/beach-detail-resolver.service.ts

import { BeachDetailResolverService } from './resolver/beach-detail-resolver.service';

const routes: Routes = [
    {
        path: '',
        component: BeachesComponent,
        resolve: {
            beaches: BeachesResolverService
        }
    },
    {
        path: ':id',
        component: DetailComponent,
        resolve: {
            beach: BeachDetailResolverService
        }
    }
];

Usar el resolver

src/app/pages/beaches/beaches-routing.module.ts

import { BeachDetailResolverService } from './resolver/beach-detail-resolver.service';


@NgModule({
    declarations: [BeachesComponent, DetailComponent],
    imports: [CommonModule, BeachesRoutingModule],
    providers: [BeachesResolverService, BeachDetailResolverService]
})

Agregar al modulo

src/app/pages/beaches/beaches-routing.module.ts

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

export class DetailComponent implements OnInit {
   beach$: Observable<Beach>;

   constructor(private router: ActivatedRoute) {}

   ngOnInit() {
       this.beach$ = this.router.data.pipe(pluck('beach'));
   }
}

Obteniendo la data

src/app/pages/beaches/components/detail/detail.component.ts

<ng-container *ngIf="beach$ | async as beach">
   <img [src]="beach.img" [alt]="beach.name">
   <div class="content">
       <h2>{{beach.name}}</h2>
       <p>{{beach.desc}}</p>
   </div>
</ng-container>

Mostrando la data

src/app/pages/beaches/components/detail/detail.component.html

¿Preguntas?

¡Muchas Gracias!

Angular routing, lazy load, guards and resolvers

By Freddy Montes

Angular routing, lazy load, guards and resolvers

  • 489