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],
},
]
¿Cómo crear una guarda?
Una guarda, en sí misma, es simplemente una función que puede devolver uno de tres valores posibles:
-
true
para permitir la navegación -
false
para 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
}
}
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
}
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 | ...
-
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));
}
}
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;
};
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]
},
]
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 HasUnsavedChanges
que 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) => {
...
};
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;
};
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;
}
}
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;
}
}
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
Top comments (0)