Cảnh báo người dùng về những thay đổi chưa được lưu trước khi rời khỏi trang


112

Tôi muốn cảnh báo người dùng về những thay đổi chưa được lưu trước khi họ rời khỏi một trang cụ thể trong ứng dụng angle 2 của tôi. Thông thường tôi sẽ sử dụng window.onbeforeunload, nhưng điều đó không hoạt động đối với các ứng dụng trang đơn.

Tôi nhận thấy rằng trong góc 1, bạn có thể tham gia vào $locationChangeStartsự kiện để đưa ra một confirmhộp cho người dùng, nhưng tôi chưa thấy bất kỳ điều gì cho thấy cách làm cho điều này hoạt động cho góc 2 hoặc nếu sự kiện đó thậm chí vẫn còn tồn tại . Tôi cũng đã thấy các plugin cho ag1 cung cấp chức năng onbeforeunload, nhưng một lần nữa, tôi chưa thấy cách nào để sử dụng nó cho ag2.

Tôi hy vọng ai đó đã tìm ra giải pháp cho vấn đề này; một trong hai phương pháp sẽ hoạt động tốt cho mục đích của tôi.


2
Nó hoạt động cho các ứng dụng trang đơn, khi bạn cố gắng đóng trang / tab. Vì vậy, bất kỳ câu trả lời nào cho câu hỏi sẽ chỉ là giải pháp một phần nếu họ bỏ qua thực tế đó.
9ilsdx 9rvj 0lo

Câu trả lời:


74

Bộ định tuyến cung cấp một cuộc gọi lại vòng đời CanDeactivate

để biết thêm chi tiết, hãy xem hướng dẫn bảo vệ

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

ban đầu (bộ định tuyến 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
Không giống như những gì OP yêu cầu, CanDeactivate -currently- không kết nối với sự kiện onbeforeunload (thật không may). Có nghĩa là nếu người dùng cố gắng điều hướng đến một URL bên ngoài, hãy đóng cửa sổ, v.v. CanDeactivate sẽ không được kích hoạt. Nó dường như chỉ hoạt động khi người dùng ở trong ứng dụng.
Christophe Vidal,

1
@ChristopheVidal là đúng. Hãy xem câu trả lời của tôi cho một giải pháp mà cũng bao gồm điều hướng đến một URL bên ngoài, đóng cửa sổ, tải lại trang, vv
stewdebaker

Điều này hoạt động khi thay đổi tuyến đường. Nếu đó là SPA thì sao? Bất kỳ cách nào khác để đạt được điều này?
sujay kodamala

stackoverflow.com/questions/36763141/… Bạn cũng sẽ cần điều này với các tuyến đường. Nếu cửa sổ bị đóng hoặc điều hướng khỏi trang web hiện tại canDeactivatesẽ không hoạt động.
Günter Zöchbauer

214

Để bảo vệ chống lại việc làm mới trình duyệt, đóng cửa sổ, v.v. (xem nhận xét của @ ChristopheVidal về câu trả lời của Günter để biết chi tiết về vấn đề này), tôi thấy hữu ích khi thêm trình @HostListenertrang trí vào phần canDeactivatetriển khai của lớp bạn để lắng nghe beforeunload windowsự kiện. Khi được định cấu hình chính xác, điều này sẽ bảo vệ chống lại cả điều hướng trong ứng dụng và điều hướng bên ngoài cùng một lúc.

Ví dụ:

Thành phần:

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

Bảo vệ:

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

Các tuyến:

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ô-đun:

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

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

LƯU Ý : Như @JasperRisseeuw đã chỉ ra, IE và Edge xử lý beforeunloadsự kiện khác với các trình duyệt khác và sẽ bao gồm từ này falsetrong hộp thoại xác nhận khi beforeunloadsự kiện kích hoạt (ví dụ: làm mới trình duyệt, đóng cửa sổ, v.v.). Điều hướng đi trong ứng dụng Angular không bị ảnh hưởng và sẽ hiển thị đúng thông báo cảnh báo xác nhận được chỉ định của bạn. Những người cần hỗ trợ IE / Edge và không muốn falsehiển thị / muốn thông báo chi tiết hơn trong hộp thoại xác nhận khi beforeunloadsự kiện kích hoạt cũng có thể muốn xem câu trả lời của @ JasperRisseeuw để biết cách giải quyết.


2
Điều này thực sự hoạt động tốt @stewdebaker! Tôi có một bổ sung cho giải pháp này, hãy xem câu trả lời của tôi bên dưới.
Jasper Risseeuw

1
nhập {Observable} từ 'rxjs / Observable'; là mất tích từ ComponentCanDeactivate
Mahmoud Ali Kassem

1
Tôi đã phải thêm @Injectable()vào lớp PendingChangesGuard. Ngoài ra, tôi đã phải thêm PendingChangesGuard vào các nhà cung cấp của mình trong@NgModule
spottedmahn

Tôi đã phải thêmimport { HostListener } from '@angular/core';
fidev 19/07/17

4
Cần lưu ý rằng bạn phải trả về boolean trong trường hợp bạn đang bị điều hướng beforeunload. Nếu bạn trả về một Observable, nó sẽ không hoạt động. Bạn có thể muốn thay đổi giao diện của bạn cho một cái gì đó giống như canDeactivate: (internalNavigation: true | undefined)và gọi thành phần của bạn như thế này: return component.canDeactivate(true). Bằng cách này, bạn có thể kiểm tra xem bạn có đang điều hướng nội bộ không để quay lại falsethay vì có thể quan sát được.
jsgoupil

58

Ví dụ với @Hostlistener từ steverdebaker hoạt động thực sự tốt, nhưng tôi đã thực hiện thêm một thay đổi đối với nó vì IE và Edge hiển thị "false" được trả về bởi phương thức canDeactivate () trên lớp MyComponent cho người dùng cuối.

Thành phần:

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
Bắt tốt @JasperRisseeuw! Tôi không nhận ra rằng IE / Edge xử lý điều này theo cách khác. Đây là một giải pháp rất hữu ích cho những ai cần hỗ trợ IE / Edge và không muốn falsehiển thị trong hộp thoại xác nhận. Tôi đã thực hiện một chỉnh sửa nhỏ cho câu trả lời của bạn để đưa '$event'vào @HostListenerchú thích, vì điều đó là bắt buộc để có thể truy cập nó trong unloadNotificationhàm.
thợ hầm

1
Cảm ơn, tôi đã quên sao chép ", ['$ event']" từ mã của riêng tôi, vì vậy bạn cũng nắm bắt được rất tốt!
Jasper Risseeuw

giải pháp duy nhất hoạt động là giải pháp này (sử dụng Edge). tất cả những người khác làm việc nhưng chỉ hiển thị mặc định nhắn thoại (Chrome / Firefox), chứ không phải văn bản của tôi ... Tôi thậm chí hỏi một câu hỏi để hiểu những gì đang xảy ra
Elmer Dantas

@ElmerDantas vui lòng xem câu trả lời của tôi cho câu hỏi của bạn để biết giải thích về lý do tại sao thông báo hộp thoại mặc định hiển thị trong Chrome / Firefox.
thợ hầm

2
Trên thực tế, nó hoạt động, xin lỗi! Tôi đã phải tham khảo người bảo vệ trong các nhà cung cấp mô-đun.
tudor.iliescu Ngày

6

Tôi đã triển khai giải pháp từ @stewdebaker hoạt động rất tốt, tuy nhiên tôi muốn có một cửa sổ bật lên khởi động đẹp thay vì xác nhận JavaScript tiêu chuẩn rườm rà. Giả sử bạn đã sử dụng ngx-bootstrap, bạn có thể sử dụng giải pháp của @ stwedebaker, nhưng hãy hoán đổi 'Guard' cho giải pháp mà tôi đang hiển thị ở đây. Bạn cũng cần giới thiệu ngx-bootstrap/modalvà thêm mới ConfirmationComponent:

Bảo vệ

(thay thế 'xác nhận' bằng một hàm sẽ mở một phương thức bootstrap - hiển thị một tùy chỉnh mới 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;
    })
  }
}

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

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

