Extracting values from HTML form elements in webpages has historically been and still is a messy business when done client-side.
Usually the process entails a rather cumbersome hand-written javascript function that acesses each DOM form element and extracts its value into a variable for later use.
Many of the modern MVVM frameworks solve this by using view-model classes that are data-bound to the elements. However for the vast majority of us that don't have this luxury then we're usually stuck with a brittle javascript logic that has intimate knowledge of the HTML DOM structure.
This unfortunately makes our code hard to maintain, difficult to test and close to impossible to reuse.
How about JSON?
For a recent project I was yet again in the place of having to extract a large amount of data out of form elements for client side manipulation.
For this project (which uses handlebars-js) I needed to convert the user input into a JSON data structure. I came up with a generic way to very easily extract all user entered data automatically by relying on custom DOM entity attributes.
Perhaps an interactive demo explains this best :)
How it works
The only thing you need to do for your HTML elements is to decorate them with an additional attribute. By default this attribute is named serialize-as
but you can use any other name you want. This attribute value will control where in the resulting JSON the field value will be written.
<input type="text"
serialize-as="site_name"
value="My Awesome Site" />
results in
{
site_name: "My Awesome Site"
}
Nesting can be achived by using dots .
in the serialization attribute (there is no limit on the depth of nesting that can be done).
<input type="text"
serialize-as="site.name"
value="My Awesome Site" />
results in
{
site: {
name: "My Awesome Site"
}
}
Go ahead, try this out in the
Splitting values
The code supports automatic splitting of entered value into JSON arrays based on a delimeter. An additional attribute can be applied, serialize-separator
that contains the delimiter to split on. This attribute name can also be customized.
<input type="text" id="keywords"
serialize-as="seo.keywords"
serialize-separator=","
value="software, services, seo">
results in
{
"keywords": [
"software",
" services",
" seo"
],
}
Producing alternative JSON structure
A useful feature when working with templates and fragments of HTML is to be able provide the data in multiple different formats. The jsonify function supports that by allowing you to specify as many alternative serialization attributes as you want for a form element.
Consider the HTML below
<input type="text"
serialize-as="website_name"
serialize-hb="site.name"
serialize-angular="post.info.meta.name"
value="My Awesome Site" />
The data in this input element can be serialized in three different ways depending on what serialization attribute we instruct the function to use.
Calling
jsonify()
results in the serialize-as
attribute to be used (the default) and the following json to be created
{
website_name: "My Awesome Site"
}
However
jsonify(null, 'serialize-hb')
results in serialize-hb
to be used which produces
{
site: {
name: "My Awesome Site"
}
}
and finally
jsonify(null, 'serialize-angular')
results in serialize-angular
to be used which produces
{
post: {
info:{
meta:{
name: "My Awesome Site"
}
}
}
}
This same trick of using different attribute names, also works for the
serialize-separator
attribute.
Optional options
The final argument to the function is an option configuration. Here the code can be instructed on what to do if they encounter an empty field. By default a null
value will be assigned but this can be overwritten by setting usenull: false
.
Also the code will attempt to parse numerical values correctly in the JSON, this can be turned off by setting parsenumbers:false
.
The code
For those of you who just want the final js
code. Then you can find the complete code below.
This version requires
jquery
function jsonify(parentEl,
attributeName,
separatorName,
options = {'usenull':true, 'parsenumbers':true}) {
parentEl = typeof(parentEl) === typeof undefined || parentEl === null
? document.body : parentEl;
attributeName = typeof(attributeName) === typeof undefined || attributeName === null
? "serialize-as" : attributeName;
separatorName = typeof(separatorName) === typeof undefined || separatorName === null
? "serialize-separator" : separatorName;
var json = {};
$(parentEl).find('[' + attributeName + ']').each(function() {
var val = null;
if ($(this).is(':checkbox') || $(this).is(':radio')) {
var tmpValueAttr = $(this).attr('value');
if (typeof tmpValueAttr !== typeof undefined && tmpValueAttr !== false) {
if (!$(this).is(':checked'))
return;
val = $(this).val();
} else {
val = $(this).is(':checked');
}
} else {
val = $(this).val();
var tmpSepAttrVal = $(this).attr(separatorName);
if( tmpSepAttrVal && val )
val = val.split(tmpSepAttrVal);
}
if( options && options['usenull']
&& (typeof val === typeof undefined || val === "" ) ){
val = null;
}
if( options && options['parsenumbers'] && val
&& !Array.isArray(val) && typeof(val) !== "boolean"
&& isFinite(val) && !isNaN(parseFloat(val)) ) {
if( val.length > 0 && val[0] != "0" )
val = !isNaN(parseFloat(val)) ? parseFloat(val) : parseInt(val);
}
var path = $(this).attr(attributeName).split('.');
var path_len = path.length; data = null;
$.each(path, function(index, key) {
if (data == null)
data = json;
if (index + 1 === path_len) {
data[key] = val;
} else {
if (!(key in data))
data[key] = {};
data = data[key]
}
});
});
return json;
}
I found this approach to be extremely helpful and I hope this will be of use to some of you.
Developer & Programmer with +18 years professional experience building software.