Warn user before leaving web page with unsaved changes - javascript

I have some pages with forms in my application.
How can I secure the form in such a way that if someone navigates away or closes the browser tab, they should be prompted to to confirm they really want to leave the form with unsaved data?

Short, wrong answer:
You can do this by handling the beforeunload event and returning a non-null string:
window.addEventListener("beforeunload", function (e) {
var confirmationMessage = 'It looks like you have been editing something. '
+ 'If you leave before saving, your changes will be lost.';
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
});
The problem with this approach is that submitting a form is also firing the unload event. This is fixed easily by adding the a flag that you're submitting a form:
var formSubmitting = false;
var setFormSubmitting = function() { formSubmitting = true; };
window.onload = function() {
window.addEventListener("beforeunload", function (e) {
if (formSubmitting) {
return undefined;
}
var confirmationMessage = 'It looks like you have been editing something. '
+ 'If you leave before saving, your changes will be lost.';
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
});
};
Then calling the setter when submitting:
<form method="post" onsubmit="setFormSubmitting()">
<input type="submit" />
</form>
But read on...
Long, correct answer:
You also don't want to show this message when the user hasn't changed anything on your forms. One solution is to use the beforeunload event in combination with a "dirty" flag, which only triggers the prompt if it's really relevant.
var isDirty = function() { return false; }
window.onload = function() {
window.addEventListener("beforeunload", function (e) {
if (formSubmitting || !isDirty()) {
return undefined;
}
var confirmationMessage = 'It looks like you have been editing something. '
+ 'If you leave before saving, your changes will be lost.';
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
});
};
Now to implement the isDirty method, there are various approaches.
You can use jQuery and form serialization, but this approach has some flaws. First you have to alter the code to work on any form ($("form").each() will do), but the greatest problem is that jQuery's serialize() will only work on named, non-disabled elements, so changing any disabled or unnamed element will not trigger the dirty flag. There are workarounds for that, like making controls readonly instead of enabling, serializing and then disabling the controls again.
So events seem the way to go. You can try listening for keypresses. This event has a few issues:
Won't trigger on checkboxes, radio buttons, or other elements that are being altered through mouse input.
Will trigger for irrelevant keypresses like the Ctrl key.
Won't trigger on values set through JavaScript code.
Won't trigger on cutting or pasting text through context menus.
Won't work for virtual inputs like datepickers or checkbox/radiobutton beautifiers which save their value in a hidden input through JavaScript.
The change event also doesn't trigger on values set from JavaScript code, so also won't work for virtual inputs.
Binding the input event to all inputs (and textareas and selects) on your page won't work on older browsers and, like all event handling solutions mentioned above, doesn't support undo. When a user changes a textbox and then undoes that, or checks and unchecks a checkbox, the form is still considered dirty.
And when you want to implement more behavior, like ignoring certain elements, you'll have even more work to do.
Don't reinvent the wheel:
So before you think about implementing those solutions and all required workarounds, realize you're reinventing the wheel and you're prone to running into problems others have already solved for you.
If your application already uses jQuery, you may as well use tested, maintained code instead of rolling your own, and use a third-party library for all of this.
jquery.dirty (suggested by #troseman in the comments) provides functions for properly detecting whether a form has been changed or not, and preventing the user from leaving the page while displaying a prompt. It also has other useful functions like resetting the form, and setting the current state of the form as the "clean" state. Example usage:
$("#myForm").dirty({preventLeaving: true});
An older, currently abandoned project, is jQuery's Are You Sure? plugin, which also works great; see their demo page. Example usage:
<script src="jquery.are-you-sure.js"></script>
<script>
$(function() {
$('#myForm').areYouSure(
{
message: 'It looks like you have been editing something. '
+ 'If you leave before saving, your changes will be lost.'
}
);
});
</script>
Custom messages not supported everywhere
Do note that since 2011 already, Firefox 4 didn't support custom messages in this dialog. As of april 2016, Chrome 51 is being rolled out in which custom messages are also being removed.
Some alternatives exist elsewhere on this site, but I think a dialog like this is clear enough:
Do you want to leave this site?
Changes you made may not be saved.
Leave Stay

Check out the JavaScript onbeforeunload event. It's non-standard JavaScript introduced by Microsoft, however it works in most browsers and their onbeforeunload documentation has more information and examples.

Universal solution requiring no configuration that automatically detects all input modification, including contenteditable elements:
"use strict";
(() => {
const modified_inputs = new Set;
const defaultValue = "defaultValue";
// store default values
addEventListener("beforeinput", (evt) => {
const target = evt.target;
if (!(defaultValue in target || defaultValue in target.dataset)) {
target.dataset[defaultValue] = ("" + (target.value || target.textContent)).trim();
}
});
// detect input modifications
addEventListener("input", (evt) => {
const target = evt.target;
let original;
if (defaultValue in target) {
original = target[defaultValue];
} else {
original = target.dataset[defaultValue];
}
if (original !== ("" + (target.value || target.textContent)).trim()) {
if (!modified_inputs.has(target)) {
modified_inputs.add(target);
}
} else if (modified_inputs.has(target)) {
modified_inputs.delete(target);
}
});
// clear modified inputs upon form submission
addEventListener("submit", (evt) => {
modified_inputs.clear();
// to prevent the warning from happening, it is advisable
// that you clear your form controls back to their default
// state with evt.target.reset() or form.reset() after submission
});
// warn before closing if any inputs are modified
addEventListener("beforeunload", (evt) => {
if (modified_inputs.size) {
const unsaved_changes_warning = "Changes you made may not be saved.";
evt.returnValue = unsaved_changes_warning;
return unsaved_changes_warning;
}
});
})();

via jquery
$('#form').data('serialize',$('#form').serialize()); // On load save form current state
$(window).bind('beforeunload', function(e){
if($('#form').serialize()!=$('#form').data('serialize'))return true;
else e=null; // i.e; if form state change show warning box, else don't show it.
});
You can Google JQuery Form Serialize function, this will collect all form inputs and save it in array. I guess this explain is enough :)

