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

112

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

J'ai trouvé que dans angular 1, vous pouvez vous connecter à l' $locationChangeStartévénement pour lancer une confirmboîte 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 également vu des plugins pour ag1 qui fournissent des fonctionnalités pour onbeforeunload, mais encore une fois, je n'ai vu aucun moyen de l'utiliser pour ag2.

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

Buccin
la source
2
Cela fonctionne pour les applications à page unique, lorsque vous essayez de fermer la page / l'onglet. Donc, toute réponse à la question ne serait qu'une solution partielle s'ils ignorent ce fait.
9ilsdx 9rvj 0lo

Réponses:

74

Le routeur fournit un rappel de cycle de vie CanDeactivate

pour plus de détails, voir le tutoriel sur les 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 {}

d'origine (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]
  }])
);
Günter Zöchbauer
la source
29
Contrairement à ce que l'OP a demandé, CanDeactivate n'accroche -currently- pas à l'événement onbeforeunload (malheureusement). Cela signifie que si l'utilisateur essaie de naviguer vers une URL externe, fermez la fenêtre, etc. CanDeactivate ne sera pas déclenché. Cela semble fonctionner uniquement lorsque l'utilisateur reste dans l'application.
Christophe Vidal
1
@ChristopheVidal a raison. Veuillez consulter ma réponse pour une solution qui couvre également la navigation vers une URL externe, la fermeture de la fenêtre, le rechargement de la page, etc.
stewdebaker
Cela fonctionne lors du changement d'itinéraire. Et si c'était SPA? Un autre moyen d'y parvenir?
sujay kodamala
stackoverflow.com/questions/36763141/... Vous auriez également ce besoin avec les routes. Si la fenêtre est fermée ou si vous quittez le site actuel, canDeactivatecela ne fonctionnera pas.
Günter Zöchbauer
214

Pour couvrir également les protections contre les actualisations 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 sur le problème), j'ai trouvé utile d'ajouter le @HostListenerdécorateur à l' canDeactivateimplémentation de votre classe pour écouter l' beforeunload windowévénement. Une fois configuré correctement, cela protégera à la fois la navigation dans l'application et la navigation externe.

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

REMARQUE : Comme @JasperRisseeuw l'a souligné, IE et Edge gèrent l' beforeunloadévénement différemment des autres navigateurs et incluront le mot falsedans la boîte de dialogue de confirmation lorsque l' beforeunloadévénement s'activera (par exemple, le navigateur s'actualise, ferme 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 prendre en charge IE / Edge et qui ne veulent falsepas afficher / veulent un message plus détaillé dans la boîte de dialogue de confirmation lorsque l' beforeunloadévénement s'active peuvent également vouloir voir la réponse de @ JasperRisseeuw pour une solution de contournement.

cuisinier
la source
2
Cela fonctionne vraiment bien @stewdebaker! J'ai un ajout à cette solution, voir ma réponse ci-dessous.
Jasper Risseeuw
1
import {Observable} de 'rxjs / Observable'; est absent de ComponentCanDeactivate
Mahmoud Ali Kassem
1
J'ai dû ajouter @Injectable()à la classe PendingChangesGuard. De plus, j'ai dû ajouter PendingChangesGuard à mes fournisseurs en@NgModule
spottedmahn
Je devais ajouterimport { HostListener } from '@angular/core';
fidev
4
Il est à noter que vous devez renvoyer un booléen au cas où vous seriez en train de naviguer beforeunload. Si vous renvoyez un observable, cela ne fonctionnera pas. Vous pouvez modifier votre interface pour quelque chose comme canDeactivate: (internalNavigation: true | undefined)et appeler votre composant comme ceci: return component.canDeactivate(true). De cette façon, vous pouvez vérifier si vous ne naviguez pas en interne pour revenir falseau lieu d'un observable.
jsgoupil
58

L'exemple avec @Hostlistener de stewdebaker fonctionne très bien, mais j'y ai apporté une autre modification car IE et Edge affichent le "false" qui est retourné par la méthode canDeactivate () sur la classe MyComponent à l'utilisateur final.

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)";
    }
  }
}
Jasper Risseeuw
la source
2
Bonne prise @JasperRisseeuw! Je ne savais pas que IE / Edge gérait cela différemment. C'est une solution très utile pour ceux qui ont besoin de prendre en charge IE / Edge et ne veulent pas que le falses'affiche dans la boîte de dialogue de confirmation. J'ai apporté une petite modification à votre réponse pour inclure le '$event'dans l' @HostListenerannotation, car cela est nécessaire pour pouvoir y accéder dans la unloadNotificationfonction.
stewdebaker
1
Merci, j'ai oublié de copier ", ['$ event']" de mon propre code, donc bonne prise de votre part aussi!
Jasper Risseeuw
la seule solution qui fonctionne était celle-ci (en utilisant Edge). tous les autres fonctionnent mais ne montrent que le message de dialogue par défaut (Chrome / Firefox), pas mon texte ... J'ai même posé une question pour comprendre ce qui se passe
Elmer Dantas
@ElmerDantas s'il vous plaît voir ma réponse à votre question pour une explication de pourquoi le message de dialogue par défaut s'affiche dans Chrome / Firefox.
stewdebaker
2
En fait, ça marche, désolé! J'ai dû référencer la garde dans les fournisseurs de modules.
tudor.iliescu
6

