Предупредить пользователя о несохраненных изменениях перед тем, как покинуть страницу

112

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

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

Я надеюсь, что кто-то еще нашел решение этой проблемы; любой метод подойдет для моих целей.

Whelch
источник
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 -currently- не подключается к событию onbeforeunload (к сожалению). Это означает, что если пользователь пытается перейти по внешнему URL-адресу, закрыть окно и т. Д., CanDeactivate не сработает. Кажется, это работает, только когда пользователь находится в приложении.
Christophe Vidal
1
@ChristopheVidal прав. См. Мой ответ о решении, которое также охватывает переход по внешнему URL-адресу, закрытие окна, перезагрузку страницы и т. Д.
stewdebaker
Это работает при смене маршрута. А если это СПА? Есть ли другой способ добиться этого?
суджай кодамала
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
импортировать {Observable} из 'rxjs / Observable'; отсутствует в ComponentCanDeactivate
Махмуд Али Кассем
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), а не мой текст ... Я даже задал вопрос, чтобы понять, что происходит
Элмер Дантас
@ElmerDantas, пожалуйста, посмотрите мой ответ на ваш вопрос, чтобы узнать, почему диалоговое сообщение по умолчанию отображается в Chrome / Firefox.
stewdebaker
2
На самом деле это работает, извините! Пришлось ссылаться на охранника в провайдерах модулей.
tudor.iliescu 07
6

Я реализовал решение от @stewdebaker, которое работает очень хорошо, однако мне нужно было красивое всплывающее окно начальной загрузки вместо неуклюжего стандартного подтверждения 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;
    })
  }
}

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будет отображаться без использования a 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
Должен быть способ, хотя это решение подходило для моих нужд. Если у меня будет время, я буду развиваться дальше, хотя я не смогу обновить этот ответ довольно долго, извините!
Крис Хэлкроу,
1

Решение оказалось проще, чем ожидалось, не используйте, hrefпотому что это не обрабатывается routerLinkдирективой использования Angular Routing .

Байрон Лопес
источник
1

Ответ за июнь 2020 г .:

Обратите внимание, что все решения, предложенные до этого момента, не устраняют существенный известный недостаток средств canDeactivateзащиты Angular :

  1. Пользователь нажимает кнопку «Назад» в браузере, отображается диалоговое окно, и пользователь нажимает ОТМЕНА .
  2. Пользователь снова нажимает кнопку «Назад», отображается диалоговое окно, и пользователь нажимает ПОДТВЕРДИТЬ .
  3. Примечание: пользователь возвращается назад 2 раза, что может даже полностью вывести его из приложения :(

Это обсуждалось здесь , здесь и подробно здесь


Пожалуйста, ознакомьтесь с моим решением проблемы, продемонстрированным здесь, которое безопасно позволяет обойти эту проблему *. Это было протестировано в Chrome, Firefox и Edge.


* ВАЖНОЕ ПРЕДУПРЕЖДЕНИЕ : на этом этапе вышеупомянутое очищает историю пересылки при нажатии кнопки «Назад», но сохраняет историю назад. Это решение не будет подходящим, если сохранение вашей будущей истории жизненно важно. В моем случае, когда дело доходит до форм , я обычно использую стратегию маршрутизации « главный-подробный» , поэтому ведение прямой истории не имеет значения.

Стивен Пол
источник