Built on top of Wasim A.'s excellent idea to use serialization. The problem there was that the warning was also shown when the form was being submitted. This has been fixed here.
var isSubmitting = false
$(document).ready(function () {
$('form').submit(function(){
isSubmitting = true
})
$('form').data('initial-state', $('form').serialize());
$(window).on('beforeunload', function() {
if (!isSubmitting && $('form').serialize() != $('form').data('initial-state')){
return 'You have unsaved changes which will not be saved.'
}
});
})
It has been tested in Chrome and IE 11.

Based on the previous answers, and cobbled together from various places in stack overflow, here is the solution I came up with which handles the case when you actually want to submit your changes:
window.thisPage = window.thisPage || {};
window.thisPage.isDirty = false;
window.thisPage.closeEditorWarning = function (event) {
if (window.thisPage.isDirty)
return 'It looks like you have been editing something' +
' - if you leave before saving, then your changes will be lost.'
else
return undefined;
};
$("form").on('keyup', 'textarea', // You can use input[type=text] here as well.
function () {
window.thisPage.isDirty = true;
});
$("form").submit(function () {
QC.thisPage.isDirty = false;
});
window.onbeforeunload = window.thisPage.closeEditorWarning;
It's worth noting that IE11 seems to require that the closeEditorWarning function returns undefined for it not to show an alert.

The following one-liner has worked for me.
window.onbeforeunload = s => modified ? "" : null;
Just set modified to true or false depending on the state of your application.

You can use serialize() to create a URL encoded text string by serializing form values and check whether the form has changed beforeunload
$(document).ready(function(){
var form = $('#some-form'),
original = form.serialize()
form.submit(function(){
window.onbeforeunload = null
})
window.onbeforeunload = function(){
if (form.serialize() != original)
return 'Are you sure you want to leave?'
}
})
Refer this link https://coderwall.com/p/gny70a/alert-when-leaving-page-with-unsaved-form
Written by Vladimir Sidorenko

Following code works great. You need to reach your form elements' input changes via id attribute:
var somethingChanged=false;
$('#managerForm input').change(function() {
somethingChanged = true;
});
$(window).bind('beforeunload', function(e){
if(somethingChanged)
return "You made some changes and it's not saved?";
else
e=null; // i.e; if form state change show warning box, else don't show it.
});
});

Tested Eli Grey's universal solution, only worked after I simplified the code to
'use strict';
(() => {
const modified_inputs = new Set();
const defaultValue = 'defaultValue';
// store default values
addEventListener('beforeinput', evt => {
const target = evt.target;
if (!(defaultValue in target.dataset)) {
target.dataset[defaultValue] = ('' + (target.value || target.textContent)).trim();
}
});
// detect input modifications
addEventListener('input', evt => {
const target = evt.target;
let original = target.dataset[defaultValue];
let current = ('' + (target.value || target.textContent)).trim();
if (original !== current) {
if (!modified_inputs.has(target)) {
modified_inputs.add(target);
}
} else if (modified_inputs.has(target)) {
modified_inputs.delete(target);
}
});
addEventListener(
'saved',
function(e) {
modified_inputs.clear()
},
false
);
addEventListener('beforeunload', evt => {
if (modified_inputs.size) {
const unsaved_changes_warning = 'Changes you made may not be saved.';
evt.returnValue = unsaved_changes_warning;
return unsaved_changes_warning;
}
});
})();
The modifications to his is deleted the usage of target[defaultValue] and only use target.dataset[defaultValue] to store the real default value.
And I added a 'saved' event listener where the 'saved' event will be triggered by yourself on your saving action succeeded.
But this 'universal' solution only works in browsers, not works in app's webview, for example, wechat browsers.
To make it work in wechat browsers(partially) also, another improvements again:
'use strict';
(() => {
const modified_inputs = new Set();
const defaultValue = 'defaultValue';
// store default values
addEventListener('beforeinput', evt => {
const target = evt.target;
if (!(defaultValue in target.dataset)) {
target.dataset[defaultValue] = ('' + (target.value || target.textContent)).trim();
}
});
// detect input modifications
addEventListener('input', evt => {
const target = evt.target;
let original = target.dataset[defaultValue];
let current = ('' + (target.value || target.textContent)).trim();
if (original !== current) {
if (!modified_inputs.has(target)) {
modified_inputs.add(target);
}
} else if (modified_inputs.has(target)) {
modified_inputs.delete(target);
}
if(modified_inputs.size){
const event = new Event('needSave')
window.dispatchEvent(event);
}
});
addEventListener(
'saved',
function(e) {
modified_inputs.clear()
},
false
);
addEventListener('beforeunload', evt => {
if (modified_inputs.size) {
const unsaved_changes_warning = 'Changes you made may not be saved.';
evt.returnValue = unsaved_changes_warning;
return unsaved_changes_warning;
}
});
const ua = navigator.userAgent.toLowerCase();
if(/MicroMessenger/i.test(ua)) {
let pushed = false
addEventListener('needSave', evt => {
if(!pushed) {
pushHistory();
window.addEventListener("popstate", function(e) {
if(modified_inputs.size) {
var cfi = confirm('确定要离开当前页面嘛?' + JSON.stringify(e));
if (cfi) {
modified_inputs.clear()
history.go(-1)
}else{
e.preventDefault();
e.stopPropagation();
}
}
}, false);
}
pushed = true
});
}
function pushHistory() {
var state = {
title: document.title,
url: "#flag"
};
window.history.pushState(state, document.title, "#flag");
}
})();

var unsaved = false;
$(":input").change(function () {
unsaved = true;
});
function unloadPage() {
if (unsaved) {
alert("You have unsaved changes on this page. Do you want to leave this page and discard your changes or stay on this page?");
}
}
window.onbeforeunload = unloadPage;

Short answer:
let pageModified = true
window.addEventListener("beforeunload",
() => pageModified ? 'Close page without saving data?' : null
)

