Routing en Angular: Guía completa: Parte 7

Seguimos aprendiendo sobre routing en esta serie de capítulos. Por fin llegamos a la parte #7, ¡que será la última! Aún nos queda mucho por aprender (me incluyo, porque aprendo muchísimo haciendo estos artículos). ¡Vamos a ello!

Cómo usar nuestro servicio de autenticación simulada

Para terminar de configurar el comportamiento del canActivate, vamos a crear la UI y la lógica para que un usuario pueda hacer login logout en nuestra app...o al menos, simularlo. 

1. En el home.component.html, al final, añadimos dos botones, uno para hacer login y otro para hacer logout, vinculados cada uno a un método que después configuraremos.

<button class="btn btn-info mx-2" (click)="onlogin()">Login</button>
<button class="btn btn-outline-info" (click)="onlogout()">Logout</button>

2. En el home.component.ts, inyectamos nuestro authService y lo convertimos en una propiedad de TS.

3. Añadimos la función onlogin, donde ejecutamos la función login del authService, que dará el valor de true al boolean "loggedIn", recuerda.

4. Hacemos lo mismo con el método onlogout, pero al contrario: llamando al método logout, para que establezca el valor de loggedIn como false. 

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

...

  onlogin() {
    this.authService.login();
  }

  onlogout() {
    this.authService.logout();
  }

Con estos cambios, verás que si hacemos clic en el botón Login y luego vamos a Servers, podemos ver la lista de servidores y su correspondiente servidor individual a la derecha (el componente Server). Pero si haces logout desde el botón Logout, verás que ya no tienes acceso a los detalles de la lista de servidores. ¡Estupendo!


Cómo controlar la navegación con canDeactivate

Vamos a aprender cómo gestionar el caso en el que un usuario quiera irse de una ruta y debamos asegurarnos de que algo suceda antes de que salga. Por ejemplo, el típico caso de cuando un usuario hace un cambio en un campo pero no guarda los cambios y se dispone a salir de la página. Debemos ser capaces de detectar ese comportamiento para, por ejemplo, preguntarle si desea salir sin guardar los cambios que ha hecho o por el contrario prefiere quedarse en la ruta en la que está.

En el caso concreto de nuestra app, sería cuando un usuario está en la ruta /servers y se quiere marchar después de haber pinchado en un servidor para editarlo y no haber hecho clic en el botón Update server.

1. En el edit-server.component.ts añadimos una propiedad llamada changesSaved, cuyo valor inicial es false. Su valor cambiará a true cuando la función onUpdateServer se dispare.

2. Inyectamos el Router y lo convertimos en propiedad de TS.

3. Después de cambiar el valor de changesSaved en el método, usamos el router para navegar fuera de la ruta con navigate, navegando un nivel hacia arriba, lo que nos llevará al último servidor cargado relativo a la ruta activa en ese momento.

import { ActivatedRoute, Params, Router } from '@angular/router';

...

  changesSaved = false;

...

  onUpdateServer() {
    this.serversService.updateServer(this.server.id, {name: this.serverName, status: this.serverStatus});
    this.changesSaved = true;
    this.router.navigate(['../'], {relativeTo: this.route});
  }

Hecho esto, vamos a configurar un código para preguntarle al usuario si desea salir sin guardar sus cambios. Ese futuro código deberá ejecutarse dentro del edit-server.component.ts, pero no podemos crearlo ahí dentro, sino en un archivo a parte, porque lo que necesitamos configurar es un guardián de rutas ??, que necesita tener la forma de un service, y por tanto, deberá ser un archivo independiente.

4. Dentro de la carpeta edit-server creamos un archivo llamado can-deactivate-guard.service.ts.

5. Exportamos una interface de Angular y la llamamos CanComponentDeactivate.

Una interface es un código que obliga a la clase que lo importe a tener una determinada forma, como unos requisitos. Pero no contiene lógica de programación, sino la apariencia que debe tener esa lógica.

Desde la interface, configuramos que el componente que la exporte debe tener un método llamado canDeactivate, cuyo tipo será una función que devuelva un observable, una promesa o un boolean. Reconocerás este patrón del canActivate guard.

6. Exportamos una clase llamada CanDeactivateGuard, que implementará una interface llamada CanDeactivate. Esta interface es genérica, así que debemos especificarle qué interface concretamente queremos implementar. Por tanto, le pasamos la nuestra.

Esto, que suena un pelín complejo, es la clave para que luego podamos conectar un componente a nuestro guardián ?, cosa que Max explica a la perfección en su curso de Angular.

