Avvisa l'utente delle modifiche non salvate prima di lasciare la pagina


112

Vorrei avvisare gli utenti delle modifiche non salvate prima che abbandonino una determinata pagina della mia app angular 2. Normalmente lo userei window.onbeforeunload, ma non funziona per le applicazioni a pagina singola.

Ho scoperto che nell'angolo 1, puoi agganciarti $locationChangeStartall'evento per lanciare una confirmscatola per l'utente, ma non ho visto nulla che mostri come farlo funzionare per l'angolo 2, o se quell'evento è ancora presente . Ho anche visto plugin per ag1 che forniscono funzionalità per onbeforeunload, ma ancora una volta, non ho visto alcun modo per usarlo per ag2.

Spero che qualcun altro abbia trovato una soluzione a questo problema; entrambi i metodi funzioneranno bene per i miei scopi.


2
Funziona per applicazioni a pagina singola, quando provi a chiudere la pagina / scheda. Quindi qualsiasi risposta alla domanda sarebbe solo una soluzione parziale se ignorassero questo fatto.
9ilsdx 9rvj 0lo

Risposte:


74

Il router fornisce una richiamata del ciclo di vita CanDeactivate

per maggiori dettagli vedere il tutorial delle guardie

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 {}

originale (router 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
A differenza di quanto richiesto dall'OP, CanDeactivate, attualmente, non si aggancia all'evento onbeforeunload (sfortunatamente). Significa che se l'utente cerca di navigare verso un URL esterno, chiudere la finestra, ecc. CanDeactivate non verrà attivato. Sembra funzionare solo quando l'utente rimane all'interno dell'app.
Christophe Vidal

1
@ChristopheVidal ha ragione. Si prega di consultare la mia risposta per una soluzione che copre anche la navigazione a un URL esterno, la chiusura della finestra, il ricaricamento della pagina, ecc.
stewdebaker

Funziona quando si cambia percorso. E se fosse SPA? Qualche altro modo per ottenere questo risultato?
sujay kodamala

stackoverflow.com/questions/36763141/… Avresti questo bisogno anche con le rotte. Se la finestra viene chiusa o se esci dal sito corrente canDeactivate, non funzionerà.
Günter Zöchbauer

214

Per coprire anche le protezioni contro gli aggiornamenti del browser, la chiusura della finestra, ecc. (Vedere il commento di @ ChristopheVidal alla risposta di Günter per i dettagli sul problema), ho trovato utile aggiungere il @HostListenerdecoratore canDeactivateall'implementazione della tua classe per ascoltare l' beforeunload windowevento. Se configurato correttamente, proteggerà dalla navigazione sia in-app che esterna allo stesso tempo.

Per esempio:

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
  }
}

Guardia:

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.');
  }
}

Itinerari:

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] },
];

Modulo:

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

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

NOTA : come ha sottolineato @JasperRisseeuw, IE ed Edge gestiscono l' beforeunloadevento in modo diverso dagli altri browser e includeranno la parola falsenella finestra di dialogo di conferma quando l' beforeunloadevento si attiva (ad esempio, il browser si aggiorna, chiude la finestra, ecc.). La navigazione all'interno dell'app Angular non viene modificata e mostrerà correttamente il messaggio di avviso di conferma designato. Coloro che hanno bisogno di supportare IE / Edge e non vogliono falsemostrare / volere un messaggio più dettagliato nella finestra di dialogo di conferma quando l' beforeunloadevento si attiva potrebbero anche voler vedere la risposta di @ JasperRisseeuw per una soluzione alternativa.


2
Funziona davvero bene @stewdebaker! Ho un'aggiunta a questa soluzione, vedi la mia risposta di seguito.
Jasper Risseeuw

1
import {Observable} da "rxjs / Observable"; non è presente in ComponentCanDeactivate
Mahmoud Ali Kassem

1
Ho dovuto aggiungere @Injectable()alla classe PendingChangesGuard. Inoltre, ho dovuto aggiungere PendingChangesGuard ai miei provider in@NgModule
spottedmahn il

Ho dovuto aggiungereimport { HostListener } from '@angular/core';
fidev

4
Vale la pena notare che è necessario restituire un valore booleano nel caso in cui vieni spostato beforeunload. Se restituisci un osservabile, non funzionerà. Si consiglia di cambiare l'interfaccia di qualcosa di simile canDeactivate: (internalNavigation: true | undefined)e chiamare il componente in questo modo: return component.canDeactivate(true). In questo modo, puoi controllare se non stai navigando internamente per tornare falseinvece di un osservabile.
jsgoupil

