Avise o usuário sobre alterações não salvas antes de sair da página


112

Gostaria de avisar os usuários sobre alterações não salvas antes de saírem de uma página específica do meu aplicativo angular 2. Normalmente eu usaria window.onbeforeunload, mas isso não funciona para aplicativos de página única.

Descobri que no angular 1, você pode conectar o $locationChangeStartevento para lançar uma confirmcaixa para o usuário, mas não vi nada que mostre como fazer isso funcionar para o angular 2, ou se esse evento ainda está presente . Também vi plug-ins para ag1 que fornecem funcionalidade para onbeforeunload, mas, novamente, não vi nenhuma maneira de usá-lo para ag2.

Espero que outra pessoa tenha encontrado uma solução para este problema; qualquer um dos métodos funcionará bem para meus propósitos.


2
Ele funciona para aplicativos de página única, quando você tenta fechar a página / guia. Portanto, qualquer resposta à pergunta seria apenas uma solução parcial se eles ignorassem esse fato.
9ilsdx 9rvj 0lo

Respostas:


74

O roteador fornece um retorno de chamada do ciclo de vida CanDeactivate

para mais detalhes veja o tutorial dos guardas

class UserToken {}
class Permissions {
  canActivate(user: UserToken, id: string): boolean {
    return true;
  }
}
@Injectable()
class CanActivateTeam implements CanActivate {
  constructor(private permissions: Permissions, private currentUser: UserToken) {}
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean>|Promise<boolean>|boolean {
    return this.permissions.canActivate(this.currentUser, route.params.id);
  }
}
@NgModule({
  imports: [
    RouterModule.forRoot([
      {
        path: 'team/:id',
        component: TeamCmp,
        canActivate: [CanActivateTeam]
      }
    ])
  ],
  providers: [CanActivateTeam, UserToken, Permissions]
})
class AppModule {}

original (roteador RC.x)

