12.1 dom.js overview
I want you to take the information and knowledge from this book and leverage it as I walk you through a foundation for a wishful, modern, jQuery like DOM library called dom.js. Think of dom.js as the foundation to a modern library for selecting DOM nodes and doing something with them. Not unlike jQuery the dom.js code will provide a function for selecting something from the DOM (or creating) and then doing something with it. I show some examples of thedom() function below which shouldn"t look all that foreign if you are familiar with jQuery or really any DOM utility for selecting elements.
//select in a document all li"s in the first ul and get the innerHTML for the first li
dom("li","ul").html();
//create html structure using a document fragment and get the innerHTML of ul
dom("<ul><li>hi</li></ul>").html()
For most readers this chapter is simply an exercise in taking the information in this book and applying it to a JavaScript DOM library. For others, this might just shed some light on jQuery itself and any DOM manipulation logic used in JavaScript frameworks today. Ideally, in the end, I hope this exercise inspires readers to craft their own micro DOM abstractions on an as needed bases when the situation is right. With that said, lets begin.
12.2 Create a unique scope
To protect our dom.js code from the global scope, I will first create a unique scope to which it can live and operate within without fear of collisions in the global scope. In the code below I setup a pretty standard Immediately-Invoked Function Expression to create this private scope. When the IIFE is invoked the value of global will be set to the current global scope (i.e. window).
github code: https://github.com/codylindley/domjs/blob/master/builds/dom.js
(function(win){
var global = win;var doc = this.document;
}}(window);
Inside of the IIFE we setup a reference to the window and document object (i.e. doc) to speed up the access to these objects inside of the IIFE.
12.3 Create the dom() and GetOrMakeDom() functions exposing dom() andGetOrMakeDom.prototype to the global scope
Just like jQuery we are going to create a function that will return a chain-able, wrapped set (i.e. custom array like object) of DOM nodes (e.g. {0:ELEMENT_NODE,1:ELEMENT_NODE,length:2}) based on the parameters sent into the function. In the code below I setup the dom() function and parameters which get passed on to theGetOrMakeDOM constructor function that when invoked will return the object containing the DOM nodes, that is then returned by from dom().
github code: https://github.com/codylindley/domjs/blob/master/builds/dom.js
(function(win){
var global = win;var doc = global.document;
var dom = function(params,context){
return new GetOrMakeDom(params,context);
};
var GetOrMakeDom = function(params,context){
};
})(window);
In order for the dom() function to be accessed/called from outside of the private scope setup by the IIFE we have to expose the dom function (i.e. create a reference) to the global scope. This is done by creating a property in the global scope called dom and pointing that property to the local dom() function. When dom is accessed from the global scope it will point to my locally scoped dom() function. In the code below doing, global.dom = dom; does the trick.
github code: https://github.com/codylindley/domjs/blob/master/builds/dom.js
(function(win){
var global = win;var doc = global.document;
var dom = function(params,context){
return new GetOrMakeDom(params,context);
};
var GetOrMakeDom = function(params,context){
};
//expose dom to global scopeglobal.dom = dom;
})(window);
The last thing we need to do is expose the GetOrMakeDom.prototype property to the global scope. Not unlike jQuery (e.g. jQuery.fn) we are simply going to provide a shortcut reference from dom.fn toGetOrMakeDOM.prototype. This is shown in the code below.
(function(win){
var global = win;var doc = global.document;
var dom = function(params,context){
return new GetOrMakeDom(params,context);
};
var GetOrMakeDom = function(params,context){
};
//expose dom to global scopeglobal.dom = dom;
//short cut to prototypedom.fn = GetOrMakeDom.prototype;
})(window);
Now anything attached to the dom.fn is actually a property of the GetOrMakeDOM.prototype object and is inherited during property lookup for any object instance created from the GetOrMakeDOM constructor function.
Notes
The getOrMakeDom function is invoked with the new operator. Make sure you understand what happens when a function is invoked using the new operator.
12.4 Create optional context paramater passed to dom()
When dom() is invoked, it also invokes the GetOrMakeDom function passing it the parameters that are sent todom(). When the GetOrMakeDOM constructor is invoked the first thing we need to do is determine context. The context for working with the DOM can be set by passing a selector string used to select a node or a node reference itself. If its not obvious the purpose of passing a context to the dom() function provides the ability to limit the search for element nodes to a specific branch of the DOM tree. This is very similar, almost identical, to the second parameter passed to the jQuery or $ function. In the code below I default the context to the current document found in the global scope. If a context parameter is available, I determine what it is (i.e. string or node) and either make the node passed in the context or select a node via querySelectorAll().
github code: https://github.com/codylindley/domjs/blob/master/builds/dom.js
(function(win){
var global = win;var doc = global.document;
var dom = function(params,context){
return new GetOrMakeDom(params,context);
};
var GetOrMakeDom = function(params,context){
var currentContext = doc;
if(context){
if(context.nodeType){//its either a document node or element node
currentContext = context;
}else{ //else its a string selector, use it to selector a node
currentContext = doc.querySelector(context);
}
}
};
//expose dom to global scopeglobal.dom = dom;
//short cut to prototypedom.fn = GetOrMakeDom.prototype;
})(window);
With the context parameter logic setup we can next add the logic required to deal with the params parameter used to actually select or created nodes.
12.5 Populate object with DOM node references based on params and return object
The params parameter passed to dom(), then on to the getOrMakeDom() varies in the type of parameter that can be passed. Similar to jQuery the type"s of value"s passed can be any one of the following:
- css selector string (e.g. dom("body"))
- html string (e.g. dom("
<p>Hellow</p><p> World!</p>
")) - Element node (e.g. dom(document.body))
- array of element nodes (e.g. dom([document.body]))
- a NodeList (e.g. dom(document.body.children))
- a HTMLcollection (e.g. dom(document.all))
- a dom() object itself. (e.g. dom(dom()))
The result of passing params is the construction of a chain-able object containing references to nodes (e.g.{0:ELEMENT_NODE,1:ELEMENT_NODE,length:2}) either in the DOM or in a document fragment. Lets examine how each of the above parameters can be used to produce an object containing node references.
The logic to permit such a wide variety of parameter types is shown below in the code and starts with a simple check to verify that params is not undefined, an empty string, or a string with empty spaces. If this is the case we add a length property with a value of 0 to the object constructed from calling GetOrMakeDOM and return the object so the execution of the function ends. If params is not a false (or false like) value the execution of the function continues.
Next the params value, if a string, is checked to see if contains HTML. If the string contains HTML then a document fragment is created and the string is used as the innerHTML value for a <div>
contained in the document fragment so that the string is converted to a DOM structure. With the html string converted to a node tree, the structure is looped over accessing top level nodes, and references to these nodes are passed to the object being created by GetOrMakeDom. If the string does not contain HTML execution of the function continues.
The next check simply verifies if params is a reference to a single node and if it is we wrap a reference to it up in an object and return it other wise at we are pretty sure the params value is a html collection, node list, array, stringselector, or an object created from dom(). If its a string selector, a node list is created by calling thequeryselectorAll() method on the currentContext. If its not a string selector we loop over the html collection, node list, array, or object extracting the node references and using the references as values contained in the object sent back from calling the GetOrMakeDom.
All of this logic inside of GetOrMakeDom() function can be a bit overwhelming just realize that the point of the constructor function is to construct an object containing references to nodes (e.g.{0:ELEMENT_NODE,1:ELEMENT_NODE,length:2}) and returns this object to dom().
github code: https://github.com/codylindley/domjs/blob/master/builds/dom.js
(function(win){
var global = win;var doc = global.document;
var dom = function(params,context){
return new GetOrMakeDom(params,context);
};
var regXContainsTag = /^s*<(w+|!)[^>]*>/;
var GetOrMakeDom = function(params,context){
var currentContext = doc;
if(context){
if(context.nodeType){
currentContext = context;
}else{
currentContext = doc.querySelector(context);
}
}
//if no params, return empty dom() object
if(!params || params === "" || typeof params === "string" && params.trim() === ""){
this.length = 0;
return this;
}
//if HTML string, construct domfragment, fill object, then return object
if(typeof params === "string" && regXContainsTag.test(params)){//yup its forsure html string
//create div & docfrag, append div to docfrag, then set its div"s innerHTML to the string, then get first child
var divElm = currentContext.createElement("div");
divElm.className = "hippo-doc-frag-wrapper";
var docFrag = currentContext.createDocumentFragment();
docFrag.appendChild(divElm);
var queryDiv = docFrag.querySelector("div");
queryDiv.innerHTML = params;
var numberOfChildren = queryDiv.children.length;
//loop over nodelist and fill object, needs to be done because a string of html can be passed with siblings
for (var z = 0; z < numberOfChildren; z++) {
this[z] = queryDiv.children[z];
}
//give the object a length value
this.length = numberOfChildren;
//return object
return this; //return e.g. {0:ELEMENT_NODE,1:ELEMENT_NODE,length:2}
}
//if a single node reference is passed, fill object, return object
if(typeof params === "object" && params.nodeName){
this.length = 1;
this[0] = params;
return this;
}
//if its an object but not a node assume nodelist or array, else its a string selector, so create nodelist
var nodes;
if(typeof params !== "string"){//nodelist or array
nodes = params;
}else{//ok string
nodes = currentContext.querySelectorAll(params.trim());
}
//loop over array or nodelist created above and fill object
var nodeLength = nodes.length;
for (var i = 0; i < nodeLength; i++) {
this[i] = nodes[i];
}
//give the object a length value
this.length = nodeLength;
//return object
return this; //return e.g. {0:ELEMENT_NODE,1:ELEMENT_NODE,length:2}
};
//expose dom to global scopeglobal.dom = dom;
//short cut to prototypedom.fn = GetOrMakeDom.prototype;
})(window);
12.6 Create each() method and make it a chainable method
When we invoke dom() we can access anything attached to dom.fn by way of prototypical inheritance. (e.g.dom().each()). Not unlike jQuery methods attached to dom.fn operate on the object created from theGetOrMakeDom constructor function. The code below setups the each() method.
github code: https://github.com/codylindley/domjs/blob/master/builds/dom.js
dom.fn.each = function (callback) { var len = this.length; //the specific instance create from getOrMakeDom() and returned by calling dom() for(var i = 0; i < len; i++){
//invoke the callback function setting the value of this to element node and passing it parameters callback.call(this[i], i, this[i]); }};
As you might expect the each() method takes a callback function as a parameter and invokes the function (setting the this value to the element node object with call()) for each node element in the getOrMakeDomobject instance. The this value inside of the each() function is a reference to the getOrMakeDom object instance (e.g. {0:ELEMENT_NODE,1:ELEMENT_NODE,length:2}).
When a method does not return a value (e.g. dom().length returns a length) its possible to allow method chaning by simply returning the object the method belongs too instead of a specific value. Basically, we are returning the GetOrMakeDom object so another method can be called on this instance of the object. In the code below I would like the each() method to be chainable, meaning more methods can be called after calling each(), so I simply return this. The this in the code below is the object instance created from calling the getOrMakeDomfunction.
github code: https://github.com/codylindley/domjs/blob/master/builds/dom.js
dom.fn.each = function (callback) { var len = this.length; for(var i = 0; i < len; i++){ callback.call(this[i], i, this[i]); }
return this; //make it chainable by returning e.g. {0:ELEMENT_NODE,1:ELEMENT_NODE,length:2}};
12.7 Create html(), append(), & text() methods
With the core each() method created and implicit iteration avaliable we can now build out a few dom() methods that act on the nodes we select from an HTML document or create using document fragments. The three methods we are going to create are:
- html() / html("html string")
- text() / text("text string")
- append("html | text | dom() | nodelist/HTML collection | node | array")
The html() and text() methods follow a very similar pattern. If the method is called with a parameter value we loop (using dom.fn.each() for implicit iteration ) over each element node in the getOrMakeDom object instance setting either the innerHTML value or textContent value. If no parameter is sent we simply return theinnerHTML or textContent value for the first element node in the getOrMakeDom object instance. Below you will see this logic coded.
github code: https://github.com/codylindley/domjs/blob/master/builds/dom.js
dom.fn.html = function(htmlString){
if(htmlString){
return this.each(function(){ //notice I return this so its chainable if called with param
this.innerHTML = htmlString;
});
}else{
return this[0].innerHTML;
}
};
dom.fn.text = function(textString){
if(textString){
return this.each(function(){ //notice I return this so its chainable if called with param
this.textContent = textString;
});
}else{
return this[0].textContent.trim();
}
};
The append() method leveraging insertAdjacentHTML will take a an html string, text string, dom() object, nodelist/HTML collection a single node or array of nodes and appends it to the nodes selected.
github code: https://github.com/codylindley/domjs/blob/master/builds/dom.js
dom.fn.append = function(stringOrObject){ return this.each(function(){ if(typeof stringOrObject === "string"){ this.insertAdjacentHTML("beforeend",stringOrObject); }else{ var that = this; dom(stringOrObject).each(function(name,value){ that.insertAdjacentHTML("beforeend",value.outerHTML); }); } });};
12.8 Taking dom.js for a spin
During the creation of dom.js I created some very simple qunit tests that we are now going to run outside of the testing framework. However, you can also run the testing framework to see dom.js in action. The follow code demostrates the code create in this chapter.
live code: http://jsfiddle.net/domenlightenment/7aqKm
<!DOCTYPE html>
<html lang="en">
<body>
<ul><li>1</li><li>2</li><li>3</li></ul>
<script src="https://raw.github.com/codylindley/domjs/master/builds/dom.js"></script>
<script>
//dom()console.log(dom());console.log(dom(""));console.log(dom("body"));console.log(dom("<p>Hellow</p><p> World!</p>"));console.log(dom(document.body));console.log(dom([document.body, document.body]));console.log(dom(document.body.children));console.log(dom(dom("body")));
//dom().html()console.log(dom("ul li:first-child").html("one"));console.log(dom("ul li:first-child").html() === "one");
//dom().text()console.log(dom("ul li:last-child").text("three"));console.log(dom("ul li:last-child").text() === "three");
//dom().append()dom("ul").append("<li>4</li>");dom("ul").append(document.createElement("li"));dom("ul").append(dom("li:first-child"));
</script>
</body>
</html>
12.9 Summary & continuing on with dom.js
This chapter has been about creating a foundation to a jQuery like DOM library. If you"d like to continue studying the building blocks to a jQuery-like DOM library I would suggest checking out hippo.js, which is an exercise in re-creating the jQuery DOM methods for modern browsers. Both dom.js and hippo.js make use of grunt, QUnit, andJS Hint which I highly recommend looking into if building your own JavaScript libraries is of interest. In addition to the fore mentioned developer tools I highly recommending reading, "Designing Better JavaScript APIs". Now go build something for the DOM.