CanDeactivate: Confirmando se o usuário deseja abandonar a página e descartar alterações

felipedsc

Felipe Carvalho

Posted on September 25, 2019

CanDeactivate: Confirmando se o usuário deseja abandonar a página e descartar alterações

Introdução

Você já deve ter se deparado com algum pop-up informando que o formulário possui alterações e que se abandonar a página, elas serão descartadas.
Esquecer de salvar um formulário é relativamente comum, então, neste post demonstrarei uma implementação simples com uma funcionalidade que o próprio Angular disponibiliza, o CanDeactivate.


Resumo

Criaremos um guard que será aplicado a uma rota. Este guard terá uma interface que deverá ser implementada pelo nosso componente e essa implementação deverá retornar um true quando o usuário puder deixar a página e false para o contrário. Simples, não?


Vamos ao exemplo

O exemplo consiste em um formulário e uma navegação entre dois links. Quando o usuário tiver modificado algo no formulário e não salvá-lo e tentar ir para "Outra página", uma confirmação será emitida.

Criação do Guard

OnComponentDeactivate é a interface que os componentes implementarão, ela poderá retornar um boolean de forma síncrona ou assíncrona, ou um UrlTree.

import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';

export interface OnComponentDeactivate {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean | UrlTree;
}

export class CanDeactivateGuard implements CanDeactivate<OnComponentDeactivate> {
  constructor() { }

  canDeactivate(
    component: OnComponentDeactivate,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState: RouterStateSnapshot
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return component.canDeactivate();
  }
}
Enter fullscreen mode Exit fullscreen mode

Obs.: Não é muito comum usar UrlTree com o CanDeactivate. Não acredito que isso seja o comportamento esperado, mas, retornando um UrlTree, o Angular tenta novamente "desativar" a página atual, o que acaba gerando um loop infinito. A única solução que encontrei, foi com a seguinte verificação dentro do método canDeactivate:

        const response = component.canDeactivate();

        if (response instanceof UrlTree 
            && nextState.url == response.toString()) {
            return true;
        }

        return  response;
Enter fullscreen mode Exit fullscreen mode

Incluindo o Guard nos providers

Esse Guard funcionará como um serviço e deverá estar na seção providers. Meu app.module ficou da seguinte forma:

@NgModule({
  declarations: [
    AppComponent,
    FormularioComponent,
    OutraPaginaComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ReactiveFormsModule
  ],
  providers: [
    CanDeactivateGuard
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

Aplicando o Guard à rota

Estamos quase lá. Também devemos definir quais rotas acionarão o CanDeactivateGuard. Em meu app-routing.module.ts, defini que o meu componente de formulário irá acioná-lo:

const routes: Routes = [
  { path: "", redirectTo: "/formulario", pathMatch: "full" },
  { path: "formulario", component: FormularioComponent, canDeactivate: [CanDeactivateGuard] },
  { path: "outra", component: OutraPaginaComponent }
];
Enter fullscreen mode Exit fullscreen mode

Implementando a interface

Agora só resta implementar a interface em nosso componente. Neste exemplo, utilizei o método confirm do javascript, que após a resposta do usuário, retornará um boolean. Nada impede que você utilize uma modal mais bonita ou aplique qualquer outro tipo de lógica para permitir ou não a desativação do seu componente, desde que forneça um dos retornos esperados.
Verifiquei se o usuário fez alguma alteração no formulário através da propriedade dirty. Quando o formulário for salvo, eu reseto esse estado através do markAsPristine() e usuário não precisará mais confirmar se deseja sair da página:

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { OnComponentDeactivate } from '../can-deactivate.guard';
import { Observable } from 'rxjs';
import { UrlTree } from '@angular/router';

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

  form: FormGroup;

  constructor(private router: Router) { }

  ngOnInit() {
    this.form = new FormGroup({
      "nome": new FormControl(null, [Validators.required, Validators.minLength(3)])
    });
  }

  salvar() {
    this.form.markAsPristine();
  }

  canDeactivate(): boolean | Observable<boolean> | Promise<boolean> | UrlTree {
    if (this.form.dirty) {
      return confirm("As alterações no formulário não foram salvas e serão descartadas, deseja prosseguir?");
    } else {
      return true;
    }
  };
}

Enter fullscreen mode Exit fullscreen mode

Vamos ver funcionando?

Insira qualquer coisa no campo do formulário e tente navegar para "Outra página".
Caso não esteja vendo o exemplo abaixo, clique aqui.

💖 💪 🙅 🚩
felipedsc
Felipe Carvalho

Posted on September 25, 2019

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

Sign up to receive the latest update from our blog.

Related

Angular Form Array
angular Angular Form Array

November 29, 2024

Can a Solo Developer Build a SaaS App?
undefined Can a Solo Developer Build a SaaS App?

November 29, 2024

Angular's New Feature: Signals
javascript Angular's New Feature: Signals

November 29, 2024