bentrask.com > Notes & Essays >
The Four Basic Functions of Javascript DOM Manipulation
July 2011
The DOM interface in Javascript is very flexible, but it lacks a set of robust patterns that can be applied consistently for common types of manipulations.
I’m not a fan of massive Javascript libraries or “re-imaginings” of the entire language. I think Javascript is pretty good as is. I also think that there is a class of problems—in the world—where the cure is worse than the disease, and the best thing you can do is live with them. Every language has its share of problems in that category.
The functions I’m about to describe can be declared at the top level, but for convenience, I’ll group them into an object called DOM. This allows use similar to modules in Node.js (although browser-side, without require() or anything fancy).
None of these functions have outside dependencies, and all of them should work in almost any browser, even IE6.
var DOM = {};
DOM.clone() is a simple but complete template engine in 11 lines of code.
DOM.clone = function(id, childByID) {
var element = document.getElementById(id).cloneNode(true);
element.id = "";
if(childByID) (function findIDsInElement(elem) {
var children = elem.childNodes, length = children.length, i = 0, dataID;
if(elem.getAttribute) dataID = elem.getAttribute("data-id");
if(dataID) childByID[dataID] = elem;
for(; i < length; ++i) findIDsInElement(children[i]);
})(element);
return element;
};
To use it, add a template element to your markup:
<div class="storage invisible">
<div id="myTemplate">
<div data-id="myField"></div>
</div>
</div>
Then create a clone of it like this:
var fields = {};
DOM.clone("myTemplate", fields);
alert(fields.myField);
You can pass any object to receive the template’s fields. You can even use it inside of a constructor to expose all of the template’s fields on the constructed object.
function MyObject() {
this.element = DOM.clone("myTemplate", this);
}
Because these fields are declared using the custom data-id attribute, they are visible only to the clients of DOM.clone(), and have no chance of conflicting with CSS classes, “real” ids, or other templates. Custom attributes prefixed with data- are explicitly allowed in HTML5, but have been supported by all browsers for ages.
In 13 lines of code, DOM.classify() defines a consistent system for modifying element attributes. Almost all attribute modifications should be predefined as CSS classes; then, these attributes can be changed purely by changing element classes using this function.
DOM.classify = function(elem, className, add) {
var classes = (elem.className || "").split(" "),
changed = (className || "").split(" "),
length = changed.length, i = 0, index;
if(add || undefined === add) for(; i < length; ++i) {
index = classes.indexOf(changed[i]);
if(index < 0) classes.push(changed[i]);
} else for(; i < length; ++i) {
index = classes.indexOf(changed[i]);
if(index >= 0) classes.splice(index, 1);
}
elem.className = classes.join(" ");
};
This function can add or remove any number of classes simultaneously. They are expected to be passed as a string, the same way they are defined in HTML.
DOM.classify(myElem, "active highlighted");
DOM.classify(myElem, "invisible", false);
In particular, invisible is one class that I find extremely useful. With this class and DOM.classify(), there is no need for special “hide” or “show” functions, and it is easy to specify initial visibility state directly in HTML.
.invisible { display: none !important; }
I normally avoid the !important CSS attribute, but in this case I think it’s warranted (otherwise, invisible can be overridden by accident, for example when making an element inline-block). I also declare this class directly in my HTML documents, instead of in the external style sheet, to ensure that it always loads. (Note that the template storage div earlier was also declared invisible.)
(Some web developers will tell you CSS should be used exclusively for presentation, not for defining or manipulating content. Adding presentation classes to the HTML admittedly violates this rule. In my opinion, the idea is important to understand, but it is harmful when overused. Since a single element can have any number of classes, it is fine to use both semantic as well as aesthetic labels. Adding extra, “meaningless” classes to the HTML provides useful hints for alternative style sheets, and does no harm to those that wish to ignore them.)
Note: HTML5 defines the new hidden attribute that works much like this class, but it isn’t widely supported yet.
DOM.classify() does have some limitations. It doesn’t handle animations (although this could be worked around using CSS3), and it doesn’t work when the necessary attributes aren’t known ahead of time, or there are too many to list (for example, setting a user-defined color, or positioning an element under the mouse). These limitations seem perfectly acceptable for most use cases.
DOM.fill() (12 lines of code) is a function for changing the children of an element. It can completely empty an element, or replace an element’s contents with any number of new elements. It automatically handles strings and numbers by creating text nodes for them.
DOM.fill = function(elem, child1, child2, etc) {
var i = 1, type;
while(elem.hasChildNodes()) elem.removeChild(elem.firstChild);
for(; i < arguments.length; ++i) if(arguments[i]) {
type = typeof arguments[i];
if("string" === type || "number" === type) {
elem.appendChild(document.createTextNode(arguments[i]));
} else {
elem.appendChild(arguments[i]);
}
}
};
DOM.fill() accepts as many additional arguments as necessary, but the majority of the time, it is used with only one or two arguments.
DOM.fill(myElem); // Empties
DOM.fill(myElem, otherElem); // Replaces content with element
DOM.fill(myElem, "Some text"); // Replaces content with string
DOM.fill() is designed to be used alongside built-in manipulation methods like element.appendChild() and element.insertBefore().
The last function, DOM.remove(), is only 3 lines of code. It serves as a small convenience to make removing elements from the DOM tree easier.
DOM.remove = function(elem) {
if(elem.parentNode) elem.parentNode.removeChild(elem);
};
Together, these four functions define a workflow that is both simple and expressive. I’ve been using these functions in real Javascript projects for over a year now, and I have rarely had the need to deviate from them. Of course, a typical application needs many other ways of manipulating the DOM as well, but these are the fundamental components (and, in many cases, the building blocks for more complex behavior).
These functions were slightly cleaned up for this article. If I have added any last minute errors, if you noticed any omissions, or if you have suggestions for improvement, please email me.