在离开页面之前警告用户未保存的更改
Warn user of unsaved changes before leaving page
我想在用户离开我的 angular 2 应用程序的特定页面之前警告他们未保存的更改。通常我会使用 window.onbeforeunload
,但这不适用于单页应用程序。
我发现在 angular 1 中,您可以挂钩 $locationChangeStart
事件为用户抛出一个 confirm
框,但我没有看到任何东西这显示了如何使 angular 2 工作,或者该事件是否仍然存在。我还看到 plugins 为 ag1 提供了 onbeforeunload
的功能,但同样,我还没有看到任何方法可以将它用于 ag2。
我希望其他人已经找到解决这个问题的方法;这两种方法都适合我的目的。
路由器提供生命周期回调CanDeactivate
有关详细信息,请参阅 guards tutorial
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 {}
原始(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]
}])
);
为了防止浏览器刷新、关闭 window 等(有关此问题的详细信息,请参阅@ChristopheVidal 对 Günter 的回答的评论),我发现添加 @HostListener
装饰器添加到 class 的 canDeactivate
实现中以侦听 beforeunload
window
事件。如果配置正确,这将同时防止应用内和外部导航。
例如:
分量:
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
}
}
后卫:
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
confirm('WARNING: You have unsaved changes. Press Cancel to go back and save these changes, or OK to lose these changes.');
}
}
路线:
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] },
];
模块:
import { PendingChangesGuard } from './pending-changes.guard';
import { NgModule } from '@angular/core';
@NgModule({
// ...
providers: [PendingChangesGuard],
// ...
})
export class AppModule {}
注意:正如@JasperRisseeuw 所指出的,IE 和 Edge 处理 beforeunload
事件的方式与其他浏览器不同,并且会在beforeunload
事件激活时的确认对话框(例如,浏览器刷新、关闭 window 等)。在 Angular 应用程序内导航离开不受影响,并且会正确显示您指定的确认警告消息。那些需要支持 IE/Edge 并且不希望 false
到 show/want 在 beforeunload
事件激活时在确认对话框中显示更详细消息的人可能还想看看@JasperRisseeuw 的解决方法的答案。
来自 stewdebaker 的 @Hostlistener 的示例工作得非常好,但我对其进行了更多更改,因为 IE 和 Edge 显示了 MyComponent [= 上的 canDeactivate() 方法返回的 "false" 18=] 给最终用户。
分量:
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)";
}
}
}
解决方案比预期的要简单,不要使用 href
,因为这不是由 Angular 路由处理的,而是使用 routerLink
指令。
我已经实现了@stewdebaker 的解决方案,效果非常好,但是我想要一个漂亮的 bootstrap 弹出窗口,而不是笨拙的标准 JavaScript 确认。假设您已经在使用 ngx-bootstrap,您可以使用 @stwedebaker 的解决方案,但将 'Guard' 换成我在这里展示的那个。您还需要引入ngx-bootstrap/modal
,并添加一个新的ConfirmationComponent
:
后卫
(将 'confirm' 替换为将打开 bootstrap 模式的函数 - 显示新的自定义 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
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();
}
}
并且由于新的 ConfirmationComponent
将在不使用 html 模板中的 selector
的情况下显示,它需要在 entryComponents
中声明 (not needed anymore with Ivy) 在你的根 app.module.ts
中(或者你给你的根模块起的任何名字)。对 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] // Only when using old ViewEngine
2020 年 6 月答案:
请注意,到目前为止提出的所有解决方案都没有解决 Angular 的 canDeactivate
守卫的重大已知缺陷:
- 用户单击浏览器中的 'back' 按钮,显示对话框,然后用户单击 取消。
- 用户再次单击 'back' 按钮,显示对话框,然后用户单击 CONFIRM。
- 注意:用户被导航回 2 次,这甚至可能让他们完全离开应用程序:(
已经讨论过here, here, and at length here
请参阅我对问题 demonstrated here 的解决方案,它可以安全地解决此问题*。这已经在 Chrome、Firefox 和 Edge 上进行了测试。
* 重要警告:在这个阶段,当点击后退按钮时,上面的内容将清除前进历史,但保留后退历史。如果保留您的前向历史至关重要,则此解决方案将不合适。就我而言,在涉及表单时,我通常使用 master-detail 路由策略,因此维护转发历史记录并不重要。
我想在用户离开我的 angular 2 应用程序的特定页面之前警告他们未保存的更改。通常我会使用 window.onbeforeunload
,但这不适用于单页应用程序。
我发现在 angular 1 中,您可以挂钩 $locationChangeStart
事件为用户抛出一个 confirm
框,但我没有看到任何东西这显示了如何使 angular 2 工作,或者该事件是否仍然存在。我还看到 plugins 为 ag1 提供了 onbeforeunload
的功能,但同样,我还没有看到任何方法可以将它用于 ag2。
我希望其他人已经找到解决这个问题的方法;这两种方法都适合我的目的。
路由器提供生命周期回调CanDeactivate
有关详细信息,请参阅 guards tutorial
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 {}
原始(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] }]) );
为了防止浏览器刷新、关闭 window 等(有关此问题的详细信息,请参阅@ChristopheVidal 对 Günter 的回答的评论),我发现添加 @HostListener
装饰器添加到 class 的 canDeactivate
实现中以侦听 beforeunload
window
事件。如果配置正确,这将同时防止应用内和外部导航。
例如:
分量:
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
}
}
后卫:
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
confirm('WARNING: You have unsaved changes. Press Cancel to go back and save these changes, or OK to lose these changes.');
}
}
路线:
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] },
];
模块:
import { PendingChangesGuard } from './pending-changes.guard';
import { NgModule } from '@angular/core';
@NgModule({
// ...
providers: [PendingChangesGuard],
// ...
})
export class AppModule {}
注意:正如@JasperRisseeuw 所指出的,IE 和 Edge 处理 beforeunload
事件的方式与其他浏览器不同,并且会在beforeunload
事件激活时的确认对话框(例如,浏览器刷新、关闭 window 等)。在 Angular 应用程序内导航离开不受影响,并且会正确显示您指定的确认警告消息。那些需要支持 IE/Edge 并且不希望 false
到 show/want 在 beforeunload
事件激活时在确认对话框中显示更详细消息的人可能还想看看@JasperRisseeuw 的解决方法的答案。
来自 stewdebaker 的 @Hostlistener 的示例工作得非常好,但我对其进行了更多更改,因为 IE 和 Edge 显示了 MyComponent [= 上的 canDeactivate() 方法返回的 "false" 18=] 给最终用户。
分量:
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)";
}
}
}
解决方案比预期的要简单,不要使用 href
,因为这不是由 Angular 路由处理的,而是使用 routerLink
指令。
我已经实现了@stewdebaker 的解决方案,效果非常好,但是我想要一个漂亮的 bootstrap 弹出窗口,而不是笨拙的标准 JavaScript 确认。假设您已经在使用 ngx-bootstrap,您可以使用 @stwedebaker 的解决方案,但将 'Guard' 换成我在这里展示的那个。您还需要引入ngx-bootstrap/modal
,并添加一个新的ConfirmationComponent
:
后卫
(将 'confirm' 替换为将打开 bootstrap 模式的函数 - 显示新的自定义 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
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();
}
}
并且由于新的 ConfirmationComponent
将在不使用 html 模板中的 selector
的情况下显示,它需要在 (not needed anymore with Ivy) 在你的根 entryComponents
中声明app.module.ts
中(或者你给你的根模块起的任何名字)。对 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] // Only when using old ViewEngine
2020 年 6 月答案:
请注意,到目前为止提出的所有解决方案都没有解决 Angular 的 canDeactivate
守卫的重大已知缺陷:
- 用户单击浏览器中的 'back' 按钮,显示对话框,然后用户单击 取消。
- 用户再次单击 'back' 按钮,显示对话框,然后用户单击 CONFIRM。
- 注意:用户被导航回 2 次,这甚至可能让他们完全离开应用程序:(
已经讨论过here, here, and at length here
请参阅我对问题 demonstrated here 的解决方案,它可以安全地解决此问题*。这已经在 Chrome、Firefox 和 Edge 上进行了测试。
* 重要警告:在这个阶段,当点击后退按钮时,上面的内容将清除前进历史,但保留后退历史。如果保留您的前向历史至关重要,则此解决方案将不合适。就我而言,在涉及表单时,我通常使用 master-detail 路由策略,因此维护转发历史记录并不重要。