web-dev-qa-db-ja.com

未保存の変更を検出し、angularjsを使用してユーザーに警告する

以下はこれまでのコードです

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

ユーザーが続行するかどうかを決定できるように、未保存のデータがある場合にbrowser closeまたはurl redirectで警告する方法

36
iJade

このような何かがそれを行う必要があります:

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

この例では、AngularJSはルーティングを処理しないため、$ locationChangeStartのリスナーはトリガーされませんが、実際のAngularアプリケーションで動作するはずです。

71
Anders Ekdahl

@Andersの回答を拡張して、ディレクティブが破棄された場合(例:ルートが変更された場合)リスナー(リスターのバインドを解除)をクリーンアップし、使用法を一般化するための構文糖を追加しました。

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

使用法:コントローラの例(この例のcontrollerAs構文を使用)

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

        return true;
    };
}]);

テンプレート

<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

Andersの答えは問題なく動作しますが、Angular ui-routerを使用している場合は、'$stateChangeStart' の代わりに '$locationChangeStart'

16
Razan Paul

@Andersの回答を修正して、ディレクティブにハードコードされたフォーム名が含まれないようにしました。

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

以下にHTMLコードを示します。

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

たぶんそれは誰かに役立つでしょう。 https://github.com/umbrella-web/Angular-unsavedChanges

このサービスを使用すると、スコープ内のオブジェクト(フォームだけでなく)の未保存の変更をリッスンできます。

3
Mikhail

Anders Ekdahl の優れた答えをAngular 1.5コンポーネントで使用するには、コンポーネントのコントローラーに$scopeを挿入します。

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

受け入れられた答えは素晴らしいですが、いくつかのフォームはformタグをname属性とともに使用し、他の時には使用するため、フォームコントローラーのハンドルを一貫して適切に取得することに問題がありましたng-formディレクティブ。また、thisまたはvm型パターンを利用するTypeScriptスタイル関数を使用している場合<form name='$ctrl.myForm'...

私は誰もこれについて言及していないことに驚いていますが、私の修正はディレクティブのrequireプロパティを利用し、angularに参照を与えることでしたフォームコントローラー自体

以下の承認済みの回答を更新して、変更を表示し、requireプロパティとリンク関数の追加パラメーターに注目してください。

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

これにより、要素上にフォームコントローラーが見つからない場合、angularはエラーをスローするため、フォームコントローラーに適切なハンドルがあることを保証できます。

^のような修飾子を追加することもできます(require='^form'祖先フォームをプルするか、フォームがオプションの場合はrequire='?form'(これはディレクティブを壊しませんが、有効なフォームコントローラーのハンドルを自分で持っていることを確認する必要があります)。

0
Dillon