เตือนผู้ใช้เกี่ยวกับการเปลี่ยนแปลงที่ไม่ได้บันทึกก่อนออกจากหน้า


112

ฉันต้องการเตือนผู้ใช้เกี่ยวกับการเปลี่ยนแปลงที่ไม่ได้บันทึกก่อนที่จะออกจากหน้าใดหน้าหนึ่งของแอป angular 2 ของฉัน ปกติฉันจะใช้window.onbeforeunloadแต่มันใช้ไม่ได้กับแอพพลิเคชั่นหน้าเดียว

ฉันพบว่าในเชิงมุม 1 คุณสามารถเชื่อมโยง$locationChangeStartเหตุการณ์เพื่อโยนconfirmกล่องให้กับผู้ใช้ แต่ฉันไม่เห็นอะไรเลยที่แสดงให้เห็นว่าจะทำให้สิ่งนี้ทำงานได้อย่างไรกับเชิงมุม 2 หรือหากเหตุการณ์นั้นยังคงอยู่ . ฉันยังเห็นปลั๊กอินสำหรับ ag1 ที่มีฟังก์ชันสำหรับonbeforeunloadแต่อีกครั้งฉันไม่เห็นวิธีใดที่จะใช้มันสำหรับ ag2

ฉันหวังว่าจะมีคนอื่นพบวิธีแก้ปัญหานี้ วิธีใดวิธีหนึ่งจะใช้ได้ดีสำหรับวัตถุประสงค์ของฉัน


2
ใช้งานได้กับแอปพลิเคชันหน้าเดียวเมื่อคุณพยายามปิดหน้า / แท็บ ดังนั้นคำตอบของคำถามจะเป็นเพียงคำตอบบางส่วนหากพวกเขาเพิกเฉยต่อข้อเท็จจริงนั้น
9ilsdx 9rvj 0lo

คำตอบ:


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

29
ไม่เหมือนกับสิ่งที่ OP ขอ CanDeactivate คือ - ปัจจุบัน - ไม่เชื่อมต่อกับเหตุการณ์ onbeforeunload (น่าเสียดาย) หมายความว่าหากผู้ใช้พยายามนำทางไปยัง URL ภายนอกให้ปิดหน้าต่างเป็นต้น CanDeactivate จะไม่ถูกทริกเกอร์ ดูเหมือนว่าจะใช้ได้เฉพาะเมื่อผู้ใช้อยู่ในแอป
Christophe Vidal

1
@ChristopheVidal ถูกต้อง โปรดดูคำตอบของฉันสำหรับวิธีแก้ปัญหาที่ครอบคลุมถึงการนำทางไปยัง URL ภายนอกการปิดหน้าต่างการโหลดหน้าเว็บซ้ำ ฯลฯ
stewdebaker

ใช้งานได้เมื่อเปลี่ยนเส้นทาง แล้วถ้าเป็นสปาล่ะ? มีวิธีอื่นในการบรรลุเป้าหมายนี้หรือไม่?
sujay kodamala

stackoverflow.com/questions/36763141/…คุณต้องใช้เส้นทางนี้เช่นกัน หากปิดหน้าต่างหรือออกจากไซต์ปัจจุบันcanDeactivateจะไม่ทำงาน
GünterZö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เหตุการณ์ที่แตกต่างจากเบราว์เซอร์อื่น ๆ และจะรวมคำfalseในกล่องโต้ตอบยืนยันเมื่อbeforeunloadเหตุการณ์เปิดใช้งาน (เช่นเบราว์เซอร์รีเฟรชปิดหน้าต่าง ฯลฯ ) การนำทางภายในแอป Angular ไม่ได้รับผลกระทบและจะแสดงข้อความเตือนการยืนยันที่คุณกำหนด ผู้ที่ต้องการสนับสนุน IE / Edge และไม่ต้องการfalseแสดง / ต้องการข้อความรายละเอียดเพิ่มเติมในกล่องโต้ตอบยืนยันเมื่อbeforeunloadเหตุการณ์เปิดใช้งานอาจต้องการดูคำตอบของ @ JasperRisseeuw เพื่อหาวิธีแก้ปัญหา


2
มันใช้งานได้ดีจริงๆ @stewdebaker! ฉันมีอีกหนึ่งวิธีแก้ปัญหานี้ดูคำตอบของฉันด้านล่าง
Jasper Risseeuw

1
นำเข้า {Observable} จาก 'rxjs / Observable'; หายไปจาก ComponentCanDeactivate
Mahmoud Ali Kassem

1
ฉันต้องเพิ่ม@Injectable()ในคลาส PendingChangesGuard นอกจากนี้ฉันต้องเพิ่ม PendingChangesGuard ให้กับผู้ให้บริการของฉันใน@NgModule
spottedmahn

ฉันต้องเพิ่มimport { HostListener } from '@angular/core';
fidev

4
beforeunloadมันคุ้มค่าที่สังเกตเห็นว่าคุณจะต้องกลับแบบบูลในกรณีที่คุณมีการสำรวจไปด้วย หากคุณส่งคืน Observable มันจะไม่ทำงาน คุณอาจต้องการที่จะเปลี่ยนอินเตอร์เฟซของคุณสำหรับสิ่งที่ต้องการและเรียกคอมโพเนนต์ของคุณเช่นนี้canDeactivate: (internalNavigation: true | undefined) return component.canDeactivate(true)ด้วยวิธีนี้คุณสามารถตรวจสอบได้ว่าคุณไม่ได้นำทางออกไปภายในเพื่อกลับfalseแทนที่จะเป็น Observable
jsgoupil