58

L'esempio con @Hostlistener di stewdebaker funziona davvero bene, ma ho apportato un'ultima modifica perché IE ed Edge visualizzano il "falso" restituito dal metodo canDeactivate () sulla classe MyComponent all'utente finale.

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
Buona cattura @JasperRisseeuw! Non mi rendevo conto che IE / Edge gestisse la cosa in modo diverso. Questa è una soluzione molto utile per coloro che hanno bisogno di supportare IE / Edge e non vogliono che venga falsevisualizzato nella finestra di dialogo di conferma. Ho apportato una piccola modifica alla tua risposta per includerla '$event'nell'annotazione @HostListener, poiché è necessaria per potervi accedere nella unloadNotificationfunzione.
stufato

1
Grazie, ho dimenticato di copiare ", ['$ event']" dal mio codice quindi buona cattura anche da te!
Jasper Risseeuw

l'unica soluzione che funziona è stata questa (utilizzando Edge). tutti gli altri funzionano ma mostrano solo il messaggio di dialogo predefinito (Chrome / Firefox), non il mio testo ... Ho persino posto una domanda per capire cosa sta succedendo
Elmer Dantas

@ElmerDantas leggi la mia risposta alla tua domanda per una spiegazione del motivo per cui il messaggio di dialogo predefinito viene visualizzato in Chrome / Firefox.
Stewdebaker

2
In realtà funziona, mi dispiace! Ho dovuto fare riferimento alla guardia nei fornitori di moduli.
tudor.iliescu

6

Ho implementato la soluzione da @stewdebaker che funziona molto bene, tuttavia volevo un bel popup di bootstrap invece della goffa conferma JavaScript standard. Supponendo che tu stia già utilizzando ngx-bootstrap, puoi usare la soluzione di @ stwedebaker, ma scambia il "Guard" con quello che sto mostrando qui. È inoltre necessario introdurre ngx-bootstrap/modale aggiungere un nuovo ConfirmationComponent:

Guardia

(sostituire 'confirm' con una funzione che aprirà un modal bootstrap, visualizzando un nuovo, personalizzato 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;
    })
  }
}

confirmation.component.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>

confirmation.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 poiché il nuovo ConfirmationComponentverrà visualizzato senza utilizzare un selectorin un modello html, deve essere dichiarato entryComponentsnella tua root app.module.ts(o qualunque cosa tu chiami il tuo modulo root). Apporta le seguenti modifiche a 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
c'è qualche possibilità di mostrare il modello personalizzato per l'aggiornamento del browser?
k11k2

Doveva esserci un modo, anche se questa soluzione andava bene per le mie esigenze. Se ho tempo svilupperò ulteriormente le cose, anche se non sarò in grado di aggiornare questa risposta per un bel po 'di tempo mi dispiace!
Chris Halcrow

1

La soluzione è stata più semplice del previsto, non utilizzare hrefperché non è gestita dalla routerLinkdirettiva di utilizzo di Angular Routing .


1

Risposta di giugno 2020:

Si noti che tutte le soluzioni proposte fino a questo punto non affrontano un significativo difetto noto con le canDeactivateprotezioni di Angular :

  1. L'utente fa clic sul pulsante "Indietro" nel browser, viene visualizzata la finestra di dialogo e l'utente fa clic su ANNULLA .
  2. L'utente fa di nuovo clic sul pulsante "Indietro", viene visualizzata la finestra di dialogo e l'utente fa clic su CONFERMA .
  3. Nota: l'utente viene riportato indietro 2 volte, il che potrebbe persino portarlo fuori dall'app del tutto :(

Questo è stato discusso qui , qui e qui a lungo


Si prega di consultare la mia soluzione al problema dimostrata qui che risolve in modo sicuro questo problema *. Questo è stato testato su Chrome, Firefox e Edge.


* AVVISO IMPORTANTE : in questa fase, quanto sopra cancellerà la cronologia in avanti quando si fa clic sul pulsante Indietro, ma preserverà la cronologia precedente. Questa soluzione non sarà appropriata se preservare la cronologia in avanti è vitale. Nel mio caso, di solito utilizzo un master-detail strategia di routing quando si tratta di moduli, quindi il mantenimento della cronologia in avanti non è importante.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.