Detecting Unsaved Changes

Warn user before leaving web page with unsaved changes

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

Detecting Unsaved 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.

Detect if a form has unsaved changes before letting user focus to a different form

You could do this by setting a class on the form when input values change then listening for clicks on the document and checking if a form other than the one being interacted with has changes. If one does, present a message to the user.

This should work:

Note that you dont have to use forms, just add the track-changes class to some parent of the inputs you have grouped together

jsFiddle example

$(document).mouseup(function(e) { 
var $target = $(e.target);
// get any forms with changes that are not the current form or do not contain the clicked element
var $otherFormsWithChanges = $('.pending').filter(function() {
var $this=$(this);
return $this.hasClass('pending') && (!$this.is($target) || $this.has($target).length !=0);
});
// if another form has a change, thow up a message
// allow the user to go back to the form or ignore the changes
if ($otherFormsWithChanges.length > 0 && $otherFormsWithChanges.has($target).length===0 ) {
var c = confirm("You have unsaved changes.\n\n Click cancel to go back to the unsaved form or OK to ignore");
c ? $otherFormsWithChanges.removeClass('pending') : $otherFormsWithChanges.find('input, select, textarea').focus();
}
});

$('.track-changes').on('keyup change', 'input, select, textarea', function() {
$(this).closest('.track-changes').addClass('pending');
});

$('.save').click(function() {
// save data...
$(this).closest('.track-changes').removeClass('pending');
});
form{
padding:10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h4>form 1</h4>
<form class="track-changes" has-changes="false" action="">
<input type="text">
some text
<br>
<button type="button" class="save">Save</button>
</form>

<h4>form 2</h4>
<form class="track-changes" has-changes="false" action="">
<input type="text">
some text
<br>
<button type="button" class="save">Save</button>
</form>

<h4>form 3</h4>
<form class="track-changes" has-changes="false" action="">
<input type="text">
some text
<br>
<button type="button" class="save">Save</button>
</form>

Alert for unsaved changes in form

This is what i am using, Put all this code in a separate JS file and load it in your header file so you will not need to copy this again and again:

var unsaved = false;

$(":input").change(function(){ //triggers change in all input fields including text type
unsaved = true;
});

function unloadPage(){
if(unsaved){
return "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;

EDIT for $ not found:

This error can only be caused by one of three things:

  1. Your JavaScript file is not being properly loaded into your page
  2. You have a botched version of jQuery. This could happen because someone edited the core file, or a plugin may have overwritten the $
    variable.
  3. You have JavaScript running before the page is fully loaded, and as such, before jQuery is fully loaded.

Make sure all JS code is being placed in this:

$(document).ready(function () {
//place above code here
});

Edit for a Save/Send/Submit Button Exception

$('#save').click(function() {
unsaved = false;
});

Edit to work with dynamic inputs

// Another way to bind the event
$(window).bind('beforeunload', function() {
if(unsaved){
return "You have unsaved changes on this page. Do you want to leave this page and discard your changes or stay on this page?";
}
});

// Monitor dynamic inputs
$(document).on('change', ':input', function(){ //triggers change in all input fields including text type
unsaved = true;
});

Add the above code in your alert_unsaved_changes.js file.

How to track unsaved changes in reactive forms ? How to prevent user from clicking buttons on the same page before saving?

Since you've said you're using the reactive form approach you can simply use dirty:

<div class="important" *ngIf="createPrpjectDetailsForm.dirty == true">Form changes need to be saved first before submitting order.</div>
<button type="button" *ngIf="createPrpjectDetailsForm.dirty == false">Submit</button>

And as L.Loscos has mentioned, mark the form as pristine on save. this.createPrpjectDetailsForm.markAsPristine()

Implementation of Unsaved Changes Detection

As long as you need an undo/redo system, you need that stack of past operations. To detect in wich state the document is, an item of the stack is set to be the 'saved state'. Current stack node is not that item, the document is changed.

You can see an example of this in Qt QUndoStack( http://doc.qt.nokia.com/stable/qundostack.html ) and its isClean() and setClean()

For proposition 1, updating a boolean is not something problematic and take little time.



Related Topics



Leave a reply



Submit