Và vì cái mới ConfirmationComponentsẽ được hiển thị mà không sử dụng một selectormẫu html, nó cần phải được khai báo entryComponentstrong thư mục gốc của bạn app.module.ts(hoặc bất kỳ cái gì bạn đặt tên cho mô-đun gốc của mình). Thực hiện các thay đổi sau đối với 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ó cơ hội hiển thị mô hình tùy chỉnh để làm mới trình duyệt không?
k11k2

Phải có một cách, mặc dù giải pháp này phù hợp với nhu cầu của tôi. Nếu có thời gian, tôi sẽ phát triển mọi thứ hơn nữa, mặc dù tôi sẽ không thể cập nhật câu trả lời này trong một thời gian khá dài, xin lỗi!
Chris Halcrow

1

Giải pháp dễ dàng hơn mong đợi, không sử dụng hrefvì điều này không được xử lý bởi routerLinkchỉ thị sử dụng Angular Routing .


1

Câu trả lời tháng 6 năm 2020:

Lưu ý rằng tất cả các giải pháp được đề xuất cho đến thời điểm này không giải quyết được một lỗ hổng đáng kể nào đã biết với các canDeactivatevệ sĩ của Angular :

  1. Người dùng nhấp vào nút 'quay lại' trong trình duyệt, hộp thoại hiển thị và người dùng nhấp vào HỦY .
  2. Người dùng nhấp lại vào nút 'quay lại', hộp thoại hiển thị và người dùng nhấp vào XÁC NHẬN .
  3. Lưu ý: người dùng được điều hướng trở lại 2 lần, thậm chí có thể đưa họ ra khỏi ứng dụng hoàn toàn :(

Điều này đã được thảo luận ở đây , ở đây , và ở đây


Vui lòng xem giải pháp của tôi cho vấn đề được trình bày ở đây , giải pháp này hoạt động một cách an toàn xung quanh vấn đề này *. Điều này đã được thử nghiệm trên Chrome, Firefox và Edge.


* CAVEAT QUAN TRỌNG : Ở giai đoạn này, phần trên sẽ xóa lịch sử chuyển tiếp khi nhấp vào nút quay lại, nhưng giữ nguyên lịch sử quay lại. Giải pháp này sẽ không phù hợp nếu việc lưu giữ lịch sử về sau của bạn là rất quan trọng. Trong trường hợp của tôi, tôi thường sử dụng chiến lược định tuyến tổng thể chi tiết khi nói đến biểu mẫu, vì vậy việc duy trì lịch sử chuyển tiếp không quan trọng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.