在离开页面之前警告用户未保存的更改


112

我想警告用户未保存的更改,然后再离开我的angular 2应用程序的特定页面。通常,我会使用window.onbeforeunload,但是不适用于单页应用程序。

我发现在角度1中,您可以进入$locationChangeStart事件confirm为用户抛出一个框,但是我没有看到任何显示如何使角度2正常工作的东西,或者该事件是否仍然存在。我也看到过为ag1提供功能的插件onbeforeunload,但同样,我还没有看到将其用于ag2的任何方式。

我希望其他人找到了解决该问题的方法;两种方法都可以很好地达到我的目的。


2
当您尝试关闭页面/选项卡时,它确实适用于单页应用程序。因此,如果他们忽略该事实,那么对该问题的任何答案都只能是部分解决方案。
9ilsdx 9rvj 0lo

Answers:


74

路由器提供生命周期回调CanDeactivate

有关更多详细信息,请参阅“ 警卫队”教程

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

28
与OP要求的不同,CanDeactivate当前(不)挂在onbeforeunload事件上(不幸的是)。这意味着,如果用户尝试导航到外部URL,请关闭窗口等。不会触发CanDeactivate。它似乎仅在用户停留在应用内时才起作用。
Christophe Vidal

1
@ChristopheVidal是正确的。请查看我的答案,以获取一种解决方案,其中还包括导航到外部URL,关闭窗口,重新加载页面等
。– stewdebaker

这在更改路线时有效。如果是SPA怎么办?还有其他方法吗?
sujay kodamala

stackoverflow.com/questions/36763141/…您也需要使用路由。如果关闭窗口或将其导航到当前站点之外,canDeactivate将无法使用。
君特Zöchbauer

214

为了防止浏览器刷新,关闭窗口等(有关问题的详细信息,请参见@ChristopheVidal对Günter的回答的评论),我发现将@HostListener装饰器添加到类的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 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.');
  }
}

路线:

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事件的方式与其他浏览器不同,并且falsebeforeunload事件激活时(例如,浏览器刷新,关闭窗口等),将在确认对话框中包含单词。在Angular应用中导航不会受影响,并且会正确显示您指定的确认警告消息。那些需要支持IE / Edge并不想falsebeforeunload事件激活时在确认对话框中显示/想要更详细的消息的人可能还希望查看@JasperRisseeuw的解决方案。


2
@stewdebaker真的很好用!除了此解决方案外,我还有其他答案。
Jasper Risseeuw

1
从'rxjs / Observable'导入{Observable};ComponentCanDeactivate中缺少的内容
Mahmoud Ali Kassem

1
我必须添加@Injectable()到PendingChangesGuard类。此外,我还必须在@NgModule
spottedmahn'6

我必须添加import { HostListener } from '@angular/core';
fidev '17

4
值得注意的是,如果要使用导航,必须返回一个布尔值beforeunload。如果返回一个Observable,它将无法正常工作。您可能需要将界面更改为类似的名称,canDeactivate: (internalNavigation: true | undefined)然后像这样调用组件:return component.canDeactivate(true)。这样,您可以检查是否不在内部导航返回false而不是Observable。
jsgoupil

58

使用来自stewdebaker的@Hostlistener的示例确实运行良好,但是我对其进行了另一处更改,因为IE和Edge在MyComponent类上向最终用户显示了canDeactivate()方法返回的“ false”。

零件:

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
好抓@JasperRisseeuw!我没有意识到IE / Edge的处理方式有所不同。对于需要支持IE / Edge且不希望false在确认对话框中显示的用户来说,这是一个非常有用的解决方案。我对您的答案做了一个小小的修改,以将包括'$event'@HostListener注释中,因为这是必须的,以便能够在unloadNotification函数中访问它。
stewdebaker

1
谢谢,我忘了从我自己的代码中复制“,['$ event']”,所以也很适合您!
Jasper Risseeuw'2

唯一可行的解​​决方案是此解决方案(使用Edge)。其他所有功能均有效,但仅显示默认对话框消息(Chrome / Firefox),而不显示我的文字...我什至问了一个问题以了解正在发生的事情
Elmer Dantas

@ElmerDantas,请查看对问题的回答,以解释为什么默认对话框消息在Chrome / Firefox中显示。
stewdebaker

2
实际上,它有效,抱歉!我必须在模块提供者中引用防护措施。
tudor.iliescu

6

我已经实现了@stewdebaker的解决方案,效果很好,但是我想要一个不错的bootstrap弹出窗口,而不是笨拙的标准JavaScript确认。假设您已经在使用ngx-bootstrap,则可以使用@stwedebaker的解决方案,但是将“ Guard”换成我在这里显示的那个。您还需要引入ngx-bootstrap/modal,并添加一个新的ConfirmationComponent

守卫

(将'confirm'替换为将打开引导程序模式的功能-显示新的自定义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;
    })
  }
}

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>

确认组件

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将在不使用selectorhtml模板的情况下显示新内容,因此需要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]

1
是否有可能显示自定义模型以刷新浏览器?
k11k2 '18年

必须有一种方法,尽管此解决方案可以满足我的需求。如果有时间,我会进一步发展,尽管很抱歉我将无法更新此答案!
克里斯·哈克罗


1

2020年6月答案:

请注意,到目前为止,所有提出的解决方案都无法解决Angular的canDeactivate防护措施存在的重大已知缺陷:

  1. 用户单击浏览器中的“后退”按钮,显示对话框,然后单击“ 取消”
  2. 用户再次单击“后退”按钮,显示对话框,然后单击CONFIRM
  3. 注意:用户向后浏览了2次,甚至可能将他们从应用程序中完全撤出:(

这已经在这里这里这里详细讨论过


请查看我针对此处演示的问题的解决方案,该解决方案可以安全地解决此问题*。已在Chrome,Firefox和Edge上进行了测试。


* 重要注意事项:在此阶段,当单击后退按钮时,以上内容将清除前进的历史记录,但保留前进的历史记录。如果保留您的转发历史至关重要,则此解决方案将不合适。就我而言,当涉及到表单时,我通常使用主从路由策略,因此保持转发历史记录并不重要。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.