Warnen Sie den Benutzer vor nicht gespeicherten Änderungen, bevor Sie die Seite verlassen

110

Ich möchte Benutzer vor nicht gespeicherten Änderungen warnen, bevor sie eine bestimmte Seite meiner Angular 2-App verlassen. Normalerweise würde ich verwenden window.onbeforeunload, aber das funktioniert nicht für Anwendungen mit nur einer Seite.

Ich habe festgestellt, dass Sie sich in Winkel 1 an das $locationChangeStartEreignis anschließen können, um eine confirmBox für den Benutzer zu erstellen, aber ich habe nichts gesehen, was zeigt, wie dies für Winkel 2 funktioniert, oder ob dieses Ereignis überhaupt noch vorhanden ist . Ich habe auch Plugins für ag1 gesehen, die Funktionen für ag2 bereitstellen onbeforeunload, aber ich habe auch keine Möglichkeit gesehen, sie für ag2 zu verwenden.

Ich hoffe, jemand anderes hat eine Lösung für dieses Problem gefunden. Beide Methoden funktionieren für meine Zwecke einwandfrei.

Whelch
quelle
2
Dies funktioniert für Anwendungen mit nur einer Seite, wenn Sie versuchen, die Seite / Registerkarte zu schließen. Antworten auf die Frage wären also nur eine Teillösung, wenn sie diese Tatsache ignorieren.
9ilsdx 9rvj 0lo

Antworten:

73

Der Router bietet einen Lebenszyklus-Rückruf CanDeactivate

Weitere Informationen finden Sie im Tutorial für Wachen

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 (RC.x Router)

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]
  }])
);
Günter Zöchbauer
quelle
28
Im Gegensatz zu den Anforderungen des OP ist CanDeactivate derzeit (leider) nicht an das Ereignis onbeforeunload gebunden. Das heißt, wenn der Benutzer versucht, zu einer externen URL zu navigieren, das Fenster usw. zu schließen, wird CanDeactivate nicht ausgelöst. Es scheint nur zu funktionieren, wenn der Benutzer in der App bleibt.
Christophe Vidal
1
@ChristopheVidal ist korrekt. In meiner Antwort finden Sie eine Lösung, die auch das Navigieren zu einer externen URL, das Schließen des Fensters, das Neuladen der Seite usw. umfasst
stewdebaker
Dies funktioniert beim Ändern von Routen. Was ist, wenn es SPA ist? Gibt es einen anderen Weg, um dies zu erreichen?
Sujay Kodamala
stackoverflow.com/questions/36763141/… Dies benötigen Sie auch für Routen. Wenn das Fenster geschlossen oder von der aktuellen Site weg navigiert canDeactivatewird, funktioniert dies nicht.
Günter Zöchbauer
212

Ich habe es als hilfreich empfunden, den @HostListenerDekorator zur canDeactivateImplementierung Ihrer Klasse hinzuzufügen , um auf das beforeunload windowEreignis zu warten, um auch Schutz vor Browseraktualisierungen, Schließen des Fensters usw. abzudecken (siehe @ ChristopheVidals Kommentar zu Günters Antwort für Details zu diesem Problem) . Bei korrekter Konfiguration schützt dies gleichzeitig vor In-App- und externer Navigation.

Beispielsweise:

Komponente:

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

Bewachen:

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

Routen:

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

Modul:

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

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

HINWEIS : Wie @JasperRisseeuw hervorhob, behandeln IE und Edge das beforeunloadEreignis anders als andere Browser und fügen das Wort falsein den Bestätigungsdialog ein, wenn das beforeunloadEreignis aktiviert wird (z. B. Aktualisieren des Browsers, Schließen des Fensters usw.). Das Navigieren innerhalb der Angular-App bleibt davon unberührt und zeigt die angegebene Bestätigungswarnmeldung ordnungsgemäß an. Diejenigen, die IE / Edge unterstützen müssen und bei Aktivierung falsedes beforeunloadEreignisses keine detailliertere Meldung im Bestätigungsdialog anzeigen / möchten, möchten möglicherweise auch die Antwort von @ JasperRisseeuw für eine Problemumgehung sehen.

Stewdebaker
quelle
2
Das funktioniert wirklich gut @stewdebaker! Ich habe eine Ergänzung zu dieser Lösung, siehe meine Antwort unten.
Jasper Risseeuw
1
{Observable} aus 'rxjs / Observable' importieren; fehlt in ComponentCanDeactivate
Mahmoud Ali Kassem
1
Ich musste @Injectable()der PendingChangesGuard-Klasse hinzufügen . Außerdem musste ich PendingChangesGuard zu meinen Providern hinzufügen@NgModule
inottedmahn
Ich musste hinzufügenimport { HostListener } from '@angular/core';
fidev
4
Es ist zu beachten, dass Sie einen Booleschen Wert zurückgeben müssen, falls Sie mit navigiert werden beforeunload. Wenn Sie ein Observable zurückgeben, funktioniert es nicht. Möglicherweise möchten Sie Ihre Benutzeroberfläche für Folgendes ändern canDeactivate: (internalNavigation: true | undefined)und Ihre Komponente folgendermaßen aufrufen : return component.canDeactivate(true). Auf diese Weise können Sie überprüfen, ob Sie nicht intern weg navigieren, um falseanstelle eines Observable zurückzukehren.
Jsgoupil
58