The solution by Eerik Sven Puudist ...
var isSubmitting = false;
$(document).ready(function () {
$('form').submit(function(){
isSubmitting = true
})
$('form').data('initial-state', $('form').serialize());
$(window).on('beforeunload', function() {
if (!isSubmitting && $('form').serialize() != $('form').data('initial-state')){
return 'You have unsaved changes which will not be saved.'
}
});
})
... spontaneously did the job for me in a complex object-oriented setting without any changes necessary.
The only change I applied was to refer to the concrete form (only one form per file) called "formForm" ('form' -> '#formForm'):
<form ... id="formForm" name="formForm" ...>
Especially well done is the fact that the submit button is being "left alone".
Additionally, it works for me also with the lastest version of Firefox (as of February 7th, 2019).

Adding to te idea of #codecaster
you could add this to every page with a form (in my case i use it in global way so only on forms would have this warn) change his function to
if ( formSubmitting || document.getElementsByTagName('form').length == 0)
Also put on forms submit including login and in cancel buttons links so when person press cancel or submit the form won't trigger the warn also in every page witouth a form...
<a class="btn btn-danger btn-md" href="back/url" onclick="setFormSubmitting()">Cancel</a>

You could check for a detailed explanation here:
http://techinvestigations.redexp.in/comparison-of-form-values-on-load-and-before-close/
The main code:
function formCompare(defaultValues, valuesOnClose) {
// Create arrays of property names
var aPropsFormLoad = Object.keys(defaultValues);
var aPropsFormClose = Object.keys(valuesOnClose);
// If number of properties is different,
// objects are not equivalent
if (aPropsFormLoad.length != aPropsFormClose.length) {
return false;
}
for (var i = 0; i < aPropsFormLoad.length; i++) {
var propName = aPropsFormLoad[i];
// If values of same property are not equal,
// objects are not equivalent
if (defaultValues[aPropsFormLoad]+"" !== valuesOnClose[aPropsFormLoad]+"") {
return false;
}
}
// If we made it this far, objects
// are considered equivalent
return true;
}
//add polyfill for older browsers, as explained on the link above
//use the block below on load
for(i=0; i < document.forms[0].elements.length; i++){
console.log("The field name is: " + document.forms[0].elements[i].name +
" and it’s value is: " + document.forms[0].elements[i].value );
aPropsFormLoad[i] = document.forms[0].elements[i].value;
}
//create a similar array on window unload event.
//and call the utility function
if (!formCompare(aPropsOnLoad, aPropsOnClose))
{
//perform action:
//ask user for confirmation or
//display message about changes made
}

I did it differently, sharing here so that someone can get help, tested only with Chrome.
I wanted to warn user before closing the tab only if there are some changes.
<input type="text" name="field" value="" class="onchange" />
var ischanged = false;
$('.onchange').change(function () {
ischanged = true;
});
window.onbeforeunload = function (e) {
if (ischanged) {
return "Make sure to save all changes.";
}
};
Works good, but got an-other issue, when i submit the form i get the unwanted warning, i saw lots of workaround on it, this is because onbeforeunload fires before onsubmit thats why we can't handle it in onsubmit event like onbeforeunload = null, but onclick event of submit button fires before these both events, so i updated the code
var isChanged = false;
var isSubmit = false;
window.onbeforeunload = function (e) {
if (isChanged && (!isSubmit)) {
return "Make sure to save all changes.";
}
};
$('#submitbutton').click(function () {
isSubmit = true;
});
$('.onchange').change(function () {
isChanged = true;
});

I made following code. It can compare changes in all fields (except those marked with .ignoreDirty class) or optionally for currently visible fields only. It can be reinitialized for new fields added by Javascript. From that reason I save not the form status but the status of each control.
/* Dirty warning for forms */
dirty = (skipHiddenOrNullToInit) => {
/* will return True if there are changes in form(s)
for first initialization you can use both: .dirty(null) or .dirty() (ignore its result)
.dirty(null) will (re)initialize all controls - in addititon use it after Save if you stay on same page
.dirty() will initialize new controls - in addititon use it if you add new fields with JavaScript
then
.dirty() (or: .dirty(false)) says if data are changed without regard to hidden fields
.dirty(true) says if data are changed with regard to hidden fields (ie. fields with .d-none or .hidden class)
controls with .ignoreDirty class will be skipped always
previous about .d-none, .hidden, .ignoreDirty applies to the control itself and all its ancestors
*/
let isDirty = false;
let skipSelectors = '.ignoreDirty';
if (skipHiddenOrNullToInit) {
skipSelectors += ', .d-none, .hidden'
} else if (skipHiddenOrNullToInit === undefined) {
skipHiddenOrNullToInit = false;
}
$('input, select').each(
function(_idx, el) {
if ($(el).prop('type') !== 'hidden') {
let dirtyInit = $(el).data('dirty-init');
if (skipHiddenOrNullToInit === null || dirtyInit === undefined) {
try {
isChromeAutofillEl = $(el).is(":-webkit-autofill");
} catch (error) {
isChromeAutofillEl = false;
}
if (isChromeAutofillEl && $(el).data('dirty-init') === undefined) {
setTimeout(function() { // otherwise problem with Chrome autofilled controls
$(el).data('dirty-init', $(el).val());
}, 200)
} else {
$(el).data('dirty-init', $(el).val());
}
} else if ($(el).closest(skipSelectors).length === 0 && dirtyInit !== $(el).val()) {
isDirty = true;
return false; // breaks jQuery .each
}
}
}
);
return isDirty;
}
I have additional troubles with Chrome autofill values because it is difficult to initizialize and have them loaded already. So I do not initialize on page load but in any focusin event. (But: Maybe there is still problem with control values changed by JavaScript.) I use following code which I call at page load:
let init_dirty = (ifStayFunc) => {
/* ifStayFunc: optional callback when user decides to stay on page
use .clearDirty class to avoid warning on some button, however:
if the button fires JavaScript do't use .clearDirty class and instead
use directly dirty(null) in code - to be sure it will run before window.location */
$('input, select').on('focusin', function(evt) {
if (!$('body').data('dirty_initialized')) {
dirty();
$('body').data('dirty_initialized', true);
}
});
window.addEventListener('beforeunload', (evt) => {
if (dirty(true)) {
if (ifStayFunc) {
ifStayFunc();
}
evt.preventDefault();
evt.returnValue = ''; // at least Google Chrome requires this
}
});
$('.clearDirty').on('click', function(evt) {
dirty(null);
});
};
So, I add the .clearDirty class to the buttons which provide Save and that way I prevent the warning in this case.
Callback ifStayFunc allows me to do something if user will Stay on Page while he is warned. Typically I can show additional Save Button (if I have still visible only some default/primary button, which makes Safe+SomethingMore - and I want allow Save withou this "SomethingMore").

