Ostrzegaj użytkownika o niezapisanych zmianach przed opuszczeniem strony

112

Chciałbym ostrzec użytkowników o niezapisanych zmianach, zanim opuszczą określoną stronę mojej aplikacji Angular 2. Zwykle bym używał window.onbeforeunload, ale to nie działa w przypadku aplikacji jednostronicowych.

Odkryłem, że w angular 1 możesz podłączyć się do $locationChangeStartzdarzenia, aby rzucić confirmpudełko dla użytkownika, ale nie widziałem niczego, co pokazuje, jak to działa dla angular 2, lub jeśli to zdarzenie jest nadal obecne . Widziałem również wtyczki dla ag1, które zapewniają funkcjonalność dla onbeforeunload, ale znowu nie widziałem sposobu, aby użyć go dla ag2.

Mam nadzieję, że ktoś inny znalazł rozwiązanie tego problemu; każda metoda będzie dobrze działać dla moich celów.

Whelch
źródło
2
Działa w przypadku aplikacji jednostronicowych, gdy próbujesz zamknąć stronę / kartę. Zatem wszelkie odpowiedzi na to pytanie byłyby tylko częściowym rozwiązaniem, gdyby zignorowali ten fakt.
9ilsdx 9rvj 0lo

Odpowiedzi:

74

Router zapewnia funkcję zwrotną dotyczącą cyklu życia CanDeactivate

więcej szczegółów znajdziesz w samouczku strażników

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

oryginalny (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]
  }])
);
Günter Zöchbauer
źródło
29
W przeciwieństwie do tego, o co prosił OP, CanDeactivate -currently- nie przechwytuje zdarzenia onbeforeunload (niestety). Oznacza to, że jeśli użytkownik spróbuje przejść do zewnętrznego adresu URL, zamknij okno itp. CanDeactivate nie zostanie wywołane. Wydaje się, że działa tylko wtedy, gdy użytkownik pozostaje w aplikacji.
Christophe Vidal
1
@ChristopheVidal ma rację. Zobacz moją odpowiedź na rozwiązanie, które obejmuje również przechodzenie do zewnętrznego adresu URL, zamykanie okna, ponowne ładowanie strony itp.
stewdebaker
Działa to przy zmianie trasy. A jeśli to SPA? Czy jest jakiś inny sposób, aby to osiągnąć?
sujay kodamala
stackoverflow.com/questions/36763141/… Przydałbyś się również w przypadku tras. Jeśli okno jest zamknięte lub opuszczone z bieżącej witryny canDeactivate, nie będzie działać.
Günter Zöchbauer
214

Aby zabezpieczyć się przed odświeżeniem przeglądarki, zamknięciem okna itp. (Zobacz komentarz @ ChristopheVidal do odpowiedzi Güntera, aby uzyskać szczegółowe informacje na temat tego problemu), uważam, że pomocne jest dodanie @HostListenerdekoratora do canDeactivateimplementacji klasy, aby nasłuchiwaćbeforeunload window wydarzenia. Po prawidłowym skonfigurowaniu chroni to jednocześnie przed nawigacją w aplikacji i zewnętrzną.

Na przykład:

Składnik:

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

Strzec:

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

Trasy:

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

Moduł:

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

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

UWAGA : Jak zauważył @JasperRisseeuw, IE i Edge obsługują beforeunloadzdarzenie inaczej niż inne przeglądarki i dołączają słowo falsew oknie dialogowym potwierdzenia, gdy beforeunloadzdarzenie się aktywuje (np. Przeglądarka odświeża się, zamyka okno itp.). Nawigacja w aplikacji Angular nie ma wpływu i prawidłowo wyświetla wyznaczony komunikat ostrzegawczy z potwierdzeniem. Ci, którzy muszą obsługiwać IE / Edge i nie chcą falsepokazywać / chcą bardziej szczegółowego komunikatu w oknie dialogowym potwierdzenia, gdy beforeunloadzdarzenie się aktywuje, mogą również chcieć zobaczyć odpowiedź @ JasperRisseeuw na obejście.

stewdebaker
źródło
2
To działa naprawdę dobrze @stewdebaker! Mam jeden dodatek do tego rozwiązania, zobacz moją odpowiedź poniżej.
Jasper Risseeuw
1
import {Observable} z 'rxjs / Observable'; brakuje w ComponentCanDeactivate
Mahmoud Ali Kassem
1
Musiałem dodać @Injectable()do klasy PendingChangesGuard. Ponadto musiałem dodać PendingChangesGuard do moich dostawców w@NgModule
spottedmahn
Musiałem dodaćimport { HostListener } from '@angular/core';
fidev
4
Warto zauważyć, że musisz zwrócić wartość logiczną na wypadek, gdybyś był nawigowany beforeunload. Jeśli zwrócisz Observable, to nie zadziała. Możesz zmienić interfejs na coś takiego canDeactivate: (internalNavigation: true | undefined)i zadzwonić do komponentu tak: return component.canDeactivate(true). W ten sposób możesz sprawdzić, czy nie oddalasz się wewnętrznie, aby powrócić falsezamiast Obserwowalnego.
jsgoupil
58

