17

I'm looking for a way to submit only changed form fields to the server. So, let's say I have a form

<form>
    <input type="text" name="a"/>
    <select name="b">...</select>
    <input type="checkbox" name="c"/>
</form>

which is populated with certain data already. The user edits the form and clicks submit. If the user only changed input b, then I want to submit only input b. If only a and c were changed, I want to submit only a and c. And so on.

I could write something myself to accomplish this, but I am wondering maybe there is already something out there that I could use? Ideally, I would like the code to be short. Something like this would be perfect:

$('form').serialize('select-only-changed');

Also, I came across this http://code.google.com/p/jquery-form-observe/ , but I see there are issues with it. Is this plugin working solidly?

halfer
  • 19,824
  • 17
  • 99
  • 186
Dimskiy
  • 5,233
  • 13
  • 47
  • 66

8 Answers8

32

Another approach would be to serialize the form when the page loads, and then on submit, only submit the changes.

$(function() {

  var $form = $('form');

  var startItems = convertSerializedArrayToHash($form.serializeArray()); 

  $('form').submit() {
    var currentItems = convertSerializedArrayToHash($form.serializeArray());
    var itemsToSubmit = hashDiff( startItems, currentItems);

    $.post($form.attr('action'), itemsToSubmit, etc.
  }
});

Then, all you have to write is the hashDiff function, which is straightforward and generally useful.

This is nice because it can easily be packaged into a plugin, and it can work repeatedly on the same form if you're using Ajax.

function hashDiff(h1, h2) {
  var d = {};
  for (k in h2) {
    if (h1[k] !== h2[k]) d[k] = h2[k];
  }
  return d;
}

function convertSerializedArrayToHash(a) { 
  var r = {}; 
  for (var i = 0;i<a.length;i++) { 
    r[a[i].name] = a[i].value;
  }
  return r;
}

Here's a minimal test:

  describe('hashDiff()', function() {
    it('should return {} for empty hash',function() {
      expect(hashDiff({},{})).toEqual({});
    });
    it('should return {} for equivalent hashes',function() {
      expect(hashDiff({a:1,b:2,c:3},{a:1,b:2,c:3})).toEqual({});
    });
    it('should return {} for empty hash',function() {
      expect(hashDiff({a:1,b:2,c:3},{a:1,b:3,c:3})).toEqual({b:3});
    });
  });
ndp
  • 21,546
  • 5
  • 36
  • 52
  • I kind of like this solution. What would it take to write the hashDiff function? Is there a way to look up a value in serialized name value pairs? Something like var changedForm = $form.serialize(); changedForm.Find("name from formContents"); – Dimskiy Mar 07 '11 at 16:31
  • added hashDiff above. There are more edge cases, but this should handle forms fine. – ndp Mar 07 '11 at 23:55
  • I'm playing around with your solution and for me h1 and h2 in hashDiff are being treated as strings. So then the for loop goes through each character for the string. – Dimskiy Mar 08 '11 at 20:58
  • You're right. Serialize returns a single string. SerializeArray returns something closer-- but I was wrong about the format of the values. I added a conversion routine above. Guess I should just write a plugin at this point. – ndp Mar 09 '11 at 08:07
  • Thank you! I was actually thinking of writing a plugin myself as well :) – Dimskiy Mar 09 '11 at 15:39
  • I've discovered a problem. If a checkbox is checked and I uncheck it, it doesn't get sent to the server. Any idea on how to fix that? – Jørgen R Jun 27 '12 at 14:05
  • And I've found a solution to said problem. Adding `if(jQuery.inArray(h1[k], h2)) d[k] = "";` to the for loop in `hashDiff()` adds an empty value for items in `h1` that is missing in `h2`. – Jørgen R Jun 27 '12 at 14:23
  • ALSO if you are adding content dynamically on the fly, you will want to add all newly added fields to the submission as well. Simply alter the `hashdiff()` for loop like so: `for (k in h2) { if ($.inArray(h2[k], h1) == -1) { d[k] = h2[k]; } else if (h1[k] !== h2[k]) { d[k] = h2[k]; } }` – Tomanow Nov 06 '13 at 15:19
  • Hi @ndp could you show some demo how to implement this solution? – mrrsb Apr 16 '15 at 04:10
  • Thanks for the post you gave me an idea for an issue I've had for about a year now. I have a Project JS App with a mass-edit Task page to edit all the fields on a Task record as well as change there sort order and which Task list they belong to. When the user saves the data it is posted as 1 big post to DB with a complex SQL query which only updates a modified date field on the records that submitted and had data besides there sort column changed. When items sort is changed it updates tons of records even though there task data didn't change.The SQL query inserts if record don't exist..... – JasonDavis Oct 30 '15 at 06:43
  • ...continued...otherwise it updates record and only updates the date column when a DB column besides the sort_order is changed. I also have a DB table for Activity Stream which logs every event/action showing which user did which action on which project and which projects Task record and lots more. The way the mass-edit SQL query works, there is no way to really know which records had which fields changed since it will save every DB column open every record even if most of them did not change. Using your JS theory above I could build lists of which data columns changed on each record!...... – JasonDavis Oct 30 '15 at 06:44
  • ...continued.... I plan to re-do the system anyways to simply save each record instantly anytime any filed is changed but for the time being this is a neat idea to solve my issue for now! – JasonDavis Oct 30 '15 at 06:44
  • `$.post($form.attr('action'), itemsToSubmit, etc.` May I ask how you complete this piece? My form has an `action=''` where it just submits to the same page. I would like it do perform a submit like before with these new changed inputs. Thanks! – theflarenet May 08 '18 at 15:16
  • @theflarenet I believe you'd want something like [$.redirect](https://github.com/mgalante/jquery.redirect/blob/master/jquery.redirect.js) instead of $.post. $.redirect will link to a new page and send the post parameters to it. However, I'd also suggest you read about [post/get/redirect](https://en.wikipedia.org/wiki/Post/Redirect/Get) as it suggests separating the form processor from the result of the processing such that a [normal redirect](https://stackoverflow.com/questions/503093/how-do-i-redirect-to-another-webpage) would work. – mikeLundquist Jun 12 '18 at 19:32
  • @user4757074 Awesome plugin! Thank you for this noteworthy suggestion. I'm still open to alternative (native JS/JQuery) methods in order to achieve this. – theflarenet Jun 13 '18 at 16:19
16

Another option would be to mark the fields as disabled before they are submitted. By default disabled fields will not be serialized or submitted with a default form post.

Simple example:

function MarkAsChanged(){
    $(this).addClass("changed");
}
$(":input").blur(MarkAsChanged).change(MarkAsChanged);

$("input[type=button]").click(function(){
    $(":input:not(.changed)").attr("disabled", "disabled");
    $("h1").text($("#test").serialize());
});

on jsfiddle.

Mark Coleman
  • 40,542
  • 9
  • 81
  • 101
  • So then I would have to "flag" changed fields, and then right before the submit disable the non-flagged fields. – Dimskiy Mar 07 '11 at 16:27
  • @Dimskiy that is correct, either using `change` `blur` or some other method keep track if the field changed and then on the `submit` simply disable the fields and they wont be sent with the post. – Mark Coleman Mar 07 '11 at 16:36
  • 1
    So simple and compatible... Like that one the most! Even if the field's 'dirtyness' is not calculated. – Armel Larcier Sep 08 '14 at 13:07
5

You could add an 'oldvalue' parameter to the input field. Populate this value at the time the page is generated either with JavaScript or on the server-side.

<input name="field1" value="10" oldvalue="10">

Then use the following function to serialize:

function serializeForm() {
    data = "";
    $("input,textarea").each(function (index, obj) {
        if ($(obj).val() != $(obj).attr("oldvalue")) {
            data += "&" + $(obj).serialize();
        }
    });
    return data.substr(1);
}

After the data has been sent to the server, your script could update the 'oldvalue' parameters to prevent the data from being sent again unless a further change is made.

Michael
  • 51
  • 1
  • 1
1

I may be missing something but I tried this and the hashDiff function returned an "undefined" error for the first form element it tried to process.

I implemented something a bit simpler which seems to work fine.

$('#submitChangesOnlyButton').click(function () {
     var formAfterEdit = $('#myForm').serializeArray()
     var itemsToSubmit = checkDiff(formBeforeEdit,formAfterEdit);
     })

...

function checkDiff(before, after) {
    var whatsChanged = [];

    for (i = 0; i < before.length; i++) {
        if (after[i].value !== before[i].value) {
            whatsChanged.push(after[i]);
        }
    }
    return whatsChanged;
}
Tommy
  • 176
  • 1
  • 2
  • 17
  • nice solution but it's not bulletproof. In case of dynamic field changes (add / remove fields). It needs to compare by name, and it should take into account fields which do not exists in the other (added or removed) – qdev Aug 17 '19 at 08:06
1

The simplest solution would be to add something like:

$(function() {

    $("input, select").change(function() {
        $(this).addClass("changed");
    });

});

Then just select on the .changed class to get the elements that have been changed.

More information on the jQuery change event: http://api.jquery.com/change/

As @Martin points out below, the change event is only triggered for text inputs after they click off the input. If this is just to save some bandwidth, I would recommend binding on the click event instead. You may get sent some fields that haven't actually changed, but probably better to air on the side of getting too much back than too little.

Riley Dutton
  • 7,615
  • 2
  • 24
  • 26
  • I don't think the change event is triggered before you blur the input field, so if you make a change in a fiend and hit return to submit, this might just fail. – Martin Jespersen Mar 07 '11 at 15:47
  • only use param() instead serialize(). Serialize works only on forms. – Ivan Ivanic Mar 07 '11 at 15:49
  • I might have to implement something along these lines myself, but I'd like to see if there is something out there that already does the job. It is for saving the bandwidth and some processing time on the back-end, and I don't mind if a few extra values end up in the post. – Dimskiy Mar 07 '11 at 16:10
1

You could try adding a class to each field which has been changed and remove the others prior to calling $('form').serialize().

$(function() {
    $(':input').change(function() {
        $(this).addClass('changed');
    });
    $('form').submit(function () {
        $('form').find(':input:not(.changed)').remove();
        return true;
    });
});

Though this solution is destructive and only works if you're not using AJAX (a solution exists even for AJAX but it gets even more complicated).

Neil
  • 5,762
  • 24
  • 36
0

just compare betwen current value and default value like this:

var toBeSubmited = new FormData();
for(var i in formObj)
  if('value' in formObj[i] && formObj[i].value!=formObj[i].defaultValue){ //is an input or select or textarea
     toBeSubmited.add(i, formObj[i].value);
  }
//now send "toBeSubmited" form object
$.ajax(...)
  • method should be append() instead of add() – qdev Aug 01 '19 at 19:10
  • defaultValue works only with text inputs (text, password, tel, email, etc) and textareas. For checkboxes, radios and selects this is not the case and should check for defaultSelected on each select option or defaultChecked for each radio or checkbox ;) – qdev Aug 17 '19 at 06:03
0

My solution

$('form').each(function(){
    var $form = $(this);
    if (!$form.hasClass('send-all')){
        var watchedFields = 'input:not([type="hidden"]):not([type="checkbox"]), select, textarea';

        $form.find(watchedFields).addClass('watched').on('change', function() {
            $(this).addClass('changed');
        });

        $form.submit(function(e){
            var data = $form.serializeArray();

            // Loop fields
            for (i = 0; i < data.length; i++){
                $field = $form.find('[name="' + data[i].name + '"]');
                // Prevent send unchanged fields
                if ($field.hasClass('watched') && !$field.hasClass('changed')) $field.prop('disabled', 'disabled');
                // Send changed but set to readonly before
                else $field.prop('readonly', 'readonly');
            }

            return true;
        });
    }
});