Cómo prevenir el abandono accidental de una ruta gracias a la guarda canDeactivate
Tags: angular, webdev, spanish
Published: 2023-06-14 12:36:25

A la hora de crear nuestras aplicaciones en Angular hay <u>situaciones en la que abandonar una ruta puede llevar asociada la pérdida de información no guardada</u>, 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í._ {% embed https://youtu.be/dZ-k2Z2kYsY %}

---

## ¿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.

```typescript
const someRoutes: Routes = [
  ...,
  {
    path: ':id',
    component: SomeEditComponent, // <--
    canDeactivate: [withoutUnsavedChangesGuard],
  },
]
```

---

## ¿Cómo crear una guarda?
Una guarda, en sí misma, <u>es simplemente una función</u> 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. 

```typescript
// 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. 

```typescript
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**:

```typescript
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.

```typescript
// 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.

```typescript
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.

```typescript
 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. 

```typescript
export interface HasUnsavedChanges {
  hasUnsavedChanges(): boolean;
}

export const withoutUnsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
  ...
};
```
Como ahora el parámetro `component`, <u>deberá tener la referencia a un componente que implemente esta interfaz</u>, 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.

```typescript
 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.

```typescript
@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, <u>no nos protegería si por ejemplo refrescáramos la página o cerráramos la pestaña actual del navegador.</u>

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.

```typescript
@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](https://www.paypal.com/donate/?hosted_button_id=8HJNHL8VTB8ZG)_

---

[YouTube](https://youtube.com/@akotech) · [Twitter](https://twitter.com/_akotech) · [GitHub](https://github.com/ako-tech)