Peringatkan pengguna tentang perubahan yang belum disimpan sebelum meninggalkan halaman

112

Saya ingin memperingatkan pengguna tentang perubahan yang belum disimpan sebelum mereka meninggalkan halaman tertentu di aplikasi sudut 2 saya. Biasanya saya akan menggunakan window.onbeforeunload, tetapi itu tidak berfungsi untuk aplikasi satu halaman.

Saya telah menemukan bahwa di sudut 1, Anda dapat menghubungkan ke $locationChangeStartacara untuk mengeluarkan confirmkotak untuk pengguna, tetapi saya belum melihat apa pun yang menunjukkan cara membuat ini berfungsi untuk sudut 2, atau jika acara itu bahkan masih ada . Saya juga telah melihat plugin untuk ag1 yang menyediakan fungsionalitas untuk onbeforeunload, tetapi sekali lagi, saya belum melihat cara untuk menggunakannya untuk ag2.

Saya berharap orang lain telah menemukan solusi untuk masalah ini; metode mana pun akan bekerja dengan baik untuk tujuan saya.

Whelch
sumber
2
Ini berfungsi untuk aplikasi satu halaman, ketika Anda mencoba menutup halaman / tab. Jadi jawaban atas pertanyaan apa pun hanya akan menjadi solusi parsial jika mereka mengabaikan fakta itu.
9ilsdx 9rvj 0lo

Jawaban:

74

Router menyediakan callback siklus hidup CanDeactivate

untuk lebih jelasnya lihat tutorial penjaga

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

asli (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
sumber
28
Tidak seperti yang diminta OP, CanDeactivate -saat ini- tidak mengaitkan acara onbeforeunload (sayangnya). Artinya jika pengguna mencoba menavigasi ke URL eksternal, menutup jendela, dll. CanDeactivate tidak akan terpicu. Tampaknya hanya berfungsi saat pengguna tetap berada di dalam aplikasi.
Christophe Vidal
1
@ChristopheVidal benar. Silakan lihat jawaban saya untuk solusi yang juga mencakup menavigasi ke URL eksternal, menutup jendela, memuat ulang halaman, dll.
stewdebaker
Ini berfungsi saat mengubah rute. Bagaimana jika itu SPA? Adakah cara lain untuk mencapai ini?
sujay kodamala
stackoverflow.com/questions/36763141/… Anda akan membutuhkan ini dengan rute juga. Jika jendela ditutup atau dinavigasi keluar dari situs saat canDeactivateini tidak akan berfungsi.
Günter Zöchbauer
214

Untuk juga melindungi penjaga dari penyegaran browser, menutup jendela, dll. (Lihat komentar @ ChristopheVidal untuk jawaban Günter untuk detail tentang masalah ini), saya merasa terbantu untuk menambahkan @HostListenerdekorator ke canDeactivateimplementasi kelas Anda untuk mendengarkan beforeunload windowacara tersebut. Jika dikonfigurasi dengan benar, ini akan melindungi dari navigasi dalam aplikasi dan eksternal pada saat yang bersamaan.

Sebagai contoh:

Komponen:

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

Menjaga:

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

Rute:

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

CATATAN : Seperti yang ditunjukkan oleh @JasperRisseeuw, IE dan Edge menangani beforeunloadacara secara berbeda dari browser lain dan akan menyertakan kata falsedalam dialog konfirmasi saat beforeunloadacara diaktifkan (misalnya, browser menyegarkan, menutup jendela, dll.). Menavigasi dalam aplikasi Angular tidak terpengaruh dan akan menampilkan pesan peringatan konfirmasi yang Anda tunjuk dengan benar. Mereka yang perlu mendukung IE / Edge dan tidak ingin falsemenampilkan / menginginkan pesan yang lebih mendetail di dialog konfirmasi saat beforeunloadacara diaktifkan mungkin juga ingin melihat jawaban @ JasperRisseeuw untuk mengatasinya.

pelacur
sumber
2
Ini bekerja dengan sangat baik @stewdebaker! Saya memiliki satu tambahan untuk solusi ini, lihat jawaban saya di bawah.
Jasper Risseeuw
1
impor {Observable} dari 'rxjs / Observable'; hilang dari ComponentCanDeactivate
Mahmoud Ali Kassem
1
Saya harus menambahkan @Injectable()ke kelas PendingChangesGuard. Juga, saya harus menambahkan PendingChangesGuard ke penyedia saya di@NgModule
spottedmahn
Saya harus menambahkanimport { HostListener } from '@angular/core';
fidev
4
Perlu diperhatikan bahwa Anda harus mengembalikan boolean jika Anda sedang dinavigasi beforeunload. Jika Anda mengembalikan Observable, itu tidak akan berfungsi. Anda mungkin ingin mengubah antarmuka Anda untuk sesuatu seperti canDeactivate: (internalNavigation: true | undefined)dan memanggil komponen Anda seperti ini: return component.canDeactivate(true). Dengan cara ini, Anda dapat memeriksa apakah Anda tidak menavigasi secara internal untuk kembali falsealih-alih Observable.
jsgoupil
58

Contoh dengan @Hostlistener dari stewdebaker bekerja dengan sangat baik, tetapi saya membuat satu perubahan lagi karena IE dan Edge menampilkan "false" yang dikembalikan oleh metode canDeactivate () pada kelas MyComponent kepada pengguna akhir.

Komponen:

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
sumber
2
Tangkapan bagus @JasperRisseeuw! Saya tidak menyadari bahwa IE / Edge menangani ini secara berbeda. Ini adalah solusi yang sangat berguna bagi mereka yang perlu mendukung IE / Edge dan tidak ingin falseditampilkan di dialog konfirmasi. Saya membuat sedikit pengeditan pada jawaban Anda untuk menyertakan '$event'dalam @HostListeneranotasi, karena itu diperlukan agar dapat mengaksesnya dalam unloadNotificationfungsi.
stewdebaker
1
Terima kasih, saya lupa menyalin ", ['$ event']" dari kode saya sendiri, jadi bagus juga untuk Anda!
Jasper Risseeuw
satu-satunya solusi yang berhasil adalah yang ini (menggunakan Edge). semua yang lain berfungsi tetapi hanya menampilkan pesan dialog default (Chrome / Firefox), bukan teks saya ... Saya bahkan mengajukan pertanyaan untuk memahami apa yang terjadi
Elmer Dantas
@ElmerDantas silakan lihat jawaban saya atas pertanyaan Anda untuk penjelasan mengapa pesan dialog default muncul di Chrome / Firefox.
stewdebaker
2
Sebenarnya, berhasil, maaf! Saya harus merujuk penjaga di penyedia modul.
tudor.iliescu
6

Saya telah mengimplementasikan solusi dari @stewdebaker yang bekerja dengan sangat baik, namun saya menginginkan popup bootstrap yang bagus daripada konfirmasi JavaScript standar yang kikuk. Dengan asumsi Anda sudah menggunakan ngx-bootstrap, Anda dapat menggunakan solusi @ stwedebaker, tetapi tukar 'Guard' dengan yang saya tunjukkan di sini. Anda juga perlu memperkenalkan ngx-bootstrap/modal, dan menambahkan yang baru ConfirmationComponent:

Menjaga

(ganti 'confirm' dengan fungsi yang akan membuka modal bootstrap - menampilkan custom baru 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;
    })
  }
}

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

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

