Detect unsaved changes and alert user using angularjs

49,734

Solution 1

Something like this should do it:

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

Note that the listener for $locationChangeStart isn't triggered in this example since AngularJS doesn't handle any routing in such a simple example, but it should work in an actual Angular application.

Solution 2

I've extended the @Anders answer to clean up listeners (unbind listers) when directive is destroyed (ex: when route changes), and added some syntactic sugar to generalise the usage.

confirmOnExit Directive:

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

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?">

Solution 3

Anders's answer works fine, However, for people who uses Angular ui-router, you should use '$stateChangeStart' instead of '$locationChangeStart'.

Solution 4

I modified the @Anders answer so that the directive does not contain the form name hard coded:

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

Here is the html code for it:

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

Solution 5

Maybe it will be helpful for someone. https://github.com/umbrella-web/Angular-unsavedChanges

Using this service you can listen unsaved changes for any object in the scope (not only the form)

Share:
49,734
iJade
Author by

iJade

JavaScript enthusiast

Updated on January 26, 2020

Comments

  • iJade
    iJade over 4 years

    Below is the code so far

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

    How to alert on browser close or url redirect in case there is some unsaved data, so that user can decide whether to continue?

  • iJade
    iJade over 11 years
    but this thing wont work for browser back button.any hack to get it working?
  • pdorgambide
    pdorgambide about 10 years
    Other directive that works with ui-router github.com/facultymatt/angular-unsavedChanges
  • Mikhail
    Mikhail about 10 years
    Awesome! but as I see it can listen forms only. Right?
  • Asterius
    Asterius over 9 years
    Great solution. Simple and efficient!
  • Jürgen 'Kashban' Wahlmann
    Jürgen 'Kashban' Wahlmann over 8 years
    This is great, but also triggers if the user navigates to an anker on the same page.
  • Jürgen 'Kashban' Wahlmann
    Jürgen 'Kashban' Wahlmann over 8 years
    Try this: if (current.split("#")[1] != next.split("#")[1] && $scope.confirmOnExit()) {... I compare the second String of the split array because the first hash is right behind index.html. To be absolutely sure the url contains an anker you have to do some more sophisticated comparison, perhaps with a regex.
  • pbuchheit
    pbuchheit over 8 years
    Just a quick note. If you use 'reloadOnSearch' to prevent navigation on $location.search changes, the '$locationChangeStart' event will still pop the dialog, even though the navigation would have been prevented. The '$routeChangeStart' event seems to be a better alternative in this situation.
  • Zymotik
    Zymotik about 8 years
    Note: for people who are using Angular ui-router, you should use '$stateChangeStart' instead of '$locationChangeStart'. Thanks @Blizzard
  • Zymotik
    Zymotik about 8 years
    I've added this as a comment to his answer, thanks @Blizzard
  • FilmiHero
    FilmiHero about 8 years
    What's the best way to ignore clicks coming from a Submit button on a form?
  • Patrik Beck
    Patrik Beck about 7 years
    I'd like to suggest following improvement: $scope[element.attr('name')].$dirty So it works for any generic form the directive is used on