Hi Friends
A DOM Manipulation Class in 100 Lines of JavaScript By Krasimir Tsonev
If you build web applications you probably deal with the DOM a lot. Accessing and manipulating DOM elements is a common requirement of nearly every web application. Very often we collect information from different controls, we need to set values, change the content of div or span tags. Of course there are a lot of libraries that help handle these actions, with the most popular being jQuery, of course, which is the de factor standard. However, sometimes you don’t need everything that jQuery provides, so in this article we will take a look at how to build your own class for managing DOM elements.
The API
As developers we make decisions every day. I believe in the test-driven development and one of the things which I really like is the fact that it forces you to make design decisions before you start the actual coding. Along those lines, here is what I want the DOM management class’s API to look like in the end:
// returns DOM element dom(‘.selector’).el // returns the value/content of the element dom(‘.selector’).val() // sets the value/content of the element dom(‘.selector’).val(‘value’)
This should cover most of the possible use cases. However it would be even better if we could manipulate several objects at once. And it would be great if we could generate a JavaScript object.
// generates an object containing DOM elements dom({ structure: { propA: ‘.selector’, propB: ‘.selector’ }, propC: ‘.selector’ })
Once we have our elements stored we could easily execute the val method for all of them.
// retrieving the values of several DOM elements dom({ structure: { propA: ‘.selector’, propB: ‘.selector’ }, propC: ‘.selector’ }).val()
This will be aneffective method for translating data from the DOM directly into a JavaScript object.
Now that we have an idea of what our API should look like, our class starts with the following code:
var dom = function(el) { var api = { el: null } api.val = function(value) { // … } return api; }
Scoping
It is clear that we are going to use methods like getElementById, querySelector or querySelectorAll. Typically, you might access the DOM like this:
var header = document.querySelector(‘.header’);
What is really interesting here is that querySelector, for example, is not just a method of the document object, but also of any other DOM element. This means that we are able to run the query in specific context. For example:
<header> <p>Big</p> </header> <footer> <p>Small</p> </footer> var header = document.querySelector(‘header’); var footer = document.querySelector(‘footer’); console.log(header.querySelector(‘p’).textContent); // Big console.log(footer.querySelector(‘p’).textContent); // Small
We are able to operate within specific part of the DOM tree and our class should support the passing of a scope. So, together with a selector it would be good if it accepts a parent element.
var dom = function(el, parent) { var api = { el: null } api.val = function(value) { // … } return api; }
Reaching the DOM element
As we said above, we are going to use querySelector and querySelectorAll to reach the DOM elements. Let’s create two shortcuts for these functions.
var qs = function(selector, parent) { parent = parent || document; return parent.querySelector(selector); }; var qsa = function(selector, parent) { parent = parent || document; return parent.querySelectorAll(selector); };
After that we should use the passed el argument. Normally this will be a string (selector) but we should also support:
- A DOM element – the val method of the class will be pretty handy so we may need to use the class with already referenced element;
- A JavaScript object – in order to create JavaScript object containing multiple DOM elements.
The following switch will cover both cases:
switch(typeof el) { case ‘string’: parent = parent && typeof parent === ‘string’ ? qs(parent) : parent; api.el = qs(el, parent); break; case ‘object’: if(typeof el.nodeName != ‘undefined’) { api.el = el; } else { var loop = function(value, obj) { obj = obj || this; for(var prop in obj) { if(typeof obj[prop].el != ‘undefined’) { obj[prop] = obj[prop].val(value); } else if(typeof obj[prop] == ‘object’) { obj[prop] = loop(value, obj[prop]); } } delete obj.val; return obj; } var res = { val: loop }; for(var key in el) { res[key] = dom.apply(this, [el[key], parent]); } return res; } break; }
The first case is executed if the developer passes a string. We prepare the parent and call the querySelector shortcut. The second part of the statement is for the cases where we have a DOM element sent or a JavaScript object. We are checking if the object has nodeName property, and, if it does, then we directly apply it as a value of the api.el property. If it doesn’t, then we go through all the parts of the object and initialize a class instance for every property. Here are some test cases involving the following markup:
<p>text</p> <header> <p>Big</p> </header> <footer> <p>Small</p> </footer>
Accessing the first paragraph:
dom(‘p’).el
Accessing the paragraph in the header node:
dom(‘p’, ‘header’).el
Passing a DOM element:
dom(document.querySelector(‘header’)).el
Passing a JavaScript object:
var els = dom({ footer: ‘footer’, paragraphs: { header: ‘header p’, footer: ‘footer p’ } })) // At the end we have again JavaScript object. // It’s properties are actually results // of dom function execution. For example, to get // the paragraph in the footer: els.paragraphs.footer.el
Getting or setting the value of an element
The value of the form elements like input or select could be retrieved easily – we can use the value property of the element. We already have an access to the DOM element – it is stored in api.el. However, it is a little bit tricky when we are working with radio or check boxes. For the other HTML nodes like divs, sections or spans for example we need to get the value of the textContent property. If there is no textContent defined then innerHTML will produce similar results. Let’s use another switch statement:
api.val = function(value) { if(!this.el) return null; var set = !!value; var useValueProperty = function(value) { if(set) { this.el.value = value; return api; } else { return this.el.value; } } switch(this.el.nodeName.toLowerCase()) { case ‘input’: break; case ‘textarea’: break; case ‘select’: break; default: } return set ? api : null; }
First of all we need to have api.el defined. The variable set is a boolean telling us if we are retrieving or setting the value of the element. There is a helper method defined for those elements which have .value property. The switch will contain the actual logic of the method. At the end we are returning the API itself in order to chain the methods of the class. Of course we are doing this only if we are using the function as a setter.
Let’s see how to handle the different types of elements. For example the input node:
case ‘input’: var type = this.el.getAttribute(‘type’); if(type == ‘radio’ || type == ‘checkbox’) { var els = qsa(‘[name=”‘ + this.el.getAttribute(‘name’) + ‘”]’, parent); var values = []; for(var i=0; i<els.length; i++) { if(set && els[i].checked && els[i].value !== value) { els[i].removeAttribute(‘checked’); } else if(set && els[i].value === value) { els[i].setAttribute(‘checked’, ‘checked’); els[i].checked = ‘checked’; } else if(els[i].checked) { values.push(els[i].value); } } if(!set) { return type == ‘radio’ ? values[0] : values; } } else { return useValueProperty.apply(this, [value]); } break;
This is may be the most interesting case. There are two types of elements which need to be processed differently – radio and check boxes. These elements are grouped into sets and we need to keep this in mind. That’s why we are using querySelectorAll to fetch the whole group and find out which one is selected/checked. It’s even more complex, because a group of check boxes could have more then one value. The method above successfully handles all these situations.
The processing of a textarea element is pretty simple thanks to the helper we wrote above.
case ‘textarea’: return useValueProperty.apply(this, [value]); break;
Here’s how we handle a drop down (select):
case ‘select’: if(set) { var options = qsa(‘option’, this.el); for(var i=0; i<options.length; i++) { if(options[i].getAttribute(‘value’) === value) { this.el.selectedIndex = i; } else { options[i].removeAttribute(‘selected’); } } } else { return this.el.value; } break;
And this will process everything else:
default: if(set) { this.el.innerHTML = value; } else { if(typeof this.el.textContent != ‘undefined’) { return this.el.textContent; } else if(typeof this.el.innerText != ‘undefined’) { return typeof this.el.innerText; } else { return this.el.innerHTML; } } break;
With these lines of code we have finished our val method. Here is a short HTML form and its corresponding test:
<form> <input type=”text” value=”sample text” /> <input type=”radio” name=”options” value=”A”> <input type=”radio” name=”options” checked value=”B”> <select> <option value=”10″></option> <option value=”20″></option> <option value=”30″ selected></option> </select> <footer>version: 0.3</footer> </form>
If we use the following code:
dom({ name: ‘[type=”text”]’, data: { options: ‘[type=”radio”]’, count: ‘select’ }, version: ‘footer’ }, ‘form’).val();
We will get:
{ data: { count: “30”, options: “B” }, name: “sample text”, version: “version: 0.3” }
This method could be really helpful if you want to translate a data from HTML form into JavaScript object. This is a pretty common task that many of us need to accomplish almost every day.
Final result
The finished class is only 100 lines of code but it still gives us what we need to access DOM elements and to get or set their value/content.
var dom = function(el, parent) { var api = { el: null } var qs = function(selector, parent) { parent = parent || document; return parent.querySelector(selector); }; var qsa = function(selector, parent) { parent = parent || document; return parent.querySelectorAll(selector); }; switch(typeof el) { case ‘string’: parent = parent && typeof parent === ‘string’ ? qs(parent) : parent; api.el = qs(el, parent); break; case ‘object’: if(typeof el.nodeName != ‘undefined’) { api.el = el; } else { var loop = function(value, obj) { obj = obj || this; for(var prop in obj) { if(typeof obj[prop].el != ‘undefined’) { obj[prop] = obj[prop].val(value); } else if(typeof obj[prop] == ‘object’) { obj[prop] = loop(value, obj[prop]); } } delete obj.val; return obj; } var res = { val: loop }; for(var key in el) { res[key] = dom.apply(this, [el[key], parent]); } return res; } break; } api.val = function(value) { if(!this.el) return null; var set = !!value; var useValueProperty = function(value) { if(set) { this.el.value = value; return api; } else { return this.el.value; } } switch(this.el.nodeName.toLowerCase()) { case ‘input’: var type = this.el.getAttribute(‘type’); if(type == ‘radio’ || type == ‘checkbox’) { var els = qsa(‘[name=”‘ + this.el.getAttribute(‘name’) + ‘”]’, parent); var values = []; for(var i=0; i<els.length; i++) { if(set && els[i].checked && els[i].value !== value) { els[i].removeAttribute(‘checked’); } else if(set && els[i].value === value) { els[i].setAttribute(‘checked’, ‘checked’); els[i].checked = ‘checked’; } else if(els[i].checked) { values.push(els[i].value); } } if(!set) { return type == ‘radio’ ? values[0] : values; } } else { return useValueProperty.apply(this, [value]); } break; case ‘textarea’: return useValueProperty.apply(this, [value]); break; case ‘select’: if(set) { var options = qsa(‘option’, this.el); for(var i=0; i<options.length; i++) { if(options[i].getAttribute(‘value’) === value) { this.el.selectedIndex = i; } else { options[i].removeAttribute(‘selected’); } } } else { return this.el.value; } break; default: if(set) { this.el.innerHTML = value; } else { if(typeof this.el.textContent != ‘undefined’) { return this.el.textContent; } else if(typeof this.el.innerText != ‘undefined’) { return typeof this.el.innerText; } else { return this.el.innerHTML; } } break; } return set ? api : null; } return api; }
I’ve created a JSBin example that you can play with to see how the class works.
Summary
The class I discussed above is part of the AbsurdJS client-side components. The full documentation for the module could be found here. The aim of the code is not to replace jQuery or the dozens of popular libraries available for DOM access. The idea of the function is to be independent, to do only one thing and to do it well. Which is the main concept behind AbsurdJS and its build-in modules like the router or Ajax wrapper.
You must be logged in to post a comment.