58

ตัวอย่างที่มี @Hostlistener จาก stewdebaker ใช้งานได้ดีจริงๆ แต่ฉันได้ทำการเปลี่ยนแปลงอีกครั้งเนื่องจาก IE และ Edge แสดงเมธอด "false" ที่ส่งคืนโดยเมธอด canDeactivate () บนคลาส MyComponent ให้กับผู้ใช้ปลายทาง

ส่วนประกอบ:

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

ทางออกเดียวที่ใช้งานได้คือวิธีนี้ (โดยใช้ Edge) อื่น ๆ ทั้งหมดใช้งานได้ แต่แสดงเฉพาะข้อความโต้ตอบเริ่มต้น (Chrome / Firefox) ไม่ใช่ข้อความของฉัน ... ฉันถามคำถามเพื่อทำความเข้าใจว่าเกิดอะไรขึ้น
Elmer Dantas

@ElmerDantas โปรดดูคำตอบของฉันสำหรับคำถามของคุณสำหรับคำอธิบายว่าเหตุใดข้อความโต้ตอบเริ่มต้นจึงแสดงใน Chrome / Firefox
Stewdebaker

2
ใช้งานได้จริงขออภัย! ฉันต้องอ้างอิงยามในผู้ให้บริการโมดูล
tudor.iliescu

6

ฉันได้ใช้วิธีแก้ปัญหาจาก @stewdebaker ซึ่งใช้งานได้ดี แต่ฉันต้องการป๊อปอัป bootstrap ที่ดีแทนการยืนยัน JavaScript มาตรฐานที่ไม่เป็นระเบียบ สมมติว่าคุณใช้ ngx-bootstrap อยู่แล้วคุณสามารถใช้โซลูชันของ @ stwedebaker ได้ แต่สลับ 'Guard' สำหรับอันที่ฉันแสดงไว้ที่นี่ คุณต้องแนะนำngx-bootstrap/modalและเพิ่มใหม่ConfirmationComponent:

ยาม

(แทนที่ 'ยืนยัน' ด้วยฟังก์ชันที่จะเปิดโมดอล 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 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();
    }
}

และเนื่องจากใหม่ConfirmationComponentจะแสดงโดยไม่ใช้selectorเทมเพลตใน html จึงจำเป็นต้องประกาศใน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

ต้องมีวิธีแม้ว่าวิธีนี้จะใช้ได้กับความต้องการของฉัน ถ้าฉันมีเวลาฉันจะพัฒนาสิ่งต่างๆต่อไปแม้ว่าฉันจะไม่สามารถอัปเดตคำตอบนี้ได้สักพักขออภัย!
Chris Halcrow

1

วิธีแก้ปัญหานั้นง่ายกว่าที่คาดไว้อย่าใช้hrefเพราะสิ่งนี้ไม่ได้รับการจัดการโดย Angular Routing ให้ใช้routerLinkคำสั่งแทน


1

คำตอบเดือนมิถุนายน 2020:

โปรดทราบว่าโซลูชันทั้งหมดที่เสนอจนถึงจุดนี้ไม่ได้จัดการกับข้อบกพร่องที่เป็นที่รู้จักอย่างมีนัยสำคัญกับcanDeactivateการ์ดป้องกันของ Angular :

  1. ผู้ใช้คลิกปุ่ม 'ย้อนกลับ' ในเบราว์เซอร์แสดงโต้ตอบผู้ใช้คลิกยกเลิก
  2. ผู้ใช้คลิกปุ่ม 'ย้อนกลับ' อีกครั้งแสดงโต้ตอบผู้ใช้คลิกCONFIRM
  3. หมายเหตุ: ผู้ใช้จะถูกย้อนกลับ 2 ครั้งซึ่งอาจนำพวกเขาออกจากแอพทั้งหมด :(

นี้ได้รับการกล่าวถึงที่นี่ , ที่นี่และที่มีความยาวที่นี่


โปรดดูวิธีแก้ปัญหาของฉันที่แสดงไว้ที่นี่ซึ่งสามารถแก้ไขปัญหานี้ได้อย่างปลอดภัย * สิ่งนี้ได้รับการทดสอบบน Chrome, Firefox และ Edge


* CAVEAT ที่สำคัญ : ในขั้นตอนนี้ข้อมูลข้างต้นจะล้างประวัติไปข้างหน้าเมื่อคลิกปุ่มย้อนกลับ แต่จะรักษาประวัติย้อนกลับไว้ วิธีการแก้ปัญหานี้จะไม่เหมาะสมหากการรักษาประวัติการส่งต่อของคุณมีความสำคัญ ในกรณีของฉันฉันมักจะใช้กลยุทธ์การกำหนดเส้นทางแบบรายละเอียดหลักเมื่อพูดถึงแบบฟอร์มดังนั้นการรักษาประวัติไปข้างหน้าจึงไม่สำคัญ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.