Dan karena yang baru ConfirmationComponentakan ditampilkan tanpa menggunakan selectordalam template html, itu perlu dideklarasikan di entryComponentsroot Anda app.module.ts(atau apa pun yang Anda beri nama modul root Anda). Lakukan perubahan berikut untuk 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
sumber
1
apakah ada kemungkinan menampilkan model kustom untuk penyegaran browser?
k11k2
Pasti ada jalan, meski solusi ini OK untuk kebutuhan saya. Jika saya punya waktu, saya akan mengembangkan lebih jauh, meskipun saya tidak akan dapat memperbarui jawaban ini untuk waktu yang cukup lama, maaf!
Chris Halcrow
1

Solusinya lebih mudah dari yang diharapkan, jangan gunakan hrefkarena ini tidak ditangani oleh routerLinkdirektif penggunaan Angular Routing .

Byron Lopez
sumber
1

Jawaban Juni 2020:

Perhatikan bahwa semua solusi yang diusulkan hingga saat ini tidak menangani cacat signifikan yang diketahui dengan canDeactivatepenjaga Angular :

  1. Pengguna mengklik tombol 'kembali' di browser, menampilkan dialog, dan pengguna mengklik BATAL .
  2. Pengguna mengklik tombol 'kembali' lagi, dialog ditampilkan, dan pengguna mengklik KONFIRMASI .
  3. Catatan: pengguna dinavigasi kembali 2 kali, yang bahkan dapat membuat mereka keluar dari aplikasi sama sekali :(

Ini telah dibahas di sini , di sini , dan panjang lebar di sini


Silakan lihat solusi saya untuk masalah yang ditunjukkan di sini yang bekerja dengan aman di sekitar masalah ini *. Ini telah diuji di Chrome, Firefox, dan Edge.


* PERHATIAN PENTING : Pada tahap ini, di atas akan menghapus riwayat maju ketika tombol kembali diklik, tetapi mempertahankan riwayat belakang. Solusi ini tidak akan sesuai jika mempertahankan riwayat maju Anda sangat penting. Dalam kasus saya, saya biasanya menggunakan strategi perutean detail utama ketika datang ke formulir, jadi mempertahankan riwayat ke depan tidak penting.

Stephen Paul
sumber