web-dev-qa-db-fra.com

Détecter les modifications non enregistrées et alerter l'utilisateur à l'aide d'angularjs

Ci-dessous le code jusqu'ici

    <!doctype html>
<html ng-app>
<head>
    <script src="http://code.angularjs.org/1.1.2/angular.min.js"></script>
    <script type="text/javascript">
    function Ctrl($scope) {
        var initial = {text: 'initial value'};
        $scope.myModel = angular.copy(initial);
        $scope.revert = function() {
            $scope.myModel = angular.copy(initial);
            $scope.myForm.$setPristine();
        }
    }
    </script>
</head>
<body>
    <form name="myForm" ng-controller="Ctrl">
        myModel.text: <input name="input" ng-model="myModel.text">
        <p>myModel.text = {{myModel.text}}</p>
        <p>$pristine = {{myForm.$pristine}}</p>
        <p>$dirty = {{myForm.$dirty}}</p>
        <button ng-click="revert()">Set pristine</button>
    </form>
</body>
</html>

Comment alerter sur browser close ou url redirect au cas où il y aurait des données non sauvegardées, afin que l'utilisateur puisse décider de continuer?

35
iJade

Quelque chose comme ça devrait le faire:

<!doctype html>
<html ng-app="myApp">
<head>
    <script src="http://code.angularjs.org/1.1.2/angular.min.js"></script>
    <script type="text/javascript">
    function Ctrl($scope) {
        var initial = {text: 'initial value'};
        $scope.myModel = angular.copy(initial);
        $scope.revert = function() {
            $scope.myModel = angular.copy(initial);
            $scope.myForm.$setPristine();
        }
    }

    angular.module("myApp", []).directive('confirmOnExit', function() {
        return {
            link: function($scope, elem, attrs) {
                window.onbeforeunload = function(){
                    if ($scope.myForm.$dirty) {
                        return "The form is dirty, do you want to stay on the page?";
                    }
                }
                $scope.$on('$locationChangeStart', function(event, next, current) {
                    if ($scope.myForm.$dirty) {
                        if(!confirm("The form is dirty, do you want to stay on the page?")) {
                            event.preventDefault();
                        }
                    }
                });
            }
        };
    });
    </script>
</head>
<body>
    <form name="myForm" ng-controller="Ctrl" confirm-on-exit>
        myModel.text: <input name="input" ng-model="myModel.text">
        <p>myModel.text = {{myModel.text}}</p>
        <p>$pristine = {{myForm.$pristine}}</p>
        <p>$dirty = {{myForm.$dirty}}</p>
        <button ng-click="revert()">Set pristine</button>
    </form>
</body>
</html>

Notez que l'écouteur de $ locationChangeStart n'est pas déclenché dans cet exemple car AngularJS ne gère aucun routage dans un exemple aussi simple, mais qu'il devrait fonctionner dans une application Angular réelle.

70
Anders Ekdahl

J'ai étendu la réponse @Anders pour nettoyer les écouteurs (non-rattachés aux listes) lorsque la directive est détruite (par exemple, lorsque la route change), et ajouté du sucre syntaxique pour généraliser l'utilisation.

Directive confirmOnExit :

/**
 * @name confirmOnExit
 * 
 * @description
 * Prompts user while he navigating away from the current route (or, as long as this directive 
 * is not destroyed) if any unsaved form changes present.
 * 
 * @element Attribute
 * @scope
 * @param confirmOnExit Scope function which will be called on window refresh/close or AngularS $route change to
 *                          decide whether to display the Prompt or not.
 * @param confirmMessageWindow Custom message to display before browser refresh or closed.
 * @param confirmMessageRoute Custom message to display before navigating to other route.
 * @param confirmMessage Custom message to display when above specific message is not set.
 * 
 * @example
 * Usage:
 * Example Controller: (using controllerAs syntax in this example)
 * 
 *      angular.module('AppModule', []).controller('pageCtrl', [function () {
 *          this.isDirty = function () {
 *              // do your logic and return 'true' to display the Prompt, or 'false' otherwise.
 *              return true;
 *          };
 *      }]);
 * 
 * Template:
 * 
 *      <div confirm-on-exit="pageCtrl.isDirty()" 
 *          confirm-message-window="All your changes will be lost."
 *          confirm-message-route="All your changes will be lost. Are you sure you want to do this?">
 * 
 * @see
 * http://stackoverflow.com/a/28905954/340290
 * 
 * @author Manikanta G
 */