Das Beispiel mit dem @ Hostlistener von stewdebaker funktioniert sehr gut, aber ich habe noch eine Änderung daran vorgenommen, da IE und Edge das "false" anzeigen, das von der canDeactivate () -Methode in der MyComponent-Klasse an den Endbenutzer zurückgegeben wird.

Komponente:

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)";
    }
  }
}
Jasper Risseeuw
quelle
2
Guter Fang @JasperRisseeuw! Mir war nicht klar, dass IE / Edge dies anders handhabte. Dies ist eine sehr nützliche Lösung für diejenigen, die IE / Edge unterstützen müssen und nicht möchten, dass das falseim Bestätigungsdialogfeld angezeigt wird. Ich habe Ihre Antwort geringfügig bearbeitet, um sie '$event'in die @HostListenerAnmerkung aufzunehmen, da dies erforderlich ist, um in der unloadNotificationFunktion darauf zugreifen zu können .
Stewdebaker
1
Danke, ich habe vergessen, ", ['$ event']" aus meinem eigenen Code zu kopieren, also auch ein guter Fang von dir!
Jasper Risseeuw
Die einzige Lösung, die funktioniert, war diese (mit Edge). Alle anderen funktionieren, zeigen aber nur die Standarddialogmeldung (Chrome / Firefox), nicht meinen Text ... Ich habe sogar eine Frage gestellt, um zu verstehen, was passiert
Elmer Dantas
@ElmerDantas In meiner Antwort auf Ihre Frage finden Sie eine Erklärung, warum die Standarddialogmeldung in Chrome / Firefox angezeigt wird.
Stewdebaker
2
Eigentlich funktioniert es, sorry! Ich musste den Wachmann in den Modulanbietern referenzieren.
tudor.iliescu
6

Ich habe die Lösung von @stewdebaker implementiert, die wirklich gut funktioniert, aber ich wollte ein nettes Bootstrap-Popup anstelle der klobigen Standard-JavaScript-Bestätigung. Angenommen, Sie verwenden bereits ngx-bootstrap, können Sie die Lösung von @ stwedebaker verwenden, aber tauschen Sie den 'Guard' gegen den hier gezeigten. Sie müssen ngx-bootstrap/modalauch ein neues hinzufügen und hinzufügen ConfirmationComponent:

Bewachen

(Ersetzen Sie 'Bestätigen' durch eine Funktion, die ein Bootstrap-Modal öffnet und ein neues, benutzerdefiniertes anzeigt. 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;
    })
  }
}

bestätigung.Komponente.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>

Bestätigung.Komponente.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();
    }
}

Und da das Neue ConfirmationComponentohne Verwendung eines selectorin einer HTML-Vorlage angezeigt wird , muss es entryComponentsin Ihrem Root app.module.ts(oder wie auch immer Sie Ihr Root-Modul nennen) deklariert sein . Nehmen Sie folgende Änderungen vor 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]
Chris Halcrow
quelle
1
Gibt es eine Möglichkeit, ein benutzerdefiniertes Modell für die Browseraktualisierung anzuzeigen?
k11k2
Es muss einen Weg geben, obwohl diese Lösung für meine Bedürfnisse in Ordnung war. Wenn ich Zeit habe, werde ich die Dinge weiterentwickeln, obwohl ich diese Antwort für eine Weile nicht aktualisieren kann, sorry!
Chris Halcrow
1

Die Lösung war einfacher als erwartet. Verwenden Sie sie nicht, hrefda dies nicht von der Angular Routing- routerLinkDirektive use verwendet wird.

Byron Lopez
quelle
1

Antwort vom Juni 2020:

Beachten Sie, dass alle bis zu diesem Zeitpunkt vorgeschlagenen Lösungen keinen wesentlichen bekannten Fehler bei Angulars canDeactivateWachen behandeln:

  1. Der Benutzer klickt im Browser auf die Schaltfläche "Zurück", das Dialogfeld wird angezeigt und der Benutzer klickt auf " ABBRECHEN" .
  2. Der Benutzer klickt erneut auf die Schaltfläche "Zurück", das Dialogfeld wird angezeigt und der Benutzer klickt auf BESTÄTIGEN .
  3. Hinweis: Der Benutzer wird zweimal zurück navigiert, wodurch er möglicherweise sogar vollständig aus der App entfernt wird :(

Dies wurde hier , hier und ausführlich hier diskutiert


Bitte lesen Sie meine Lösung für das hier gezeigte Problem , die dieses Problem sicher umgeht *. Dies wurde auf Chrome, Firefox und Edge getestet.


* WICHTIGE CAVEAT : In diesem Stadium wird der Vorwärtsverlauf gelöscht , wenn auf die Schaltfläche "Zurück" geklickt wird. Der Rückwärtsverlauf wird jedoch beibehalten. Diese Lösung ist nicht geeignet, wenn die Aufbewahrung Ihrer Vorwärtshistorie von entscheidender Bedeutung ist. In meinem Fall verwende ich normalerweise eine Master-Detail- Routing-Strategie, wenn es um Formulare geht. Daher ist es nicht wichtig, den Vorwärtsverlauf beizubehalten.

Stephen Paul
quelle