web-dev-qa-db-fra.com

Avertir l'utilisateur des modifications non enregistrées avant de quitter la page

Je souhaite avertir les utilisateurs des modifications non enregistrées avant qu'ils ne quittent une page particulière de mon application angular 2. Normalement, j'utiliserais window.onbeforeunload, mais cela ne fonctionne pas pour les applications à page unique.

J'ai trouvé que dans angular 1, vous pouvez vous connecter à l'événement $locationChangeStart pour afficher une boîte confirm pour l'utilisateur, mais je n'ai rien vu qui montre comment faire fonctionner cela pour angular 2, ou si cet événement est même toujours présent. J'ai aussi vu plugins for ag1 qui fournit une fonctionnalité pour onbeforeunload, mais encore une fois, je n'ai pas vu de moyen de l'utiliser pour ag2.

J'espère que quelqu'un d'autre a trouvé une solution à ce problème; chaque méthode fonctionnera très bien pour mes besoins.

75
Whelch

Le routeur fournit un rappel de cycle de vie CanDeactivate

pour plus de détails, voir le tutoriel gardes

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

original (routeur 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]
  }])
);
60
Günter Zöchbauer

Pour couvrir également les gardes contre l'actualisation du navigateur, la fermeture de la fenêtre, etc. (voir le commentaire de @ ChristopheVidal à la réponse de Günter pour plus de détails), j'ai trouvé utile d'ajouter le décorateur @HostListener à l'implémentation canDeactivate de votre classe pour écouter les beforeunloadwindow un événement. Une fois configuré correctement, cela vous protégera contre la navigation interne et externe simultanément.

Par exemple:

Composant:

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

Garde:

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.');
  }
}

Itinéraires:

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

Module:

import { PendingChangesGuard } from './pending-changes.guard';
import { NgModule } from '@angular/core';

@NgModule({
  // ...
  providers: [PendingChangesGuard],
  // ...
})
export class AppModule {}

NOTE: Comme @JasperRisseeuw l'a souligné, IE et Edge traitent l'événement beforeunload différemment des autres navigateurs et incluront le mot false dans la boîte de dialogue de confirmation lors de l'activation de l'événement beforeunload (par ex. fermer la fenêtre, etc.). La navigation dans l'application Angular n'est pas affectée et affichera correctement votre message d'avertissement de confirmation désigné. Ceux qui ont besoin de supporter IE/Edge et qui ne veulent pas que false affiche/veulent un message plus détaillé dans la boîte de dialogue de confirmation lorsque l'événement beforeunload est activé peuvent également vouloir voir la réponse de @ JasperRisseeuw comme solution de contournement.

152
stewdebaker

L'exemple avec le @Hostlistener de stewdebaker fonctionne très bien, mais j'ai apporté une modification supplémentaire car IE et Edge affichent le "false" renvoyé par la méthode canDeactivate () de la classe MyComponent .

Composant:

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)";
    }
  }
}
45
Jasper Risseeuw

J'ai implémenté la solution de @stewdebaker qui fonctionne vraiment bien, mais je voulais un Nice bootstrap popup à la place de la confirmation JavaScript standard maladroite. En supposant que vous utilisiez déjà ngx-bootstrap, vous pouvez utiliser la solution de @ stwedebaker, mais échanger le 'Guard' pour celui que je montre ici. Vous devez également introduire ngx-bootstrap/modal et ajouter une nouvelle ConfirmationComponent:

Garde

(remplacez 'confirm' par une fonction qui ouvrira un modal d'amorçage - affichant une nouvelle variable ConfirmationComponent personnalisée):

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

Et comme la nouvelle ConfirmationComponent sera affichée sans utiliser une selector dans un modèle html, elle doit être déclarée dans entryComponents dans votre racine app.module.ts (ou peu importe le nom de votre module racine). Apportez les modifications suivantes à 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]
2
Chris Halcrow

La solution était plus simple que prévu, n'utilisez pas href car elle n'est pas gérée par Angular Routing à l'aide de la directive routerLink.

0
Byron Lopez