Fluent DOM Manipulation in JavaScript

Contents hide

This article was originally published in my blog (affectionately referred to as blargh) on . The original blog no longer exists as I've migrated everything to this wiki.

The original URL of this post was at https://tmont.com/blargh/2009/11/fluent-dom-manipulation-in-javascript. Hopefully that link redirects back to this page.

Everybody hates the DOM. Except me. I kinda love it. I get a little hit of endorphins every time I type all 23 characters of "document.getElementById". I need a cigarette.

Anyway, I found myself dissatisfied with the state of jQuery's DOM manipulation (hint: there isn't any). You can traverse, and do stuff with events, but there's no API to create elements and then append them to the DOM. So I looked at the jQuery plugins, and found this, which I tried and quickly learned to hate. Using JSON to create DOM elements is just as painful as creating them using the native DOM API.

So, like any good programmer, I rolled my own. And I called it FluentDom (or $dom for short). Tested in Firefox, IE and Opera. Licensed under the WTFPL license.

FluentDom.js

javascript
/**
 * Fluent DOM Manipulation
 *
 * @author  Tommy Montgomery
 * @license http://sam.zoy.org/wtfpl/
 */
(function(){
	var FluentDom = function(node) {
		return new FluentDomInternal(node);
	}
	
	FluentDom.create = function(tagName) {
		var f = new FluentDomInternal();
		f.create(tagName);
		return f;
	}
	
	var FluentDomInternal = function(node) {
		var root = node || null;
		
		this.fluentDom = "1.0";
		
		this.append = function(obj) {
			if (!root || !root.appendChild) {
				throw new Error("Cannot append to a non-element");
			}
			
			var type = typeof(obj);
			if (type === "object") {
				if (obj.fluentDom) {
					root.appendChild(obj.toDom());
				} else if (obj.nodeType) {
					root.appendChild(obj);
				} else {
					throw new Error("Invalid argument: not a DOM element or a FluentDom object");
				}
			} else if (type === "string" || type === "number") {
				root.appendChild(document.createTextNode(obj));
			} else {
				throw new Error("Invalid argument: not an object (you gave me a " + typeof(obj) + ")");
			}
			
			return this;
		}
		
		this.attr = function(name, value) {
			if (!root || !root.setAttribute) {
				throw new Error("Cannot set an attribute on a non-element");
			}
			
			root.setAttribute(name, value);
			return this;
		}
		
		this.text = function(text) {
			return this.append(text);
		}
		
		this.create = function(tagName) {
			root = document.createElement(tagName);
			return this;
		}
		
		this.id = function(value) {
			return this.attr("id", value);
		}
		
		this.title = function(value) {
			return this.attr("title", value);
		}
		
		this.cls = function(value) {
			return this.attr("class", value);
		}
		
		this.clear = function() {
			root = null;
			return this;
		}
		
		this.toDom = function() {
			return root;
		}
		
		this.href = function(link) {
			return this.attr("href", link);
		}
		
	};
	
	window.FluentDom = window.$dom = FluentDom;
}());

Usage

Some sample usage, also showing how to integrate with jQuery:

javascript
// let's make a list!
$dom(document.body).append(
	$dom.create("ul").id("menu").append(
		$dom.create("li").cls("menu-item").id("menu-item-1").append(
			$dom.create("a").text("List Item #1").href("#")
		)
	).append(
		$dom.create("li").cls("menu-item").id("menu-item-2").title("click to toggle").append(
			$($dom.create("a").text("List Item #2").href("#").toDom()).bind("click", function() {
				$(this).next("ul").toggle();
				return false;
			}).get(0)
		).append(
			$dom.create("ul").append(
				$dom.create("li").cls("sub-menu-item").id("menu-item-4").append(
					$dom.create("a").text("Sublist Item #1").href("#")
				)
			).append(
				$dom.create("li").cls("sub-menu-item").id("menu-item-5").append(
					$dom.create("a").text("Sublist Item #2").href("#")
				)
			).append(
				$dom.create("li").cls("sub-menu-item").id("menu-item-6").append(
					$dom.create("a").text("Sublist Item #3").href("#")
				)
			)
		)
	).append(
		$dom.create("li").cls("menu-item").id("menu-item-3").append(
			$dom.create("a").text("List Item #3").href("#")
		)
	)
);

Which creates and appends this DOM tree to document.body:

html
<ul id="menu">
  <li class="menu-item" id="menu-item-1"><a href="#">List Item #1</a></li>
  <li class="menu-item" id="menu-item-2" title="click to toggle"><a href="#">List Item #2</a>
    <ul>
      <li class="sub-menu-item" id="menu-item-4"><a href="#">Sublist Item #1</a></li>
      <li class="sub-menu-item" id="menu-item-5"><a href="#">Sublist Item #2</a></li>
      <li class="sub-menu-item" id="menu-item-6"><a href="#">Sublist Item #3</a></li>
    </ul>
  </li>
  <li class="menu-item" id="menu-item-3"><a href="#">List Item #3</a></li>
</ul>

Admittedly, it kind of looks like a lot of code, but it's much easier to read than if you had used the native API. Compare that with what the same implementation would look like if you went native:

Native API Implementation

javascript
var ul = document.createElement("ul");
ul.id = "menu";
var li = document.createElement("li");
li.className = "menu-item";
li.id = "menu-item-1";
var a = document.createElement("a");
a.href = "#";
a.appendChild(document.createTextNode("List Item #1"));
li.appendChild(a);
ul.appendChild(li);

li = document.createElement("li");
li.className = "menu-item";
li.id = "menu-item-2";
li.title = "click to toggle";
a = document.createElement("a");
a.href = "#";
a.appendChild(document.createTextNode("List Item #2"));
$(a).bind("click", function() {
	$(this).next("ul").toggle();
	return false;
});
li.appendChild(a);

var subList = document.createElement("ul");

var subItem = document.createElement("li");
subItem.className = "menu-item";
subItem.id = "menu-item-4";
var subLink = document.createElement("a");
subLink.href = "#";
subLink.appendChild(document.createTextNode("Sublist Item #1"));
subItem.appendChild(subLink);
subList.appendChild(subItem);

subItem = document.createElement("li");
subItem.className = "menu-item";
subItem.id = "menu-item-5";
subLink = document.createElement("a");
subLink.href="#";
subLink.appendChild(document.createTextNode("Sublist Item #2"));
subItem.appendChild(subLink);
subList.appendChild(subItem);

subItem = document.createElement("li");
subItem.className = "menu-item";
subItem.id = "menu-item-6";
subLink = document.createElement("a");
subLink.href="#";
subLink.appendChild(document.createTextNode("Sublist Item #3"));
subItem.appendChild(subLink);
subList.appendChild(subItem);

li.appendChild(subList);

ul.appendChild(li);

li = document.createElement("li");
li.className = "menu-item";
li.id = "menu-item-3";
a = document.createElement("a");
a.href = "#";
a.appendChild(document.createTextNode("List Item #3"));
li.appendChild(a);
ul.appendChild(li);

document.body.appendChild(ul);

30 lines vs. 70 lines. Fluent == rad. Tell your friends.