Related

Conditional onbeforeunload event in a page

window.onbeforeunload = function(evt) {
var message = 'Are you sure you want to leave the page. All data will be lost!';
if (typeof evt === 'undefined') {
evt = window.event;
}
if (evt && !($("#a_exit").click)) {
evt.returnValue = message;
}
return message;
};
I want user to leave the page clicking to the link (has id ="a_exit") only. In other circumstances such as refreshing the page, clicking another link, user will be prompted that if he/she wants to leave the page. I have tried to use the code above. It still asks me if I want to go away when I click the exit link.
$(window).bind('beforeunload',function() {
return "'Are you sure you want to leave the page. All data will be lost!";
});
$('#a_exit').live('click',function() {
$(window).unbind('beforeunload');
});
Above works for me
My quick example of the conditional prompt before leaving page:
window.onbeforeunload = confirmExit;
function confirmExit(event) {
var messageText = tinymce.get('mMessageBody').getContent();
messageText = messageText.trim();
// ... whatever you want
if (messageText != "")
return true;
else
return void (0);
};
It works under Chrome, FF.
You can return null when you do not want the prompt to show. Tested on Chrome 79.
window.onbeforeunload = () => {
if(shouldPrompt){
return true
}else{
return null
}
}
BTW modern browser no longer support custom onBeforeUnload promp text.
It will always prompt you if you want to leave the page. It's a security issue and cannot be worked-around.
Moreover, anything you return in the onbeforeunload event handler that is not void will be treated as the message for the prompt. Refer to this article: https://developer.mozilla.org/en-US/docs/Web/API/Window.onbeforeunload
This worked for me.
window.onbeforeunload = function(){
if(someBoolean) {
return 'message'
}else{
window.onbeforeunload = null;
}
}
You could just remove window.onbeforeunload in the click handler.
How about a variable to decide, if the link was clicked?
var exitClicked = false;
$("#a_exit").click(function() {
exitClicked = true;
});
window.onbeforeunload = function(evt) {
//...
if(evt && !exitClicked) {
evt.returnValue = message;
}
//...
};
You only set exitClicked = true when the link was really clicked and afterwards, before unloading you can simply check this variable.
This is quite old but I wanted just to change the code given that .live is no longer working in Jquery. So, the updated version would be:
$(window).bind('beforeunload',function() {
return "'Are you sure you want to leave the page. All data will be lost!";
});
$('#a_exit').on("click", function(){
$(window).unbind('beforeunload');
});

how can i prevent onbeforeunload from firing when a certain condition is met?

i have a page on which i want to confirm if the user wants to leave.
i have to confirm only when a certain condition is met so i wrote code like this
var back=false;
back=//check if user pressed back button
window.onbeforeunload = function (e) {
alert(back); //this alerts true
if(back==true)
return false;
//e.preventDefault; --this does not work too
};
but this does not work. i mean when i click on back button this onbeforeunload still fires and i still get the confirmation message even when i m returning false.Whats can be wrong?
Thanks
Return a string if you want to offer an option to the user to abort the unload. Return nothing in other cases.
var back = false;
back = true; //Somewhere, the condition is set to true
window.onbeforeunload = function (e) {
if(back == true)
return "Are you sure to exit?";
}
$(window).bind('beforeunload',function() {
return "'Are you sure you want to leave the page. All data will be lost!";
});
$('#a_exit').live('click',function() {
$(window).unbind('beforeunload');
});
Try this. Above code is working in most of conditions.
For the sake of completeness here a more modern, recommended approach:
let warn = false;
window.addEventListener('beforeunload', e => {
if (!warn) return;
// Cancel the event
e.preventDefault();
// Chrome requires returnValue to be set
e.returnValue = '';
});
warn = true; // during runtime you change warn to true
Typically, it is better to use window.addEventListener() and the
beforeunload event, instead of onbeforeunload.
Source
The reason why your originally posted code didn't work is that false is a non-null value. If you would have returned null or undefined in the situation where you don't want to spawn a pop-up warning your code would have worked as expected.
The currently accepted answer works because JavaScript implicitly returns undefined at the end of the function.
Condition for back-end
var confirmExist = function (e) {
return true;
}
window.onbeforeunload = confirmExist;
http get, post request
.then(function(r)) {
window.onbeforeunload = null;
}
You could also consider not setting the window.beforeunload event untill your list of conditions are met.
var confirmUserToLeave = function () {
if (/* conditions are met */) {
window.unbeforeunload = function (e) {
/* whatever you want to do here */
};
} else {
window.unbeforeunload = undefined;
}
};
Then just call that method on certain events that might change the outcome of your 'conditions are met'.

How to determine which control in window.onbeforeunload in javascript caused the event

I have set up in javascript:
var onBeforeUnloadFired = false;
window.onbeforeunload = function (sender, args)
{
if(window.event){
if(!onBeforeUnloadFired) {
onBeforeUnloadFired = true;
window.event.returnValue = 'You will lose any unsaved changes!'; //IE
}
}
else {
return 'You will lose any unsaved changes!'; //FX
}
windows.setTimeout("ResetOnBeforeUnloadFired()", 1000);
}
function ResetOnBeforeUnloadFired() {
//Need this variable to prevent IE firing twice.
onBeforeUnloadFired = false;
}
I'm trying to achieve an edit screen where the user is warned before navigating away. It works fine except I get the pop up for normal post backs of button clicks. I'm hoping to avoid this so I'm figuring if I could determine which button was pressed it would work.
Does anybody know how to determine which button was pressed in the windows.onbeforeunload?
Alternatively anyone know a better approach to what I'm trying to achieve?
Solved this by putting into an update panel all edit items TextBoxes etc.
Now the windows.onbeforeunload only fires for components external to this.
Another method, if you can't "control" that deep you controls, is to mark somewhat the "good controls", that is the ones which should not trigger the away-navigation logic.
That is easily achievable setting a global javascript variable such as
var isGoodLink=false;
window.onbeforeunload = function (e) {
var message = "Whatever";
e = e || window.event;
if (!isGoodLink) {
// For IE and Firefox
if (e) {
e.returnValue = message;
}
// For Safari
return message;
}
};
function setGoodLink() {
isGoodLink=true;
}
And add the setGoodLink function on the events you want to keep safe:
<button type="button" onclick="javascript:setGoodLink() ">I am a good button!</button>

