Попередити користувача про збережені зміни перед виходом на сторінку


112

Я хотів би попередити користувачів про незбережені зміни, перш ніж вони залишать певну сторінку мого додатка 2. Зазвичай я б використовував window.onbeforeunload, але це не працює для програм на одній сторінці.

Я виявив, що в кутовій 1 ви можете підключити $locationChangeStartподії, щоб підкинути confirmкоробку для користувача, але я не бачив нічого, що б показало, як зробити це робочим для кутового 2 або якщо ця подія ще присутня . Я також бачив плагіни для ag1, які забезпечують функціональність onbeforeunload, але знову ж таки, я не бачив жодного способу використовувати його для ag2.

Я сподіваюся, що хтось інший знайшов рішення цієї проблеми; будь-який метод буде добре працювати в моїх цілях.


2
Це працює для програм на одній сторінці, коли ви намагаєтесь закрити сторінку / вкладку. Тож будь-які відповіді на питання були б лише частковим рішенням, якщо вони ігнорують цей факт.
9ilsdx 9rvj 0ло

Відповіді:


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
На відміну від того, що просив ОП, CanDeactivate - це поточно - не зачепляється на події перед завантаженням (на жаль). Це означає, що якщо користувач намагається перейти до зовнішньої URL-адреси, закрийте вікно тощо. CanDeactivate не буде спрацьовано. Здається, це працює лише тоді, коли користувач перебуває в додатку.
Крістоф Відал

1
@ChristopheVidal правильний. Будь ласка, дивіться мою відповідь на рішення, яке також охоплює перехід до зовнішньої URL-адреси, закриття вікна, перезавантаження сторінки тощо
stewdebaker

Це працює при зміні маршрутів. Що робити, якщо це SPA? Будь-який інший спосіб досягти цього?
sujay kodamala

stackoverflow.com/questions/36763141/… Вам це знадобиться і для маршрутів. Якщо вікно закрите або відійде від поточного сайту, canDeactivateвін не працюватиме.
Günter 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подією по-різному від інших браузерів і включать слово falseв діалогове вікно підтвердження, коли beforeunloadподія активується (наприклад, оновлення браузера, закриття вікна тощо). Навігація далеко за межами програми Angular не вплине, і вона належним чином покаже вказане попередження про підтвердження. Ті, кому потрібно підтримувати IE / Edge і не хочуть falseпоказувати / хочуть більш детальне повідомлення у діалоговому вікні підтвердження, коли beforeunloadподія активується, також можуть побачити відповідь @ JasperRisseeuw для вирішення.


2
Це дуже добре працює @stewdebaker! У мене є одне доповнення до цього рішення, дивіться мою відповідь нижче.
Jasper Risseeuw

1
import {Observable} з 'rxjs / Observable'; відсутня у ComponentCanDeactivate
Махмуд Алі Кассем

1
Мені довелося додати @Injectable()до класу PendingChangesGuard. Крім того, мені довелося додати PendingChangesGuard моїм провайдерам у@NgModule
spottedmahn

Мені довелося додатиimport { HostListener } from '@angular/core';
fidev

4
Варто зауважити, що ви повинні повернути булевий випадок, якщо ви переходите з нього beforeunload. Якщо ви повернете спостережуване, воно не спрацює. Ви можете змінити свій інтерфейс для чого - то , як canDeactivate: (internalNavigation: true | undefined)і викликати ваш компонент , як це: return component.canDeactivate(true). Таким чином, ви можете перевірити, чи не рухаєтесь ви внутрішньо, щоб повернутися falseзамість спостережуваного.
jsgoupil

58

Приклад із @Hostlistener від stewdebaker працює дуже добре, але я вніс ще одну зміну до нього, оскільки IE і Edge відображають "помилковий", який повертається кінцевим користувачем методом 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, яке працює дуже добре, проте мені хотілося приємного спливаючого вікна завантаження замість незграбного стандартного підтвердження JavaScript. Якщо припустити, що ви вже використовуєте ngx-bootstrap, ви можете використовувати рішення @ stwedebaker, але поміняти "Guard" на ту, яку я показую тут. Вам також потрібно представити 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;
    })
  }
}

підтвердження.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>

підтвердження.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буде відображатися без використання 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

Має бути спосіб, хоча це рішення було в порядку для моїх потреб. Якщо я встигну, я буду розвивати щось далі, хоча мені не вдасться оновити цю відповідь надовго!
Кріс Халкроу

1

Рішення було простіше, ніж очікувалося, не використовуйте, hrefоскільки це не обробляється routerLinkдирективою використання Angular Routing .


1

Червень 2020 відповідь:

Зауважте, що всі рішення, запропоновані до цього моменту, не стосуються суттєвого відомого недоліку з canDeactivateохоронцями Angular :

  1. Користувач натискає кнопку "назад" у веб-переглядачі, діалогових вікнах і користувач натискає кнопку CANCEL .
  2. Користувач знову натискає кнопку «назад», відображається діалогове вікно, а користувач натискає ПІДТВЕРДЖИТИ .
  3. Примітка: користувач повертається назад 2 рази, що навіть може взагалі вийняти їх із програми :(

Про це йшлося тут , тут і довго, тут


Будь ласка, дивіться моє вирішення проблеми, продемонстрована тут, яка безпечно працює навколо цього питання *. Це було перевірено на Chrome, Firefox та Edge.


* ВАЖЛИВИЙ КАВАТ : На цьому етапі вище буде очищено історію перемоги при натисканні кнопки "Назад", але зберегти історію спинки. Це рішення не буде доцільним, якщо збереження вашої історії розвитку є життєво важливим. У моєму випадку я зазвичай використовую стратегію маршрутизації основної деталізації, коли мова йде про форми, тому збереження історії вперед не важливо.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.