Detect unsaved changes and alert user using angularjs
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)
Comments
-
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
orurl redirect
in case there is some unsaved data, so that user can decide whether to continue? -
iJade over 11 yearsbut this thing wont work for browser back button.any hack to get it working?
-
pdorgambide about 10 yearsOther directive that works with ui-router github.com/facultymatt/angular-unsavedChanges
-
Mikhail about 10 yearsAwesome! but as I see it can listen forms only. Right?
-
Asterius over 9 yearsGreat solution. Simple and efficient!
-
Jürgen 'Kashban' Wahlmann over 8 yearsThis is great, but also triggers if the user navigates to an anker on the same page.
-
Jürgen 'Kashban' Wahlmann over 8 yearsTry 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 over 8 yearsJust 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 about 8 yearsNote: for people who are using Angular ui-router, you should use '$stateChangeStart' instead of '$locationChangeStart'. Thanks @Blizzard
-
Zymotik about 8 yearsI've added this as a comment to his answer, thanks @Blizzard
-
FilmiHero about 8 yearsWhat's the best way to ignore clicks coming from a Submit button on a form?
-
Patrik Beck about 7 yearsI'd like to suggest following improvement: $scope[element.attr('name')].$dirty So it works for any generic form the directive is used on