Detecting Unsaved Changes

I have a requirement to implement an "Unsaved Changes" prompt in an ASP .Net application. If a user modifies controls on a web form, and attempts to navigate away before saving, a prompt should appear warning them that they have unsaved changes, and give them the option to cancel and stay on the current page. The prompt should not display if the user hasn't touched any of the controls.
Ideally I'd like to implement this in JavaScript, but before I go down the path of rolling my own code, are there any existing frameworks or recommended design patterns for achieving this? Ideally I'd like something that can easily be reused across multiple pages with minimal changes.
Using jQuery:
var _isDirty = false;
$("input[type='text']").change(function(){
_isDirty = true;
});
// replicate for other input types and selects
Combine with onunload/onbeforeunload methods as required.
From the comments, the following references all input fields, without duplicating code:
$(':input').change(function () {
Using $(":input") refers to all input, textarea, select, and button elements.
One piece of the puzzle:
/**
* Determines if a form is dirty by comparing the current value of each element
* with its default value.
*
* #param {Form} form the form to be checked.
* #return {Boolean} <code>true</code> if the form is dirty, <code>false</code>
* otherwise.
*/
function formIsDirty(form) {
for (var i = 0; i < form.elements.length; i++) {
var element = form.elements[i];
var type = element.type;
if (type == "checkbox" || type == "radio") {
if (element.checked != element.defaultChecked) {
return true;
}
}
else if (type == "hidden" || type == "password" ||
type == "text" || type == "textarea") {
if (element.value != element.defaultValue) {
return true;
}
}
else if (type == "select-one" || type == "select-multiple") {
for (var j = 0; j < element.options.length; j++) {
if (element.options[j].selected !=
element.options[j].defaultSelected) {
return true;
}
}
}
}
return false;
}
And another:
window.onbeforeunload = function(e) {
e = e || window.event;
if (formIsDirty(document.forms["someForm"])) {
// For IE and Firefox
if (e) {
e.returnValue = "You have unsaved changes.";
}
// For Safari
return "You have unsaved changes.";
}
};
Wrap it all up, and what do you get?
var confirmExitIfModified = (function() {
function formIsDirty(form) {
// ...as above
}
return function(form, message) {
window.onbeforeunload = function(e) {
e = e || window.event;
if (formIsDirty(document.forms[form])) {
// For IE and Firefox
if (e) {
e.returnValue = message;
}
// For Safari
return message;
}
};
};
})();
confirmExitIfModified("someForm", "You have unsaved changes.");
You'll probably also want to change the registration of the beforeunload event handler to use LIBRARY_OF_CHOICE's event registration.
In the .aspx page, you need a Javascript function to tell whether or not the form info is "dirty"
<script language="javascript">
var isDirty = false;
function setDirty() {
isDirty = true;
}
function checkSave() {
var sSave;
if (isDirty == true) {
sSave = window.confirm("You have some changes that have not been saved. Click OK to save now or CANCEL to continue without saving.");
if (sSave == true) {
document.getElementById('__EVENTTARGET').value = 'btnSubmit';
document.getElementById('__EVENTARGUMENT').value = 'Click';
window.document.formName.submit();
} else {
return true;
}
}
}
</script>
<body class="StandardBody" onunload="checkSave()">
and in the codebehind, add the triggers to the input fields as well as resets on the submission/cancel buttons....
btnSubmit.Attributes.Add("onclick", "isDirty = 0;");
btnCancel.Attributes.Add("onclick", "isDirty = 0;");
txtName.Attributes.Add("onchange", "setDirty();");
txtAddress.Attributes.Add("onchange", "setDirty();");
//etc..
The following uses the browser's onbeforeunload function and jquery to capture any onchange event. IT also looks for any submit or reset buttons to reset the flag indicating changes have occurred.
dataChanged = 0; // global variable flags unsaved changes
function bindForChange(){
$('input,checkbox,textarea,radio,select').bind('change',function(event) { dataChanged = 1})
$(':reset,:submit').bind('click',function(event) { dataChanged = 0 })
}
function askConfirm(){
if (dataChanged){
return "You have some unsaved changes. Press OK to continue without saving."
}
}
window.onbeforeunload = askConfirm;
window.onload = bindForChange;
Thanks for the replies everyone. I ended up implementing a solution using JQuery and the Protect-Data plug-in. This allows me to automatically apply monitoring to all controls on a page.
There are a few caveats however, especially when dealing with an ASP .Net application:
When a user chooses the cancel option, the doPostBack function will throw a JavaScript error. I had to manually put a try-catch around the .submit call within doPostBack to suppress it.
On some pages, a user could perform an action that performs a postback to the same page, but isn't a save. This results in any JavaScript logic resetting, so it thinks nothing has changed after the postback when something may have. I had to implement a hidden textbox that gets posted back with the page, and is used to hold a simple boolean value indicating whether the data is dirty. This gets persisted across postbacks.
You may want some postbacks on the page to not trigger the dialog, such as a Save button. In this case, you can use JQuery to add an OnClick function which sets window.onbeforeunload to null.
Hopefully this is helpful for anyone else who has to implement something similar.
General Solution Supporting multiple forms in a given page (Just copy and paste in your project)
$(document).ready(function() {
$('form :input').change(function() {
$(this).closest('form').addClass('form-dirty');
});
$(window).bind('beforeunload', function() {
if($('form:not(.ignore-changes).form-dirty').length > 0) {
return 'You have unsaved changes, are you sure you want to discard them?';
}
});
$('form').bind('submit',function() {
$(this).closest('form').removeClass('form-dirty');
return true;
});
});
Note: This solution is combined from others' solutions to create a general integrated solution.
Features:
Just copy and paste into your app.
Supports Multiple Forms.
You can style or make actions dirty forms, since they've the class "form-dirty".
You can exclude some forms by adding the class 'ignore-changes'.
The following solution works for prototype (tested in FF, IE 6 and Safari). It uses a generic form observer (which fires form:changed when any fields of the form have been modified), which you can use for other stuff as well.
/* use this function to announce changes from your own scripts/event handlers.
* Example: onClick="makeDirty($(this).up('form'));"
*/
function makeDirty(form) {
form.fire("form:changed");
}
function handleChange(form, event) {
makeDirty(form);
}
/* generic form observer, ensure that form:changed is being fired whenever
* a field is being changed in that particular for
*/
function setupFormChangeObserver(form) {
var handler = handleChange.curry(form);
form.getElements().each(function (element) {
element.observe("change", handler);
});
}
/* installs a form protector to a form marked with class 'protectForm' */
function setupProtectForm() {
var form = $$("form.protectForm").first();
/* abort if no form */
if (!form) return;
setupFormChangeObserver(form);
var dirty = false;
form.observe("form:changed", function(event) {
dirty = true;
});
/* submitting the form makes the form clean again */
form.observe("submit", function(event) {
dirty = false;
});
/* unfortunatly a propper event handler doesn't appear to work with IE and Safari */
window.onbeforeunload = function(event) {
if (dirty) {
return "There are unsaved changes, they will be lost if you leave now.";
}
};
}
document.observe("dom:loaded", setupProtectForm);
Here's a javascript / jquery solution that is simple. It accounts for "undos" by the user, it is encapsulated within a function for ease of application, and it doesn't misfire on submit. Just call the function and pass the ID of your form.
This function serializes the form once when the page is loaded, and again before the user leaves the page. If the two form states are different, the prompt is shown.
Try it out: http://jsfiddle.net/skibulk/Ydt7Y/
function formUnloadPrompt(formSelector) {
var formA = $(formSelector).serialize(), formB, formSubmit = false;
// Detect Form Submit
$(formSelector).submit( function(){
formSubmit = true;
});
// Handle Form Unload
window.onbeforeunload = function(){
if (formSubmit) return;
formB = $(formSelector).serialize();
if (formA != formB) return "Your changes have not been saved.";
};
}
$(function(){
formUnloadPrompt('form');
});
I recently contributed to an open source jQuery plugin called dirtyForms.
The plugin is designed to work with dynamically added HTML, supports multiple forms, can support virtually any dialog framework, falls back to the browser beforeunload dialog, has a pluggable helper framework to support getting dirty status from custom editors (a tinyMCE plugin is included), works within iFrames, and the dirty status can be set or reset at will.
https://github.com/snikch/jquery.dirtyforms
Detect form changes with using jQuery is very simple:
var formInitVal = $('#formId').serialize(); // detect form init value after form is displayed
// check for form changes
if ($('#formId').serialize() != formInitVal) {
// show confirmation alert
}
I expanded on Slace's suggestion above, to include most editable elements and also excluding certain elements (with a CSS style called "srSearch" here) from causing the dirty flag to be set.
<script type="text/javascript">
var _isDirty = false;
$(document).ready(function () {
// Set exclude CSS class on radio-button list elements
$('table.srSearch input:radio').addClass("srSearch");
$("input[type='text'],input[type='radio'],select,textarea").not(".srSearch").change(function () {
_isDirty = true;
});
});
$(window).bind('beforeunload', function () {
if (_isDirty) {
return 'You have unsaved changes.';
}
});
var unsaved = false;
$(":input").change(function () {
unsaved = true;
});
function unloadPage() {
if (unsaved) {
alert("You have unsaved changes on this page. Do you want to leave this page and discard your changes or stay on this page?");
}
}
window.onbeforeunload = unloadPage;
This is exactly what the Fleegix.js plugin fleegix.form.diff (http://js.fleegix.org/plugins/form/diff) was created for. Serialize the initial state of the form on load using fleegix.form.toObject (http://js.fleegix.org/ref#fleegix.form.toObject) and save it in a variable, then compare with the current state using fleegix.form.diff on unload. Easy as pie.
A lot of outdated answers so here's something a little more modern.
ES6
let dirty = false
document.querySelectorAll('form').forEach(e => e.onchange = () => dirty = true)
One method, using arrays to hold the variables so changes can be tracked.
Here's a very simple method to detect changes, but the rest isn't as elegant.
Another method which is fairly simple and small, from Farfetched Blog:
<body onLoad="lookForChanges()" onBeforeUnload="return warnOfUnsavedChanges()">
<form>
<select name=a multiple>
<option value=1>1
<option value=2>2
<option value=3>3
</select>
<input name=b value=123>
<input type=submit>
</form>
<script>
var changed = 0;
function recordChange() {
changed = 1;
}
function recordChangeIfChangeKey(myevent) {
if (myevent.which && !myevent.ctrlKey && !myevent.ctrlKey)
recordChange(myevent);
}
function ignoreChange() {
changed = 0;
}
function lookForChanges() {
var origfunc;
for (i = 0; i < document.forms.length; i++) {
for (j = 0; j < document.forms[i].elements.length; j++) {
var formField=document.forms[i].elements[j];
var formFieldType=formField.type.toLowerCase();
if (formFieldType == 'checkbox' || formFieldType == 'radio') {
addHandler(formField, 'click', recordChange);
} else if (formFieldType == 'text' || formFieldType == 'textarea') {
if (formField.attachEvent) {
addHandler(formField, 'keypress', recordChange);
} else {
addHandler(formField, 'keypress', recordChangeIfChangeKey);
}
} else if (formFieldType == 'select-multiple' || formFieldType == 'select-one') {
addHandler(formField, 'change', recordChange);
}
}
addHandler(document.forms[i], 'submit', ignoreChange);
}
}
function warnOfUnsavedChanges() {
if (changed) {
if ("event" in window) //ie
event.returnValue = 'You have unsaved changes on this page, which will be discarded if you leave now. Click "Cancel" in order to save them first.';
else //netscape
return false;
}
}
function addHandler(target, eventName, handler) {
if (target.attachEvent) {
target.attachEvent('on'+eventName, handler);
} else {
target.addEventListener(eventName, handler, false);
}
}
</script>
In IE document.ready will not work properly it will update the values of input.
so we need to bind load event inside the document.ready function that will handle for IE browser also.
below is the code you should put inside the document.ready function.
$(document).ready(function () {
$(window).bind("load", function () {
$("input, select").change(function () {});
});
});
I have found that this one works in Chrome with an exception... The messages being returned do not match those in the script:
dataChanged = 0; // global variable flags unsaved changes
function bindForChange() {
$("input,checkbox,textarea,radio,select").bind("change", function (_event) {
dataChanged = 1;
});
$(":reset,:submit").bind("click", function (_event) {
dataChanged = 0;
});
}
function askConfirm() {
if (dataChanged) {
var message =
"You have some unsaved changes. Press OK to continue without saving.";
return message;
}
}
window.onbeforeunload = askConfirm;
window.onload = bindForChange;
The messages returned seem to be triggered by the specific type of action I'm performing. A RELOAD displays a question "Reload Site?
And a windows close returns a "Leave Site?" message.

What is the best way to track changes in a form via javascript?

I'd like to track changes in inputs in a form via javascript. My intent is (but not limited) to
enable "save" button only when something has changed
alert if the user wants to close the page and something is not saved
Ideas?
Loop through all the input elements, and put an onchange handler on each. When that fires, set a flag which lets you know the form has changed. A basic version of that would be very easy to set up, but wouldn't be smart enough to recognize if someone changed an input from "a" to "b" and then back to "a". If it were important to catch that case, then it'd still be possible, but would take a bit more work.
Here's a basic example in jQuery:
$("#myForm")
.on("input", function() {
// do whatever you need to do when something's changed.
// perhaps set up an onExit function on the window
$('#saveButton').show();
})
;
Text form elements in JS expose a .value property and a .defaultValue property, so you can easily implement something like:
function formChanged(form) {
for (var i = 0; i < form.elements.length; i++) {
if(form.elements[i].value != form.elements[i].defaultValue) return(true);
}
return(false);
}
For checkboxes and radio buttons see whether element.checked != element.defaultChecked, and for HTML <select /> elements you'll need to loop over the select.options array and check for each option whether selected == defaultSelected.
You might want to look at using a framework like jQuery to attach handlers to the onchange event of each individual form element. These handlers can call your formChanged() code and modify the enabled property of your "save" button, and/or attach/detach an event handler for the document body's beforeunload event.
Here's a javascript & jquery method for detecting form changes that is simple. It disables the submit button until changes are made. It detects attempts to leave the page by means other than submitting the form. It accounts for "undos" by the user, it is encapsulated within a function for ease of application, and it doesn't misfire on submit. Just call the function and pass the ID of your form.
This function serializes the form once when the page is loaded, and again before the user leaves the page. If the two form states are different, the prompt is shown.
Try it out: http://jsfiddle.net/skibulk/ev5rE/
function formUnloadPrompt(formSelector) {
var formA = $(formSelector).serialize(), formB, formSubmit = false;
// Detect Form Submit
$(formSelector).submit( function(){
formSubmit = true;
});
// Handle Form Unload
window.onbeforeunload = function(){
if (formSubmit) return;
formB = $(formSelector).serialize();
if (formA != formB) return "Your changes have not been saved.";
};
// Enable & Disable Submit Button
var formToggleSubmit = function(){
formB = $(formSelector).serialize();
$(formSelector+' [type="submit"]').attr( "disabled", formA == formB);
};
formToggleSubmit();
$(formSelector).change(formToggleSubmit);
$(formSelector).keyup(formToggleSubmit);
}
// Call function on DOM Ready:
$(function(){
formUnloadPrompt('form');
});
Try
function isModifiedForm(form){
var __clone = $(form).clone();
__clone[0].reset();
return $(form).serialize() == $(__clone).serialize();
}
Hope its helps ))
If your using a web app framework (rails, ASP.NET, Cake, symfony), there should be packages for ajax validation,
http://webtecker.com/2008/03/17/list-of-ajax-form-validators/
and some wrapper on onbeforeunload() to warn users taht are about to close the form:
http://pragmatig.wordpress.com/2008/03/03/protecting-userdata-from-beeing-lost-with-jquery/
Detecting Unsaved Changes
I answered a question like this on Ars Technica, but the question was framed such that the changes needed to be detected even if the user does not blur a text field (in which case the change event never fires). I came up with a comprehensive script which:
enables submit and reset buttons if field values change
disables submit and reset buttons if the form is reset
interrupts leaving the page if form data has changed and not been submitted
supports IE 6+, Firefox 2+, Safari 3+ (and presumably Opera but I did not test)
This script depends on Prototype but could be easily adapted to another library or to stand alone.
$(document).observe('dom:loaded', function(e) {
var browser = {
trident: !!document.all && !window.opera,
webkit: (!(!!document.all && !window.opera) && !document.doctype) ||
(!!window.devicePixelRatio && !!window.getMatchedCSSRules)
};
// Select form elements that won't bubble up delegated events (eg. onchange)
var inputs = $('form_id').select('select, input[type="radio"], input[type="checkbox"]');
$('form_id').observe('submit', function(e) {
// Don't bother submitting if form not modified
if(!$('form_id').hasClassName('modified')) {
e.stop();
return false;
}
$('form_id').addClassName('saving');
});
var change = function(e) {
// Paste event fires before content has been pasted
if(e && e.type && e.type == 'paste') {
arguments.callee.defer();
return false;
}
// Check if event actually results in changed data
if(!e || e.type != 'change') {
var modified = false;
$('form_id').getElements().each(function(element) {
if(element.tagName.match(/^textarea$/i)) {
if($F(element) != element.defaultValue) {
modified = true;
}
return;
} else if(element.tagName.match(/^input$/i)) {
if(element.type.match(/^(text|hidden)$/i) && $F(element) != element.defaultValue) {
modified = true;
} else if(element.type.match(/^(checkbox|radio)$/i) && element.checked != element.defaultChecked) {
modified = true;
}
}
});
if(!modified) {
return false;
}
}
// Mark form as modified
$('form_id').addClassName('modified');
// Enable submit/reset buttons
$('reset_button_id').removeAttribute('disabled');
$('submit_button_id').removeAttribute('disabled');
// Remove event handlers as they're no longer needed
if(browser.trident) {
$('form_id').stopObserving('keyup', change);
$('form_id').stopObserving('paste', change);
} else {
$('form_id').stopObserving('input', change);
}
if(browser.webkit) {
$$('#form_id textarea').invoke('stopObserving', 'keyup', change);
$$('#form_id textarea').invoke('stopObserving', 'paste', change);
}
inputs.invoke('stopObserving', 'change', arguments.callee);
};
$('form_id').observe('reset', function(e) {
// Unset form modified, restart modified check...
$('reset_button_id').writeAttribute('disabled', true);
$('submit_button_id').writeAttribute('disabled', true);
$('form_id').removeClassName('modified');
startObservers();
});
var startObservers = (function(e) {
if(browser.trident) {
$('form_id').observe('keyup', change);
$('form_id').observe('paste', change);
} else {
$('form_id').observe('input', change);
}
// Webkit apparently doesn't fire oninput in textareas
if(browser.webkit) {
$$('#form_id textarea').invoke('observe', 'keyup', change);
$$('#form_id textarea').invoke('observe', 'paste', change);
}
inputs.invoke('observe', 'change', change);
return arguments.callee;
})();
window.onbeforeunload = function(e) {
if($('form_id').hasClassName('modified') && !$('form_id').hasClassName('saving')) {
return 'You have unsaved content, would you really like to leave the page? All your changes will be lost.';
}
};
});
I would store each fields value in a variable when the page loads, then compare those values when the user unloads the page. If any differences are detected you will know what to save and better yet, be able to specifically tell the user what data will not be saved if they exit.
// this example uses the prototype library
// also, it's not very efficient, I just threw it together
var valuesAtLoad = [];
var valuesOnCheck = [];
var isDirty = false;
var names = [];
Event.observe(window, 'load', function() {
$$('.field').each(function(i) {
valuesAtLoad.push($F(i));
});
});
var checkValues = function() {
var changes = [];
valuesOnCheck = [];
$$('.field').each(function(i) {
valuesOnCheck.push($F(i));
});
for(var i = 0; i <= valuesOnCheck.length - 1; i++ ) {
var source = valuesOnCheck[i];
var compare = valuesAtLoad[i];
if( source !== compare ) {
changes.push($$('.field')[i]);
}
}
return changes.length > 0 ? changes : [];
};
setInterval(function() { names = checkValues().pluck('id'); isDirty = names.length > 0; }, 100);
// notify the user when they exit
Event.observe(window, 'beforeunload', function(e) {
e.returnValue = isDirty ? "you have changed the following fields: \r\n" + names + "\r\n these changes will be lost if you exit. Are you sure you want to continue?" : true;
});
I've used dirtyforms.js. Works well for me.
http://mal.co.nz/code/jquery-dirty-forms/
To alert the user before closing, use unbeforeunload:
window.onbeforeunload = function() {
return "You are about to lose your form data.";
};
I did some Cross Browser Testing.
On Chrome and Safari this is nice:
<form onchange="validate()">
...
</form>
For Firefox + Chrome/Safari I go with this:
<form onkeydown="validate()">
...
<input type="checkbox" onchange="validate()">
</form>
Items like checkboxes or radiobuttons need an own onchange event listener.
Attach an event handler to each form input/select/textarea's onchange event. Setting a variable to tell you if you should enable the "save" button. Create an onunload hander that checks for a dirty form too, and when the form is submitted reset the variable:
window.onunload = checkUnsavedPage;
var isDirty = false;
var formElements = //Get a reference to all form elements
for(var i = 0; len = formElements.length; i++) {
//Add onchange event to each element to call formChanged()
}
function formChanged(event) {
isDirty = false;
document.getElementById("savebtn").disabled = "";
}
function checkUnsavedPage() {
if (isDirty) {
var isSure = confirm("you sure?");
if (!isSure) {
event.preventDefault();
}
}
}
Here's a full implementation of Dylan Beattie's suggestion:
Client/JS Framework for "Unsaved Data" Protection?
You shouldn't need to store initial values to determine if the form has changed, unless you're populating it dynamically on the client side (although, even then, you could still set up the default properties on the form elements).
You can also check out this jQuery plugin I built at jQuery track changes in forms plugin
See the demo here and download the JS here
If you are open to using jQuery, see my answer a similar question:
Disable submit button unless original form data has changed.
I had the same challenge and i was thinking of a common solution. The code below is not perfect, its from initial r&d. Following are the steps I used:
1) Move the following JS to a another file (say changeFramework.js)
2) Include it in your project by importing it
3) In your html page, whichever control needs monitoring, add the class "monitorChange"
4) The global variable 'hasChanged' will tell, if there is any change in the page you working on.
<script type="text/javascript" id="MonitorChangeFramework">
// MONITOR CHANGE FRAMEWORK
// ALL ELEMENTS WITH CLASS ".monitorChange" WILL BE REGISTERED FOR CHANGE
// ON CHANGE IT WILL RAISE A FLAG
var hasChanged;
function MonitorChange() {
hasChanged = false;
$(".monitorChange").change(function () {
hasChanged = true;
});
}
Following are the controls where I used this framework:
<textarea class="monitorChange" rows="5" cols="10" id="testArea"></textarea></br>
<div id="divDrinks">
<input type="checkbox" class="chb monitorChange" value="Tea" />Tea </br>
<input type="checkbox" class="chb monitorChange" value="Milk" checked='checked' />Milk</br>
<input type="checkbox" class="chb monitorChange" value="Coffee" />Coffee </br>
</div>
<select id="comboCar" class="monitorChange">
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</select>
<button id="testButton">
test</button><a onclick="NavigateTo()">next >>> </a>
I believe there can be huge improvement in this framework. Comment/Changes/feedbacks are welcome. :)

Categories

Resources