views:

3318

answers:

10

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?

+9  A: 

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 recognise 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 :input")
    .once("change", function() {
        // do whatever you need to do when something's changed.
        // perhaps set up an onExit function on the window
        $('#saveButton').show();

        // now remove the event handler from all the elements
        // since you don't need it any more.
        $("#myForm :input").unbind("change");
    })
;
nickf
You could use a closure in your loop to store the value of the input field, and then you can test to see if the input changed back to the value. You'll have to do some special cases for radio buttons, checkboxes and the submitbutton though. OH, and don't forget to remove the unbink in this case!
Jonathan Arkell
AFAIK, the change event is only triggered for e.g. a text input if the contents is changed and _the input loses focus_. This would mean the save button is not enabled until the user e.g. hits tab or so, wouldn't it ?
ssc
@ssc, yep that's correct. Most forms would probably have more than one field though.
nickf
+2  A: 

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.

Dylan Beattie
+1  A: 

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/ http://stackoverflow.com/questions/155739/detecting-unsaved-changes-using-javascript

Gene T
A: 

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();
        }
    }
}
Eric Wendelin
A: 

To alert the user before closing, use unbeforeunload:

window.onbeforeunload = function() {
return confirm("You are about to lose your form data.")
}
Diodeus
Just return the string. The browser will confirm() it.
eyelidlessness
A: 

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:

  1. enables submit and reset buttons if field values change
  2. disables submit and reset buttons if the form is reset
  3. interrupts leaving the page if form data has changed and not been submitted
  4. 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.';
        }
    };

});
eyelidlessness
A: 

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;
});
Jared
A: 

Here's a full implementation of Dylan Beattie's suggestion:

http://stackoverflow.com/questions/140460/clientjs-framework-for-unsaved-data-protection#140508

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).

insin
A: 

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

A: 

@nickf AFAIK once is not a jquery function, I think the function is one. That should be substituted out.

paul