class CanActivateTeam implements CanActivate {
  constructor(private permissions: Permissions, private currentUser: UserToken) {}
  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable<boolean> {
    return this.permissions.canActivate(this.currentUser, this.route.params.id);
  }
}
bootstrap(AppComponent, [
  CanActivateTeam,
  provideRouter([{
    path: 'team/:id',
    component: Team,
    canActivate: [CanActivateTeam]
  }])
);

28
Ao contrário do que o OP pediu, CanDeactivate - atualmente - não está ligando no evento onbeforeunload (infelizmente). O que significa que se o usuário tentar navegar para uma URL externa, feche a janela, etc. CanDeactivate não será disparado. Parece funcionar apenas quando o usuário está dentro do aplicativo.
Christophe Vidal

1
@ChristopheVidal está correto. Por favor, veja minha resposta para uma solução que também abrange navegar para um URL externo, fechar a janela, recarregar a página etc.
stewdebaker

Isso funciona ao alterar as rotas. E se for SPA? Alguma outra maneira de conseguir isso?
sujay kodamala

stackoverflow.com/questions/36763141/… Você faria isso com as rotas também. Se a janela estiver fechada ou navegada para fora do site atual canDeactivate, não funcionará.
Günter Zöchbauer

214

Para cobrir também as proteções contra atualizações do navegador, fechamento de janela, etc. (consulte o comentário de @ChristopheVidal à resposta de Günter para obter detalhes sobre o problema), achei útil adicionar o @HostListenerdecorador à canDeactivateimplementação de sua classe para ouvir o beforeunload windowevento. Quando configurado corretamente, isso protegerá contra navegação interna e externa ao mesmo tempo.

Por exemplo:

Componente:

import { ComponentCanDeactivate } from './pending-changes.guard';
import { HostListener } from '@angular/core';
import { Observable } from 'rxjs/Observable';

export class MyComponent implements ComponentCanDeactivate {
  // @HostListener allows us to also guard against browser refresh, close, etc.
  @HostListener('window:beforeunload')
  canDeactivate(): Observable<boolean> | boolean {
    // insert logic to check if there are pending changes here;
    // returning true will navigate without confirmation
    // returning false will show a confirm dialog before navigating away
  }
}

Guarda:

import { CanDeactivate } from '@angular/router';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';

export interface ComponentCanDeactivate {
  canDeactivate: () => boolean | Observable<boolean>;
}

@Injectable()
export class PendingChangesGuard implements CanDeactivate<ComponentCanDeactivate> {
  canDeactivate(component: ComponentCanDeactivate): boolean | Observable<boolean> {
    // if there are no pending changes, just allow deactivation; else confirm first
    return component.canDeactivate() ?
      true :
      // NOTE: this warning message will only be shown when navigating elsewhere within your angular app;
      // when navigating away from your angular app, the browser will show a generic warning message
      // see http://stackoverflow.com/a/42207299/7307355
      confirm('WARNING: You have unsaved changes. Press Cancel to go back and save these changes, or OK to lose these changes.');
  }
}

Rotas:

import { PendingChangesGuard } from './pending-changes.guard';
import { MyComponent } from './my.component';
import { Routes } from '@angular/router';

export const MY_ROUTES: Routes = [
  { path: '', component: MyComponent, canDeactivate: [PendingChangesGuard] },
];

Módulo:

import { PendingChangesGuard } from './pending-changes.guard';
import { NgModule } from '@angular/core';

@NgModule({
  // ...
  providers: [PendingChangesGuard],
  // ...
})
export class AppModule {}

NOTA : Como @JasperRisseeuw apontou, o IE e o Edge tratam o beforeunloadevento de maneira diferente de outros navegadores e incluirão a palavra falsena caixa de diálogo de confirmação quando o beforeunloadevento for ativado (por exemplo, o navegador é atualizado, fechando a janela, etc.). A navegação no aplicativo Angular não é afetada e mostrará corretamente a mensagem de aviso de confirmação designada. Aqueles que precisam suportar o IE / Edge e não querem falsemostrar / querem uma mensagem mais detalhada na caixa de diálogo de confirmação quando o beforeunloadevento é ativado também podem querer ver a resposta de @JasperRisseeuw para uma solução alternativa.


2
Isso funciona muito bem, @stewdebaker! Eu tenho um acréscimo a esta solução, veja minha resposta abaixo.
Jasper Risseeuw

1
import {Observable} de 'rxjs / Observable'; está faltando em ComponentCanDeactivate
Mahmoud Ali Kassem

1
Eu tive que adicionar @Injectable()à classe PendingChangesGuard. Além disso, tive que adicionar PendingChangesGuard aos meus provedores em@NgModule
spottedmahn

Eu tive que adicionarimport { HostListener } from '@angular/core';
fidev

4
É importante notar que você deve retornar um booleano caso esteja sendo navegado com ele beforeunload. Se você retornar um Observable, não funcionará. Você pode querer mudar a sua interface para algo como canDeactivate: (internalNavigation: true | undefined)e chamar seu componente como este: return component.canDeactivate(true). Dessa forma, você pode verificar se não está navegando internamente para retornar ao falseinvés de um Observável.
jsgoupil

58

O exemplo com o @Hostlistener de stewdebaker funciona muito bem, mas fiz mais uma alteração porque o IE e o Edge exibem o "false" que é retornado pelo método canDeactivate () na classe MyComponent para o usuário final.

Componente:

import {ComponentCanDeactivate} from "./pending-changes.guard";
import { Observable } from 'rxjs'; // add this line

export class MyComponent implements ComponentCanDeactivate {

  canDeactivate(): Observable<boolean> | boolean {
    // insert logic to check if there are pending changes here;
    // returning true will navigate without confirmation
    // returning false will show a confirm alert before navigating away
  }

  // @HostListener allows us to also guard against browser refresh, close, etc.
  @HostListener('window:beforeunload', ['$event'])
  unloadNotification($event: any) {
    if (!this.canDeactivate()) {
        $event.returnValue = "This message is displayed to the user in IE and Edge when they navigate without using Angular routing (type another URL/close the browser/etc)";
    }
  }
}

2
Boa captura @JasperRisseeuw! Não sabia que o IE / Edge lidava com isso de forma diferente. Esta é uma solução muito útil para aqueles que precisam suportar o IE / Edge e não querem falseque apareça na caixa de diálogo de confirmação. Fiz uma pequena edição em sua resposta para incluir o '$event'na @HostListeneranotação, pois isso é necessário para poder acessá-lo na unloadNotificationfunção.
stewdebaker

1
Obrigado, esqueci de copiar ", ['$ event']" do meu próprio código, então você também pode ler bem!
Jasper Risseeuw

a única solução que deu certo foi essa (usando o Edge). todos os outros funcionam, mas mostra apenas a mensagem de diálogo padrão (Chrome / Firefox), não o meu texto ... Eu até fiz uma pergunta para entender o que está acontecendo
Elmer Dantas

@ElmerDantas, consulte minha resposta à sua pergunta para obter uma explicação de por que a mensagem de diálogo padrão é exibida no Chrome / Firefox.
stewdebaker

2
Na verdade, funciona, desculpe! Tive que fazer referência ao guarda nos provedores de módulo.
tudor.iliescu

6

Eu implementei a solução de @stewdebaker que funciona muito bem, no entanto, eu queria um pop-up de bootstrap legal em vez da confirmação desajeitada do JavaScript padrão. Supondo que você já esteja usando o ngx-bootstrap, você pode usar a solução de @stwedebaker, mas troque o 'Guarda' pelo que estou mostrando aqui. Você também precisa apresentar ngx-bootstrap/modale adicionar um novo ConfirmationComponent:

Guarda

(substitua 'confirmar' por uma função que abrirá um modal de bootstrap - exibindo um novo e personalizado ConfirmationComponent):

import { Component, OnInit } from '@angular/core';
import { ConfirmationComponent } from './confirmation.component';

import { CanDeactivate } from '@angular/router';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { BsModalService } from 'ngx-bootstrap/modal';
import { BsModalRef } from 'ngx-bootstrap/modal';

export interface ComponentCanDeactivate {
  canDeactivate: () => boolean | Observable<boolean>;
}

@Injectable()
export class PendingChangesGuard implements CanDeactivate<ComponentCanDeactivate> {

  modalRef: BsModalRef;

  constructor(private modalService: BsModalService) {};

  canDeactivate(component: ComponentCanDeactivate): boolean | Observable<boolean> {
    // if there are no pending changes, just allow deactivation; else confirm first
    return component.canDeactivate() ?
      true :
      // NOTE: this warning message will only be shown when navigating elsewhere within your angular app;
      // when navigating away from your angular app, the browser will show a generic warning message
      // see http://stackoverflow.com/a/42207299/7307355
      this.openConfirmDialog();
  }

  openConfirmDialog() {
    this.modalRef = this.modalService.show(ConfirmationComponent);
    return this.modalRef.content.onClose.map(result => {
        return result;
    })
  }
}

confirmação.componente.html

<div class="alert-box">
    <div class="modal-header">
        <h4 class="modal-title">Unsaved changes</h4>
    </div>
    <div class="modal-body">
        Navigate away and lose them?
    </div>
    <div class="modal-footer">
        <button type="button" class="btn btn-secondary" (click)="onConfirm()">Yes</button>
        <button type="button" class="btn btn-secondary" (click)="onCancel()">No</button>        
    </div>
</div>

confirm.component.ts

import { Component } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { BsModalRef } from 'ngx-bootstrap/modal';

@Component({
    templateUrl: './confirmation.component.html'
})
export class ConfirmationComponent {

    public onClose: Subject<boolean>;

    constructor(private _bsModalRef: BsModalRef) {

    }

    public ngOnInit(): void {
        this.onClose = new Subject();
    }

    public onConfirm(): void {
        this.onClose.next(true);
        this._bsModalRef.hide();
    }

    public onCancel(): void {
        this.onClose.next(false);
        this._bsModalRef.hide();
    }
}

E uma vez que o novo ConfirmationComponentserá exibido sem usar um selectorem um modelo html, ele precisa ser declarado entryComponentsem sua raiz app.module.ts(ou qualquer que seja o nome do seu módulo raiz). Faça as seguintes alterações em app.module.ts:

app.module.ts

import { ModalModule } from 'ngx-bootstrap/modal';
import { ConfirmationComponent } from './confirmation.component';

@NgModule({
  declarations: [
     ...
     ConfirmationComponent
  ],
  imports: [
     ...
     ModalModule.forRoot()
  ],
  entryComponents: [ConfirmationComponent]

1
há alguma chance de mostrar o modelo personalizado para atualização do navegador?
k11k2

Deve haver uma maneira, embora essa solução fosse adequada para minhas necessidades. Se eu der tempo, vou desenvolver mais coisas, embora não seja capaz de atualizar esta resposta por um bom tempo, desculpe!
Chris Halcrow

1

A solução foi mais fácil do que o esperado, não use hrefporque isso não é tratado pela routerLinkdiretiva de uso de Roteamento Angular .


1

Resposta de junho de 2020:

Observe que todas as soluções propostas até este ponto não lidam com uma falha significativa conhecida com os canDeactivateguardas da Angular :

  1. O usuário clica no botão 'voltar' no navegador, a caixa de diálogo é exibida e o usuário clica em CANCELAR .
  2. O usuário clica no botão 'voltar' novamente, a caixa de diálogo é exibida e o usuário clica em CONFIRMAR .
  3. Observação: o usuário navega para trás 2 vezes, o que pode até mesmo retirá-lo do aplicativo :(

Isso foi discutido aqui , aqui e longamente aqui


Consulte minha solução para o problema demonstrado aqui, que contorna esse problema com segurança *. Isso foi testado no Chrome, Firefox e Edge.


* AVISO IMPORTANTE : Neste estágio, o procedimento acima limpará o histórico de encaminhamento quando o botão Voltar for clicado, mas preservará o histórico de retorno. Esta solução não será apropriada se preservar seu histórico de encaminhamento for vital. No meu caso, normalmente uso uma estratégia de roteamento de detalhes mestres quando se trata de formulários, portanto, manter o histórico de encaminhamento não é importante.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.