Advierta al usuario de cambios no guardados antes de salir de la página

Me gustaría advertir a los usuarios de los cambios no guardados antes de que abandonen una página particular de mi aplicación angular 2. Normalmente usaría window.onbeforeunload , pero eso no funciona para aplicaciones de una sola página.

Descubrí que en el ángulo 1, puede enganchar en el evento $locationChangeStart para arrojar un cuadro de confirm para el usuario, pero no he visto nada que muestre cómo hacerlo funcionar para el angular 2, o si ese evento es aún está presente. También he visto complementos para ag1 que proporcionan funcionalidad para onbeforeunload de la onbeforeunload , pero de nuevo, no he visto ninguna forma de usarlo para ag2.

Espero que alguien más haya encontrado una solución a este problema; cualquiera de los métodos funcionará bien para mis propósitos.

El enrutador proporciona una callback del ciclo de vida [CanDeactivate] https://angular.io/api/router/CanDeactivate, donde puede evitar el cambio de ruta.

para más detalles ver el tutorial de guardias

 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|Promise|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 {} 

original (enrutador RC.x)

 class CanActivateTeam implements CanActivate { constructor(private permissions: Permissions, private currentUser: UserToken) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable { return this.permissions.canActivate(this.currentUser, this.route.params.id); } } bootstrap(AppComponent, [ CanActivateTeam, provideRouter([{ path: 'team/:id', component: Team, canActivate: [CanActivateTeam] }]) ); 

Para cubrir también las protecciones contra actualizaciones del navegador, cerrar la ventana, etc. (ver el comentario de @ ChristopheVidal a la respuesta de Günter para más detalles sobre el tema), he encontrado útil agregar el decorador @HostListener a la implementación canDeactivate su clase para escuchar la beforeunload evento de window . Cuando se configura correctamente, esto protegerá contra la navegación interna y externa al mismo tiempo.

Por ejemplo:

Componente:

 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 { // 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 } } 

Guardia:

 import { CanDeactivate } from '@angular/router'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; export interface ComponentCanDeactivate { canDeactivate: () => boolean | Observable; } @Injectable() export class PendingChangesGuard implements CanDeactivate { canDeactivate(component: ComponentCanDeactivate): boolean | Observable { // 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.'); } } 

Rutas:

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

Módulo:

 import { PendingChangesGuard } from './pending-changes.guard'; import { NgModule } from '@angular/core'; @NgModule({ // ... providers: [PendingChangesGuard], // ... }) export class AppModule {} 

NOTA : como señaló @JasperRisseeuw, IE y Edge manejan el evento beforeunload diferente a otros navegadores e incluirá la palabra false en el cuadro de diálogo de confirmación cuando el evento beforeunload se active (por ejemplo, actualizaciones del navegador, cierre de la ventana, etc.). Navegar dentro de la aplicación angular no se ve afectado y mostrará correctamente su mensaje de advertencia de confirmación designado. Aquellos que necesitan soportar IE / Edge y no quieren que false muestre / desee un mensaje más detallado en el cuadro de diálogo de confirmación cuando se activa el evento beforeunload también pueden querer ver la respuesta de JasperRisseeuw para una solución alternativa.

El ejemplo con @Hostlistener de stewdebaker funciona muy bien, pero le hice un cambio más porque IE y Edge muestran el “falso” que devuelve el método canDeactivate () en la clase MyComponent para el usuario final.

Componente:

 import {ComponentCanDeactivate} from "./pending-changes.guard"; import { Observable } from 'rxjs'; // add this line export class MyComponent implements ComponentCanDeactivate { canDeactivate(): Observable | 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)"; } } } 

La solución fue más fácil de lo esperado, no use href porque Angular Routing utiliza la directiva routerLink .

Implementé la solución de @stewdebaker que funciona muy bien, sin embargo, quería una buena ventana emergente de arranque en lugar de la típica confirmación de JavaScript estándar. Suponiendo que ya está usando ngx-bootstrap, puede usar la solución de @ stwedebaker, pero cambie la ‘Guardia’ por la que estoy mostrando aquí. También debe introducir ngx-bootstrap/modal y agregar un nuevo ConfirmationComponent :

Guardia

(reemplace ‘confirmar’ con una función que abrirá un modo de arranque – mostrando un nuevo, ConfirmationComponent personalizado):

 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; } @Injectable() export class PendingChangesGuard implements CanDeactivate { modalRef: BsModalRef; constructor(private modalService: BsModalService) {}; canDeactivate(component: ComponentCanDeactivate): boolean | Observable { // 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

 

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

Y dado que el nuevo ConfirmationComponent se mostrará sin usar un selector en una plantilla html, debe declararse en entryComponents en su raíz app.module.ts (o lo que sea que nombre su módulo raíz). Realice los siguientes cambios a 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]