7. En la clase creamos un método llamado canDeactivate, que será el método llamado cuando un usuario intente irse de la ruta.

  • Por eso, toma como primer argumento el componente en el que nos encontremos, que será de tipo CanComponenentDeactivate.
  • Como segundo argumento espera la currentRoute, que es de tipo ActivatedRouteSnapshot.
  • Como tercero, el currentState, de tipo RouterStateSnapshot.
  • Y como cuarto, el nextState, encargado de determinar a dónde queremos ir una vez abandonemos la ruta. Es un argumento opcional, de tipo RouterStateSnapshot.

Al igual que el canActivate guard, la función canDeactivate puede devolver un observable, una promesa o un boolean.

8. En el cuerpo de la función canDeactivate, llamamos al método canDeactivate del componente en el que nos encontremos, y hacemos un return del mismo.

import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot } from "@angular/router";
import { Observable } from "rxjs";
export interface CanComponentDeactivate {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
export class CanDeactivateGuard implements CanDeactivate<cancomponentdeactivate> {
  canDeactivate(component: CanComponentDeactivate,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState?: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return component.canDeactivate();
  }
}

9. Vamos al app-routing.module.ts y añadimos el canDeactivate guard a la ruta edit, hija de la ruta servers. La propiedad canDeactivate espera un array donde le pasaremos nuestra ruta.

      { path: ':id/edit', component: EditServerComponent, canDeactivate: [CanDeactivateGuard] },

Con estos cambios, Angular aplicará este guardián siempre que intentemos salir de la ruta edit. 

10. Desde el AppModule, añadimos nuestro guardián a la lista de providers.

  providers: [ServersService, AuthGuard, AuthService, CanDeactivateGuard],

Por fin, vamos a usar nuestro guardián en el componente EditServer.

11. En la clase, implementamos nuestra interface CanComponentDeactivate, lo que nos obliga a implementar el método canDeactivate en el componente. Así que lo creamos. Ya sabemos lo que puede devolvernos, así que se lo añadimos como tipo.

Aquí es donde escribimos la lógica de programación para determinar lo que pasará cuando un usuario intente irse de la ruta.

12. Comprobamos si el usuario tiene permitido editar un servidor. Si no, le dejamos ir. Hacemos otra comprobación para ver si el serverName es el mismo que había al principio o si el serverStatus ha cambiado.

Si uno de los dos ha sufrido cambios y además los cambios no han sido guardados, devolvemos una ventana de confirmación (en inglés, confirm dialog), pregutándole al usuario si quiere descartar los cambios.

Si nada de esto se cumple, devolvemos true.

export class EditServerComponent implements OnInit, CanComponentDeactivate {
 
  ...
  canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
    if (!this.allowEdit) {
      return true;
    }
    if ((this.serverName !== this.server.name || this.serverStatus !== this.server.status) && !this.changesSaved) {
      return confirm('Do you want to exit without saving your changes?');
    } else {
      return true;
    }

Antes de probar si funciona, debemos actualizar una parte del código que todavía es estática

13. En el ngOnInit, debemos actualizar el ID para obtenerlo dinámicamente. Para eso, creamos una variable llamada id y accedemos al ID de la ruta. No olvides convertirlo a número.

14. Al server ya no le pasamos un número estático, sino el id por parámetro.

    const id = +this.route.snapshot.params['id']
    this.server = this.serversService.getServer(id);

¡Y ya lo tenemos! ? Si ahora haces login, editas un servidor de la lista (actualmente sólo podemos editar el Devserver) e intentas salir sin guardar los cambios, verás que te salta la ventana de confirmación. Por el contrario, si le das a Update server, tus cambios se guardarán.


Cómo pasar datos estáticos a una ruta

Vamos a aprender a obtener información estática de una ruta cuando ésta se cargue. No desde los query params, sino desde el AppRoutingModule. Un caso típico donde usuaríamos esto sería si quisiéramos tener un componente genérico donde mostrar errores de nuestra app, que podría relacionarse con un error de búsqueda, un típico 404, que como sabemos es el componente PageNotFound.

Así que el objetivo sería reusar ese componente genérico para todo tipo de errores.

1. Creamos dicho componente dentro de la carpeta app y lo llamamos error-page.

2. En el error-page.component.html, creamos una propiedad llamada errorMessage, que vinculamos con string interpolation.

<p>{{errorMessage}}</p>

3. En el error-page.component.ts, creamos la propiedad, que será de tipo string. Pero como el fin de este componente es reusarlo, no vamos a darle ningún valor inicial a la propiedad.

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

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

