Cómo prevenir el abandono accidental de una ruta gracias a la guarda canDeactivate

akotech

akotech

Posted on June 14, 2023

Cómo prevenir el abandono accidental de una ruta gracias a la guarda canDeactivate

A la hora de crear nuestras aplicaciones en Angular hay situaciones en la que abandonar una ruta puede llevar asociada la pérdida de información no guardada, como por ejemplo en aquellas rutas en las que tengamos un formulario.

Esta situación puede resultar realmente frustrante para el usuario sobre todo si los cambios que había realizado el usuario eran numerosos.

En este artículo veremos cómo podemos prevenir esta situación utilizando la guarda CanDeactivate del Router.

Si lo prefieres, el contenido de este artículo también lo tienes en formato video aquí.


¿Qué es una guarda?

Lo primero que necesitamos es saber qué son las guardas. Las guardas son una especie de puntos de control que podemos añadir en las diferentes etapas del proceso de navegación del Router para permitir, detener o redireccionar la navegación en base a unos criterios que nosotros establezcamos.

En este caso, nos centraremos únicamente en la guarda del tipo canDeactivate, la cual se ejecuta justo antes de desmontar el componente asociado a la ruta en la cual hayamos aplicado la guarda.

const someRoutes: Routes = [
  ...,
  {
    path: ':id',
    component: SomeEditComponent, // <--
    canDeactivate: [withoutUnsavedChangesGuard],
  },
]
Enter fullscreen mode Exit fullscreen mode

¿Cómo crear una guarda?

Una guarda, en sí misma, es simplemente una función que puede devolver uno de tres valores posibles:

  • truepara permitir la navegación
  • falsepara detenerla
  • O un objeto UrlTree para redireccionar la navegación a una ruta distinta.


Hasta hace poco, la única forma de definir la lógica de las guardas, era como métodos de un servicio inyectable.

// deprecated v15.2+
@Injectable({ providedIn: 'root' })
export class MyGuard implements CanDeactivate<unknown> {

  canDeactivate(...): boolean | UrlTree | ... {
    //lógica de la guarda
  }

}
Enter fullscreen mode Exit fullscreen mode

Pero las guardas basadas en clases han sido recientemente marcadas como obsoletas en favor de las guardas funcionales introducidas en la versión 14.1, las cuales nos permiten definir directamente la lógica de nuestras guardas en una función independiente.

export const myGuard: CanDeactivateFn<unknown> = (...) => {
  //lógica de la guarda
}
Enter fullscreen mode Exit fullscreen mode

En los ejemplos de este artículo usaremos una guarda funcional, pero ese mismo código podría ser perfectamente definido dentro del método equivalente de un servicio en el caso de que estés usando una versión más antigua de Angular.


La Guarda CanDeactivate

En el caso concreto de la guarda CanDeactivate, la función de la misma podrá definir los siguientes 4 parámetros:

type CanDeactivateFn<T> = (
  component: T, 
  currentRoute: ActivatedRouteSnapshot, 
  currentState: RouterStateSnapshot, 
  nextState: RouterStateSnapshot
) => boolean | UrlTree | ...
Enter fullscreen mode Exit fullscreen mode
  • component: Donde recibirá la referencia del componente asociado a la ruta.
  • currentRoute: Con el snapshot de la ruta actual
  • currentState: Con el snapshot del estado del Router actual
  • nextState: Con el snapshot del estado del Router de la siguiente navegación solicitada

Ejemplo de ruta con Formulario

Visto esto y tomando como ejemplo una ruta asociada a un componente en el que tenemos un formulario.

// some.routes.ts
const someRoutes: Routes = [
  ...,
  {
    path: ':id',
    component: SomeEditComponent,
  },
]


// some-edit.component.ts
@Component(...)
export class SomeEditComponent {
  editForm!: FormGroup;

  ...

  // reseteamos el estado del formulario cuando
  // la info de este sea guardada correctamente.
  onSave(): void {
    const updatedInfo = this.editForm.getRawValue();

    this.someService
      .save(updatedInfo)
      .subscribe(() => this.editForm.reset(updatedInfo));
  }
}
Enter fullscreen mode Exit fullscreen mode

Si quisiéramos proteger contra el abandono accidental de dicha ruta cuando ese formulario tenga cambios sin guardar, podríamos por ejemplo definir una guarda CanDeactivate que compruebe la propiedad dirty del formulario para saber si ha sido modificado, y en el caso de que así sea, solicitar una confirmación por parte del usuario para permitir o no el abandono de la ruta.

