Предупреждать пользователя о несохраненных изменениях перед выходом на страницу
Я хочу предупредить пользователей о несохраненных изменениях, прежде чем они покинут конкретную страницу моего приложения angular 2. Обычно я использовал бы window.onbeforeunload
, но это не работает для одностраничных приложений.
Я обнаружил, что в angular 1 вы можете подключиться к событию $locationChangeStart
, чтобы создать окно confirm
для пользователя, но я не видел ничего, что показывает, как заставить это работать для angular 2, или если это событие еще присутствует. Я также видел плагины для ag1, которые предоставляют функции для onbeforeunload
, но опять же, я не видел способа использовать его для ag2.
Я надеюсь, что кто-то еще найдет решение этой проблемы; любой метод будет отлично работать для моих целей.
Ответы
Ответ 1
Маршрутизатор обеспечивает обратный вызов жизненного цикла 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]
}])
);
Ответ 2
Для того, чтобы охватывать охранник против браузера Освежает, закрывая окно и т.д. (см @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
события beforeunload
(например, обновление браузера, закрытие окна и т.д.). Навигация в пределах приложения Angular не затрагивается и будет правильно отображать указанное вами предупреждающее сообщение с подтверждением. Те, кому нужно поддерживать IE/Edge и не хотят, чтобы false
показывало/хотели более подробное сообщение в диалоговом окне подтверждения при beforeunload
события beforeunload
могут также захотеть увидеть ответ @JasperRisseeuw для обходного пути.
Ответ 3
Пример с @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)";
}
}
}
Ответ 4
Я реализовал решение от @stewdebaker, которое работает очень хорошо, однако я хотел хорошее всплывающее окно начальной загрузки вместо неуклюжего стандартного подтверждения JavaScript. Предполагая, что вы уже используете ngx-bootstrap, вы можете использовать решение @stwedebaker, но поменяйте 'Guard' на тот, который я показываю здесь. Вам также необходимо ввести ngx-bootstrap/modal
и добавить новый 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;
})
}
}
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
будет отображаться без использования 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]
Ответ 5
Решение было проще, чем ожидалось, не используйте href
, потому что вместо этого он не обрабатывается директивой Angular Routing use routerLink
.
Ответ 6
Решение зависит от того, почему вы хотите предотвратить перезагрузку страницы. Если вы хотите предотвратить это, потому что могут быть несохраненные изменения, вы фактически должны предотвратить два разных поведения:
- Перезагрузите страницу браузера. Вы можете добиться этого, создав HostListener для события beforeunload (аналогично вашей попытке), например:
@HostListener('window:beforeunload', ['$event'])
beforeUnloadHander() {
// or directly false
this.allowRedirect;
}
- Угловое изменение маршрутизации (если у вас есть маршрутизация): для этого необходимо использовать защиту деактивации на маршруте, который вы хотите заблокировать, есть много способов, но наиболее ценным является тот, который использует реализацию интерфейса:
I. Интерфейс устанавливает пару полей, используемых в угловой защите, чтобы проверить, можем ли мы изменить путь маршрутизатора:
import { Observable } from "rxjs";
import { HostListener } from "@angular/core";
// see https://scotch.io/courses/routing-angular-2-applications/candeactivate
// implementing this interface with a component in angular you can implement a candeactivate
// guard that automatically checks if there is the canDeactivate function and
// it allows to navigate out of the route or not
export default interface LockableComponent {
allowRedirect: boolean;
canDeactivate(): boolean;
}
II. Каждый компонент должен реализовать этот интерфейс с помощью метода canDeactivate или поля allowRedirect (многократно используемого в HostListener для задачи # 1) и должен возвращать логическое значение, указывающее, разрешена или нет навигация.
III. Создайте защиту маршрутизатора, которая проверяет поля этого компонента на деактивацию:
canDeactivate(
component: LockableComponent,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
if (
(component.allowRedirect === false ||
(component.canDeactivate && !component.canDeactivate()))
) {
// Angular bug! The stack navigation with candeactivate guard
// messes up all the navigation stack...
// see here: https://github.com/angular/angular/issues/13586#issuecomment-402250031
this.location.go(currentState.url);
if (
window.confirm("Sure man?")
) {
return true;
} else {
return false;
}
} else {
return true;
}
}
III. Установите защиту маршрутизатора canDeactivate в файле module.routing.ts:
const myRoutes: Routes = [
{
path: "locked-route-path",
component: ComponentThatImplementsLockedInterface,
canDeactivate: [TheCanDeactivateGuardJustMade]
}
//...
]