  errorMessage: string;

  constructor() { }

  ngOnInit() {
  }

}

4. En el AppRoutingModule, en la ruta not-found, ya no vamos a cargar el PageNotFoundComponentsino nuestro componente genérico.

Imaginemos que tenemos varias páginas del tipo PageNotFound, destinadas a mostrar diferentes mensajes de error. De momento sólo tenemos una, pero si tuviésemos más, cada una de ellas contendría un mensaje estático, definiendo el tipo de error. Esa info estática la podemos pasar en la configuración de la ruta, usando la propiedad dataque acepta un JS object. Aquí es donde definimos, mediante key-value pairs, lo que queramos. Por ejemplo: 

  • key: message
  • value: Ooopsi! Page not found.
  { path: 'not-found', component: ErrorPageComponent, data: {message: 'Ooopsi! Page not found.'} },

5. En el error-page.component.ts, inyectamos la activatedRoute. 

Rescataremos esta info desde el componente ErrorPage. Tenemos dos formas de hacer esto desde el ngOnInit:

1ª le damos a errorMessage el valor de la propiedad message, accediendo a ella desde el snapshot de la ruta. 


  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.errorMessage = this.route.snapshot.data['message'];
  }

2ª o, en caso de que se puedan producir cambios cuando aún estemos en la página, suscribiéndonos al observable "data". Eso nos da un data object de tipo Data, lo que nos da acceso al mensaje, cuyo valor asignamos a la propiedad errorMessage.

    // this.errorMessage = this.route.snapshot.data['message'];
    this.route.data.subscribe(
      (data: Data) => {
        this.errorMessage = data['message'];
      }
    )

¡Y ya lo tenemos! ?

Cómo gestionar datos dinámicos con el Resolve Guard

Supongamos que tenemos cierta información dinámica (en inglés, dynamic data) que necesitamos rescatar justo antes de cargar una ruta. Por ejemplo, desde la pestaña Servers, supongamos que cada servidor de la lista debe conectarse a un backend para mostrar los detalles de cada uno (el ServerComponent).

Simularemos esta llamada al backend haciendo que nuestro código tarde algunos milisegundos en cargar.

Para este tipo de casos necesitamos un resolverque es un servicio, al igual que canActivate canDeactivate.

Un resolver nos permitirá ejecutar un bloque de código justo antes de cargar una ruta. 

El resolver no decidirá si una ruta o un componente puede cargarse o no. Ese no es su papel. Lo que hará será "pre-cargar" cierta información que nuestro componente necesitará a posteriori.

?? Una alternativa a usar el resolver es rescatar la info directamente en el componente, en el ngOnInit. 

1. Dentro de la carpeta server, creamos un archivo llamado server-resolver.service.ts. Un resolver necesita implementar la interface "Resolve"así que así lo hacemos. Esta interface es de tipo genérico, y engloba la info que rescataremos posteriormente.

Como rescataremos un Server, le pasamos un objeto con los key-value pairs de un Server:

  • id
  • name
  • status

Aunque lo más fácil es pasarle la interface de un server, así que la creamos y se la pasamos.

2. La interface "Resolve" nos obliga a tener un método llamado resolve, que espera dos argumentos:

  • la ruta del snapshot
  • el state del snapshot

El método resolve puede ser de 3 tipos:

  • un Observable en forma del server object
  • una promesa en forma del server object
  • simplemente el server (síncrono)

3. Inyectamos en el constructor el ServersService para poder comunicarnos con él.

4. En el cuerpo de la función resolve, nos comunicamos con el ServersService y llamamos a la función getServer.

Ahora necesitamos saber el ID del servidor que queremos rescatar, porque eso es lo que estamos haciendo en el ngOnInit del server.component.ts. Para eso utilizaremos el snapshot de la ruta.

5. Lo transformamos en un número añadiéndole el signo +.

Y así es como en este curso de Angular aprendí a configurar un resolver para que cargue nuestra info por adelantado.

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { ServersService } from '../servers.service';

export interface Server {
  id: number;
  name: string;
  status: string;
}

@Injectable()
export class ServerResolver implements Resolve<server>{

  constructor(private serversService: ServersService) { }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<server> | Promise<server> | Server {
    return this.serversService.getServer(+route.params['id']);
  }

}

Lo siguiente que haremos será añadir el resolver.

6. En el AppModule, lo añadimos a los providers.

  providers: [ServersService, AuthGuard, AuthService, CanDeactivateGuard, ServerResolver],