J'ai implémenté la solution de @stewdebaker qui fonctionne très bien, mais je voulais une belle fenêtre contextuelle de démarrage au lieu du JavaScript standard maladroit. En supposant que vous utilisez déjà ngx-bootstrap, vous pouvez utiliser la solution de @ stwedebaker, mais remplacez le «Guard» par celui que je montre ici. Vous devez également introduire ngx-bootstrap/modalet ajouter un nouveau ConfirmationComponent:

Garde

(remplacez 'confirm' par une fonction qui ouvrira un modal bootstrap - affichant un nouveau, personnalisé 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();
    }
}

Et puisque le nouveau ConfirmationComponentsera affiché sans utiliser de selectordans un modèle html, il doit être déclaré entryComponentsdans votre racine app.module.ts(ou quel que soit votre nom de 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]
Chris Halcrow
la source
1
y a-t-il une chance d'afficher un modèle personnalisé pour l'actualisation du navigateur?
k11k2
Il doit y avoir un moyen, même si cette solution répondait à mes besoins. Si j'ai le temps, je développerai davantage les choses, bien que je ne puisse pas mettre à jour cette réponse pendant un bon moment, désolé!
Chris Halcrow
1

La solution était plus facile que prévu, ne l'utilisez pas hrefcar elle n'est pas gérée par la routerLinkdirective d' utilisation du routage angulaire à la place.

Byron Lopez
la source
1

Réponse de juin 2020:

Notez que toutes les solutions proposées jusqu'à présent ne traitent pas d'un défaut connu significatif des canDeactivategardes Angular :

  1. L'utilisateur clique sur le bouton «retour» dans le navigateur, la boîte de dialogue s'affiche et l'utilisateur clique sur ANNULER .
  2. L'utilisateur clique à nouveau sur le bouton «retour», la boîte de dialogue s'affiche et l'utilisateur clique sur CONFIRMER .
  3. Remarque: l'utilisateur est renvoyé 2 fois, ce qui pourrait même le retirer complètement de l'application :(

Cela a été discuté ici , ici et en détail ici


Veuillez consulter ma solution au problème illustré ici qui fonctionne en toute sécurité autour de ce problème *. Cela a été testé sur Chrome, Firefox et Edge.


* AVERTISSEMENT IMPORTANT : À ce stade, ce qui précède effacera l'historique de transfert lorsque le bouton de retour est cliqué, mais conservera l'historique de retour. Cette solution ne sera pas appropriée si la préservation de votre historique avant est vitale. Dans mon cas, j'utilise généralement une stratégie de routage maître-détails en ce qui concerne les formulaires, il n'est donc pas important de conserver l'historique avant.

Stephen Paul
la source