I recently had need to serialise a form so that I could easily store its data in either a cookie or using web storage, without the need to use any server-side code or databases.
While I could’ve used one of the many JavaScript frameworks available to me, I didn’t really want to add them to my project purely for the sake of one piece of functionality – and for this project, code size is an issue: it may well end up embedded in some firmware on a NAS box.
I did some digging around and found many stand-alone candidates, the smallest of which was form-serialize by Dimitar Ivanov at 1207 bytes
While the code worked well for bog-standard form elements, there were some issues that put me off using it:
- It only worked with form elements that had been explicitly specified in the code: none of the new HTML5 elements such as the colour picker, date picker, etc. were serialised
- It serialised disabled form elements – something that should not happen
I decided to roll my own, and am pretty happy with the results – especially as I know that it will cope with the newer HTML5 input types without an issue. I tested it on a very large form with many elements, and comparing the resulting string with those produced by jQuery 1.7.2 and Prototype 1.7.0.0 gave me hope that it should cater for most circumstances. There were only 3 differences between my output and the output from jQuery and Prototype:
- On my Mac, jQuery converted LF characters in a textarea to a CR+LF pair. Prototype did not do this, nor does my code
- jQuery converted spaces into ‘+’ characters, whereas my code and Prototype use ‘%20′
- Prototype will happily include disabled option elements in its serialised form data (see my test harness here. Neither my code nor jQuery do this.
- If the form had multiple elements with the same name, but of different types
- If the form had multiple select elements with the same name
So, all in all, I’m pretty happy with the results!
I actually wrote 2 versions of the code, and I’ll post both here (they both return identical output, but the initial version will probably be easier to follow if you’re fairly new to JavaScript).
Here’s the first version. It just didn’t feel right, having 3 branches for various element types, which is why I re-wrote it!
// From http://www.codecouch.com/2012/06/form-serialisation-in-less-than-700-bytes function serialise(formEl) { var els, loop, el, subEls, subLoop, subEl, vals = [], cbRadio; els = Array.prototype.slice.call(formEl.elements).reverse(); function add(name, val) { vals.push(encodeURIComponent(name) + '=' + encodeURIComponent(val)); }; for (loop=els.length; loop--;) { el = els[loop]; cbRadio = /checkbox|radio/.test(el.type); // Skip elements that have no 'name' attribute, are disabled, or are a button, image or file input if (el.name == '' || el.disabled || el.nodeName == 'BUTTON' || (el.nodeName == 'INPUT' && /^(button|submit|reset|image|file)$/.test(el.type))) continue; if (el.nodeName == 'SELECT') { // If we're dealing with a select element (whether single or multi), ignore selected options if they are also disabled subEls = el.options; for (subLoop=subEls.length; subLoop--;) { subEl = subEls[subLoop]; if (!subEl.disabled && subEl.selected) add(el.name, subEl.value); } } else if (el.length) { // If we're dealing with values from multiple inputs with the same name, ignore any values associated with a disabled element // Also ignore values from radio buttons & checkboxes that are not checked for (subLoop=el.length; subLoop--;) { subEl = el[subLoop]; if (!subEl.disabled && (!cbRadio || (cbRadio && subEl.checked))) add(el.name, subEl.value); } } else { // We're dealing with a non-disabled single input or textarea. // If the element is not a checkbox or radio button, add the value. If it is, add if checked if (!cbRadio || (cbRadio && el.checked)) add(el.name, el.value); } } return vals.join('&'); }
And here’s the rewrite… hopefully the comments at the top are understandable
/* Some form elements should only be serialised if a specific property is set on them, e.g. 'selected', 'checked'. Some form elements store their own collections of values that need to be iterated through, e.g. 'options' for a select element. If there are multiple form elements with the same name, form.elements['theName'] will have a length property and will need to be iterated over. I wanted to keep the code size small, so rather than have several branches of code to deal with all of these circumstances, I thought it was nicer to store the name of property that determines whether serialisation should happen or not, and treat any uniquely-named form elements as if they were in a collection. This way, only 1 loop is needed. I store the property that determines whether a value should be serialised in "includeIfHas", and I store all element with the same name in "subEls". If the element is uniquely-named, it gets wrapped in an array (which provides the "length" property). */ // From http://www.codecouch.com/2012/06/form-serialisation-in-less-than-700-bytes function serialise(formEl) { var els, loop, el, includeIfHas, subEls, subLoop, subEl, vals = []; els = Array.prototype.slice.call(formEl.elements).reverse(); for (loop=els.length; loop--;) { el = els[loop]; // Skip elements that have no 'name' attribute, that are disabled, or that are a button, image or file input if (el.name == '' || el.disabled || el.nodeName == 'BUTTON' || (el.nodeName == 'INPUT' && /^(button|submit|reset|image|file)$/.test(el.type))) continue; includeIfHas = (el.nodeName == 'SELECT') ? 'selected' : (/^(checkbox|radio)$/.test(el.type)) ? 'checked' : 'name'; subEls = (el.nodeName == 'SELECT') ? el.options : (el.length) ? el : [el]; for (subLoop=subEls.length; subLoop--;) { subEl = subEls[subLoop]; if (!subEl.disabled && subEl[includeIfHas]) vals.push(encodeURIComponent(el.name) + '=' + encodeURIComponent(subEl.value)); } } return vals.join('&'); }
Ignoring the comments and removing all unnecessary whitespace, the code weighs in at 679 bytes… which sure beats adding jQuery or Prototype to the project!
// From http://www.codecouch.com/2012/06/form-serialisation-in-less-than-700-bytes function serialise(formEl){var els=Array.prototype.slice.call(formEl.elements).reverse(),loop,el,includeIfHas,subEls,subLoop,subEl,vals=[];for(loop=els.length;loop--;){el=els[loop];if(el.name==''||el.disabled||el.nodeName=='BUTTON'||(el.nodeName=='INPUT'&&/^(button|submit|reset|image|file)$/.test(el.type)))continue;includeIfHas=(el.nodeName=='SELECT')?'selected':(/^(checkbox|radio)$/.test(el.type))?'checked':'name';subEls=(el.nodeName=='SELECT')?el.options:(el.length)?el:[el];for(subLoop=subEls.length;subLoop--;){subEl=subEls[subLoop];if(!subEl.disabled&&subEl[includeIfHas])vals.push(encodeURIComponent(el.name)+'='+encodeURIComponent(subEl.value))}}return vals.join('&')}
I can’t say that I’ve tested this on every possible combination of form elements, so if you find something that doesn’t work for you, post a comment and let me know!
Note: Just in case you are thinking of using my code, there are a couple of ‘gotchas’ that you should probably know about – circumstances under which it may fail to serialise correctly:
If you need to serialise forms with such an eclectic mix of controls, this code is not for you