Ahora debemos añadirlo al AppRoutingModule.

7. En el path donde queramos usarlo, por ejemplo en el primer child de servers, añadimos una propiedad llamada resolve, que espera un objeto de JS. Aquí definimos key-value pairs, donde el key puede llamarse como queramos. Lo llamamos server y le damos el value de nuestro resolver.

?? Esto mapeará la info que el resolver nos dé.

path: 'servers',
...    
    children: [
      {
        path: ':id/edit',
        component: EditServerComponent,
        canDeactivate: [CanDeactivateGuard],
        resolve: { server: ServerResolver }
      },

Si vamos al server.component.ts, vemos que ahora estamos obteniendo el server en el ngOnInit usando los params. 

8. Comentamos ese código, porque ahora vamos a usar un resolver para hacer lo mismo.

9. Utilizamos el data observable de la ruta y nos suscribimos a él.

10. En el cuerpo de la función subscribe, recibiremos nuestra data, de tipo Data. 

11. Asignamos a nuestro server el valor del server proviente de la data de nuestro parámetro. Ese último server tiene (y debe tener) el mismo nombre que le hemos dado en el AppRoutingModule.

  ngOnInit() {
    // const id = +this.route.snapshot.params['id'];
    // this.server = this.serversService.getServer(id);

    // this.route.params.subscribe((params: Params) => {
    //   this.server = this.serversService.getServer(+params['id']);
    // });
    this.route.data.subscribe(
      (data: Data) => {
        this.server = data['server'];
      }
    )
  }

¡Y ya está! ? Si guardas, verás que tu app funciona igual que antes, pero el código que hay detrás ha cambiado.

THE END!

¡Y con esto terminamos esta Routing saga! Espero que hayas aprendido algo nuevo ?.  Si te queda alguna duda, ¡nos vemos en los comentarios!

Sobre la autora de este post

Soy Rocío, una abogada reconvertida en programadora. Soy una apasionada de aprender cosas nuevas y ferviente defensora de que la única manera de ser feliz es alcanzando un equilibrio entre lo que te encanta hacer y lo que te saque de pobre. Mi historia completa, aquí. 

Si quieres ayudar a hacer este blog sostenible, puedes invitarme a un café digital ツ
¡Gracias!¡Gracias!

Más recursos de aprendizaje

En mi experiencia, la manera más eficaz para aprender Angular es combinando varias vías de aprendizaje. Uno de mis métodos favoritos son los vídeo-cursos y mi plataforma predilecta para eso es Udemy. He hecho varios cursos pero sólo recomiendo aquellos que verdaderamente me han sido útiles. Aquí van:

angular the complete guide - curso Max S.

  Max Schwarzmüller

curso angular fernando herrera

   Fernando Herrera

Si necesitas apoyo en forma de libro, puede que éstos te sirvan de ayuda:

libro 1 angular
libro 2 angular

La programación es un mundo que evoluciona a una velocidad de vértigo. Los autores de estos libros lo saben, por eso suelen encargarse de actualizar su contenido regularmente. Asegúrate de que así sea antes de adquirirlos ?.

Participo en el programa de afiliados de Udemy y Amazon, lo que significa que, si compras alguno de estos cursos y/o libros, yo me llevaré una pequeña comisión y a ti no costará nada extra. Vamos, lo que se dice un win-win ?.

Otros artículos que pueden interesarte

Cómo aprendí a programar cuando estaba «programada» para ser de letras
[tcb-script src="https://player.vimeo.com/api/player.js"][/tcb-script]A nadie le gusta su trabajo. Eso es lo que me decía a mí misma cuando conseguí mi primer[...]
Días del 160 al 203 – ¡Primer objetivo conseguido!
“A veces podemos pasarnos años sin vivir en absoluto, y de pronto toda nuestra vida se concentra en un solo[...]
Claves para entender Angular. Qué es y cómo se utiliza
Angular es un framework creado por Google que nos permite construir Single Page Applications (SPA, por sus siglas en inglés).Frameworks¿Pero qué es[...]
Si crees que este post puede serle útil a alguien, por favor, ¡compártelo!:

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Esta web utiliza cookies para asegurar que se da la mejor experiencia al usuario. Si continúas utilizando este sitio se asume que estás de acuerdo. más información

Los ajustes de cookies en esta web están configurados para «permitir las cookies» y ofrecerte la mejor experiencia de navegación posible. Si sigues usando esta web sin cambiar tus ajustes de cookies o haces clic en «Aceptar», estarás dando tu consentimiento a esto.

Cerrar