Przykład z @Hostlistener ze stewdebaker działa naprawdę dobrze, ale wprowadziłem jeszcze jedną zmianę, ponieważ IE i Edge wyświetlają "false", które jest zwracane przez metodę canDeactivate () w klasie MyComponent do użytkownika końcowego.

Składnik:

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
źródło
2
Dobry chwyt @JasperRisseeuw! Nie zdawałem sobie sprawy, że IE / Edge radzi sobie z tym inaczej. Jest to bardzo przydatne rozwiązanie dla tych, którzy muszą obsługiwać IE / Edge i nie chcą, falseaby pojawiały się w oknie dialogowym potwierdzenia. Dokonałem niewielkiej zmiany w Twojej odpowiedzi, aby dołączyć adnotację '$event'do @HostListeneradnotacji, ponieważ jest to wymagane, aby mieć do niej dostęp w unloadNotificationfunkcji.
stewdebaker
1
Dzięki, zapomniałem skopiować ", ['$ event']" z mojego własnego kodu, więc również od Ciebie niezły chwyt!
Jasper Risseeuw
jedynym rozwiązaniem, które działa, było to (przy użyciu Edge). wszystkie inne działają, ale pokazuje tylko domyślny komunikat w oknie dialogowym (Chrome / Firefox), a nie mój tekst ... Zadałem nawet pytanie, aby zrozumieć, co się dzieje
Elmer Dantas
@ElmerDantas, zobacz moją odpowiedź na swoje pytanie, aby wyjaśnić, dlaczego domyślny komunikat w oknie dialogowym wyświetla się w przeglądarce Chrome / Firefox.
stewdebaker
2
Właściwie to działa, przepraszam! Musiałem odwołać się do strażnika w dostawcach modułów.
tudor.iliescu
6

Zaimplementowałem rozwiązanie z @stewdebaker, które działa naprawdę dobrze, jednak chciałem mieć ładne okienko bootstrap zamiast niezgrabnego standardowego potwierdzenia JavaScript. Zakładając, że używasz już ngx-bootstrap, możesz użyć rozwiązania @ stwedebaker, ale zamień „Guard” na ten, który tutaj pokazuję. Musisz także wprowadzić ngx-bootstrap/modali dodać nowe ConfirmationComponent:

Strzec

(zamień 'confirm' na funkcję, która otworzy modal bootstrap - wyświetlając nowy, niestandardowy 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;
    })
  }
}

potwierdzenie.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>

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

A ponieważ nowy ConfirmationComponentbędzie wyświetlany bez użycia a selectorw szablonie html, musi być zadeklarowany w entryComponentskatalogu głównym app.module.ts(lub jakkolwiek nazwiesz swój moduł główny). Wprowadź następujące zmiany w 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
źródło
1
czy jest szansa na pokazanie własnego modelu do odświeżenia przeglądarki?
k11k2
Musi być sposób, chociaż to rozwiązanie odpowiadało moim potrzebom. Jeśli zdążę, rozwinę dalej, chociaż nie będę mógł zaktualizować tej odpowiedzi przez dłuższy czas, przepraszam!
Chris Halcrow,
1

Rozwiązanie było łatwiejsze niż oczekiwano, nie używaj, hrefponieważ nie jest to obsługiwane przez routerLinkdyrektywę użycia kątowego routingu .

Byron Lopez
źródło
1

Odpowiedź z czerwca 2020 r .:

Zwróć uwagę, że wszystkie proponowane do tej pory rozwiązania nie rozwiązują problemu z istotną znaną wadą canDeactivateosłon Angulara:

  1. Użytkownik klika przycisk „Wstecz” w przeglądarce, wyświetla się okno dialogowe, a użytkownik klika ANULUJ .
  2. Użytkownik ponownie klika przycisk „Wstecz”, wyświetla się okno dialogowe, a użytkownik klika POTWIERDŹ .
  3. Uwaga: użytkownik jest cofany 2 razy, co może nawet całkowicie usunąć go z aplikacji :(

Zostało to omówione tutaj , tutaj i szczegółowo tutaj


Zobacz moje rozwiązanie problemu , które zostało tutaj przedstawione, które bezpiecznie rozwiązuje ten problem *. Zostało to przetestowane w Chrome, Firefox i Edge.


* WAŻNE OSTRZEŻENIE : na tym etapie powyższe wyczyści historię do przodu po kliknięciu przycisku Wstecz, ale zachowa historię wstecz. To rozwiązanie nie będzie odpowiednie, jeśli zachowanie historii do przodu jest niezbędne. W moim przypadku, jeśli chodzi o formularze, zazwyczaj używam strategii routingu typu master-detail , więc utrzymywanie historii przekazywania nie jest ważne.

Stephen Paul
źródło