ページを離れる前に未保存の変更をユーザーに警告する


112

Angular 2アプリの特定のページを離れる前に、保存されていない変更についてユーザーに警告します。通常はを使用しますがwindow.onbeforeunload、単一ページのアプリケーションでは機能しません。

私はangular 1で$locationChangeStartイベントにフックしてconfirmユーザーにボックスを投げることができることを発見しましたが、angular 2でこれを機能させる方法を示すものは何も見ていません、またはそのイベントがまだ存在する場合でも。の機能を提供するag1のプラグインも見ましたが、これもag2でonbeforeunload使用する方法は見ていません。

他の誰かがこの問題の解決策を見つけてくれることを願っています。私の目的にはどちらの方法でも問題なく機能します。


2
ページ/タブを閉じようとすると、単一ページのアプリケーションで機能します。したがって、質問に対する答えは、その事実を無視した場合の部分的な解決策にすぎません。
9ilsdx 9rvj 0lo 2016年

回答:


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 2017年

stackoverflow.com/questions/36763141/…これもルートで必要になります。ウィンドウが閉じているか、現在のサイトから移動canDeactivateすると、機能しません。
ギュンターZöchbauer

214

ブラウザの更新やウィンドウのクローズなどに対する保護もカバーするために(問題の詳細については、ギュンターの回答に対する@ChristopheVidalのコメントを参照)、イベントをリッスンするために@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で本当にうまくいきます!このソリューションに1つの追加があります。以下の私の回答を参照してください。
Jasper Risseeuw 2017

1
「rxjs / Observable」から{Observable}をインポートします。はComponentCanDeactivateにありません
Mahmoud Ali Kassem 2017年

1
@Injectable()PendingChangesGuardクラスに追加する必要がありました。また、PendingChangesGuardをプロバイダーに追加する必要がありました@NgModule
spottedmahn

追加しなければなりimport { HostListener } from '@angular/core';
ませんでした

4
でナビゲートされている場合に備えて、ブール値を返す必要があることに注意してくださいbeforeunload。Observableを返すと機能しません。インターフェースを次のように変更してcanDeactivate: (internalNavigation: true | undefined)、コンポーネントを次のように呼び出すことができますreturn component.canDeactivate(true)。この方法falseで、Observableの代わりに内部に移動して戻っていないかどうかを確認できます。
jsgoupil

58

stewdebakerの@Hostlistenerを使用した例は非常にうまく機能しますが、IEとEdgeがMyComponentクラスのcanDeactivate()メソッドによって返される「false」をエンドユーザーに表示するため、もう1つ変更を加えました。

成分:

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 2017

1
おかげで、私は自分のコードから "、['$ event']"をコピーするのを忘れたので、あなたからも良いキャッチができました!
Jasper Risseeuw 2017

機能する唯一のソリューションは、これ(Edgeを使用)でした。他はすべて機能しますが、デフォルトのダイアログメッセージ(Chrome / Firefox)のみが表示され、テキストは表示されません... 何が起こっているのかを理解するために質問しました
Elmer Dantas

@ElmerDantas は、Chrome / Firefoxでデフォルトのダイアログメッセージが表示される理由の説明について、質問に対する私の回答を参照しください。
stewdebaker 2017

2
実際に動作します。ごめんなさい!モジュールプロバイダーのガードを参照する必要がありました。
tudor.iliescu 2018年

6

私は@stewdebakerからのソリューションを実装しましたが、これは非常にうまく機能しますが、標準のJavaScriptの不格好な確認ではなく、素敵なブートストラップポップアップが必要でした。すでにngx-bootstrapを使用していると仮定すると、@ stwedebakerのソリューションを使用できますが、ここに表示されているものと「ガード」を交換します。またngx-bootstrap/modal、を紹介し、新しいものを追加する必要がありますConfirmationComponent

ガード

(「確認」をブートストラップモーダルを開く関数に置き換えます-新しいカスタムを表示します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>

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

そして、新しいものConfirmationComponentselectorhtmlテンプレートを使用せずに表示されるため、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 2018

この方法は私のニーズには問題ありませんでしたが、方法があるはずです。時間があれば、さらに開発を進めますが、しばらくの間、この回答を更新することはできません。
Chris Halcrow、2018

1

ソリューションは予想よりも簡単でした。代わりにhrefAngular Routing use routerLinkディレクティブでは処理されないため、使用しないでください。


1

2020年6月の回答:

この時点までに提案されたすべての解決策は、AngularのcanDeactivateガードの重大な既知の欠陥を処理しないことに注意してください。

  1. ユーザーがブラウザーの[戻る]ボタンをクリックすると、ダイアログが表示され、ユーザーは[ キャンセル ]をクリックします。
  2. ユーザーは、再びダイアログが表示さを「戻る」ボタンをクリックすると、ユーザーがクリックするCONFIRMを
  3. 注:ユーザーは2回戻ってナビゲートされるため、アプリから完全に削除される可能性もあります:(

これはここここ、そしてここで詳しく議論されました


この問題を安全に回避する、ここ示されている問題の私の解決策を参照してください*。これは、Chrome、Firefox、およびEdgeでテストされています。


* 重要な警告:この段階では、[戻る]ボタンをクリックすると転送履歴がクリアされますが、戻る履歴は保持されます。フォワード履歴を維持することが重要である場合、このソリューションは適切ではありません。私の場合、フォームに関しては通常、マスター/ディテールルーティング戦略を使用しているため、フォワード履歴を維持することは重要ではありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.