ngxDirectivesModule.directive('confirmOnExit', function() {
    return {
        scope: {
            confirmOnExit: '&',
            confirmMessageWindow: '@',
            confirmMessageRoute: '@',
            confirmMessage: '@'
        },
        link: function($scope, elem, attrs) {
            window.onbeforeunload = function(){
                if ($scope.confirmOnExit()) {
                    return $scope.confirmMessageWindow || $scope.confirmMessage;
                }
            }
            var $locationChangeStartUnbind = $scope.$on('$locationChangeStart', function(event, next, current) {
                if ($scope.confirmOnExit()) {
                    if(! confirm($scope.confirmMessageRoute || $scope.confirmMessage)) {
                        event.preventDefault();
                    }
                }
            });

            $scope.$on('$destroy', function() {
                window.onbeforeunload = null;
                $locationChangeStartUnbind();
            });
        }
    };
});

Utilisation: Exemple Controller : (en utilisant la syntaxe controllerAs dans cet exemple)

angular.module('AppModule', []).controller('pageCtrl', [function () {
    this.isDirty = function () {
        // do your logic and return 'true' to display the Prompt, or 'false' otherwise.

        return true;
    };
}]);

Modèle :

<div confirm-on-exit="pageCtrl.isDirty()" 
    confirm-message-window="All your changes will be lost." 
    confirm-message-route="All your changes will be lost. Are you sure you want to do this?">
33
manikanta

La réponse d'Anders fonctionne bien. Toutefois, pour les personnes utilisant un routeur angulaire ui, vous devez utiliser '$stateChangeStart' au lieu de '$locationChangeStart'.

16
Razan Paul

J'ai modifié la réponse @Anders afin que la directive ne contienne pas le nom du formulaire codé en dur:

    app.directive('confirmOnExit', function() {
        return {
            link: function($scope, elem, attrs, ctrl) {
                window.onbeforeunload = function(){
                    if ($scope[attrs["name"]].$dirty) {
                        return "Your edits will be lost.";
                    }
                }
            }
        };
    });

Voici le code html pour cela:

<form name="myForm" confirm-on-exit> 
9
PolinaC

Peut-être que ce sera utile pour quelqu'un . https://github.com/umbrella-web/Angular-unsavedChanges

En utilisant ce service, vous pouvez écouter les modifications non enregistrées pour n’importe quel objet de l’étendue (pas seulement le formulaire).

3
Mikhail

Pour utiliser l'excellente réponse de Anders Ekdahl avec un composant Angular 1.5, injectez le $scope dans le contrôleur du composant:

angular
  .module('myModule')
  .component('myComponent', {
    controller: ['$routeParams', '$scope',
      function MyController($routeParams, $scope) {
        var self = this;

        $scope.$on('$locationChangeStart', function (event, next, current) {
          if (self.productEdit.$dirty && !confirm('There are unsaved changes. Would you like to close the form?')) {
            event.preventDefault();
          }
        });
      }
    ]
  });
0
Wtower

La réponse acceptée est excellente, mais j’ai eu un problème avec l’obtention d’un descriptif correct sur mon contrôleur de formulaire de manière cohérente, car certains formulaires utilisent la balise form avec l’attribut name et, à d’autres moments, la directive ng-form. De même, si vous utilisez des fonctions de style TypeScript, utilisez le modèle de type this ou vm, par ex. <form name='$ctrl.myForm'...

Je suis surpris que personne d'autre n'ait mentionné cela, mais ma solution était d'utiliser la propriété require de la directive et de laisser angular me donner une référence au contrôleur de formulaire lui-même.

J'ai mis à jour la réponse acceptée ci-dessous pour afficher mes modifications, notez la propriété require et le paramètre supplémentaire de la fonction de liaison.

angular.module("myApp", []).directive('confirmOnExit', function() {
        return {
            restrict: 'A',
            require: 'form',
            link: function($scope, elem, attrs, form) {
                window.onbeforeunload = function(){
                    if (form.$dirty) {
                        return "The form is dirty, do you want to stay on the page?";
                    }
                }
                $scope.$on('$locationChangeStart', function(event, next, current) {
                    if (form.$dirty) {
                        if(!confirm("The form is dirty, do you want to stay on the page?")) {
                            event.preventDefault();
                        }
                    }
                });
            }
        };
    });

Avec cela, je peux garantir que je maîtrise bien le contrôleur de formulaire, car angular émettra une erreur s’il ne parvient pas à trouver un contrôleur de formulaire sur l’élément.

Vous pouvez également ajouter des modificateurs tels que ^ et? tel que require='^form' pour extraire un formulaire ancêtre ou require='?form' si le formulaire est optionnel (cela ne cassera pas la directive, mais vous devrez vérifier que vous avez vous-même un descripteur sur un contrôleur de formulaire valide).

0
Dillon