export const withoutUnsavedChangesGuard: CanDeactivateFn<SomeEditComponent> = (component) => {

  // Si tiene cambios sin guardar, solicitamos confirmación
  if (component.editForm.dirty) {
    return confirm('¿Desea descartar los cambios?');
  }

  // si no tiene cambios sin guardar permitimos
  // directamente la navegación a la siguiente ruta.
  return true;
};
Enter fullscreen mode Exit fullscreen mode

Y una vez definida nuestra guarda funcional, ya lo único que tendríamos que hacer sería añadirla en el array de la propiedad canDeactivate de la ruta en cuestión y ya la tendríamos protegida contra el abandono accidental.

 const someRoutes: Routes = [
  ...,
  {
    path: ':id',
    component: SomeEditComponent,
    canDeactivate: [withoutUnsavedChangesGuard]
  },
]
Enter fullscreen mode Exit fullscreen mode

Crear una guarda Reutizable

La solución anterior funciona correctamente. Y si solo tenemos que proteger una única ruta en toda la aplicación, este tipo de implementación es suficiente.

El problema que tenemos eso sí, es que al vincular directamente la guarda con un tipo especifico de componente (CanDeactivateFn<SomeEditComponent>), si necesitáramos proteger múltiples rutas, tendríamos que o bien adaptar la guarda para que tenga en cuenta todos esos distintos escenarios aumentando así su complejidad o, en su defecto, crear una guarda específica para cada una de las rutas.

Una mejor opción para estos casos, es generalizar la guarda vinculándola contra una interfaz en vez de contra un tipo específico de componente.

Para este caso por ejemplo podríamos crear una interfaz HasUnsavedChangesque incluya un método homónimo cuyo tipo de retorno sea un booleano y vincular nuestra guarda a dicha interfaz.

export interface HasUnsavedChanges {
  hasUnsavedChanges(): boolean;
}

export const withoutUnsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
  ...
};
Enter fullscreen mode Exit fullscreen mode

Como ahora el parámetro component, deberá tener la referencia a un componente que implemente esta interfaz, en vez de chequear contra la propiedad dirty del formulario como estábamos haciendo anteriormente, tendremos ahora que llamar a ese método de la interfaz genérica para saber si el componente vinculado tiene cambios sin guardar o no.

 export const withoutUnsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
  if (component.hasUnsavedChanges()) { //<---
    return confirm('¿Desea descartar los cambios?');
  }

  return true;
};
Enter fullscreen mode Exit fullscreen mode

Y por último para ahora usar esta guarda en cualquiera de las rutas de nuestra aplicación, ya lo único que tendríamos que hacer sería implementar esta nueva interfaz en el componente asociado a la ruta que queremos proteger, definiendo en el método de dicha interfaz la lógica que determine si el componente tiene cambios sin guardar.

@Component(...)
export class SomeEditComponent implements HasUnsavedChanges{
  editForm!: FormGroup;

  ...

  hasUnsavedChanges(): boolean {
    return this.editForm.dirty;
  }
}
Enter fullscreen mode Exit fullscreen mode

EXTRA window:beforeunload

Con esto ya hemos visto el funcionamiento básico de la guarda CanDeactivate, pero antes de terminar es importante recalcar que el funcionamiento de esta guarda está limitado solo a la navegación interna de la aplicación. Esto quiere decir que la guarda que acabamos de implementar, no nos protegería si por ejemplo refrescáramos la página o cerráramos la pestaña actual del navegador.

Para este tipo de protección adicional tendríamos que apoyarnos en el evento beforeunload de la ventana del navegador.

Para ello podríamos por ejemplo añadir un @HostListener en el componente a proteger el cual haga uso del método hasUnsavedChanges que añadimos anteriormente.

@Component(...)
export class SomeEditComponent implements HasUnsavedChanges{
  editForm!: FormGroup;

  @HostListener('window:beforeunload', ['$event'])
  onUnloadHandler(e: BeforeUnloadEvent) {
    return this.hasUnsavedChanges() === false; 
  }

  ...

  hasUnsavedChanges(): boolean {
    return this.editForm.dirty;
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusión

Como hemos visto haciendo uso de la guarda CanDeactivate podremos proteger de una manera sencilla todas aquellas rutas de nuestra aplicación en las que un abandono accidental pudiera provocar una pérdida de información.


Si deseas apoyar la creación de más contenido de este tipo, puedes hacerlo a través nuestro Paypal


YouTube · Twitter · GitHub

💖 💪 🙅 🚩
akotech
akotech

Posted on June 14, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related