Chapter 12 – Creating dom.js – a wishful jQuery inspired DOM Library for modern browers

最后更新于:2022-04-01 04:39:34

## 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 the*dom()* 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](http://benalman.com/news/2010/11/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](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()* and*GetOrMakeDom.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 the*GetOrMakeDOM* 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](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](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* to*GetOrMakeDOM.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](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/new) 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 to*dom()*. 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](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](http://javascriptweblog.wordpress.com/2011/02/07/truth-equality-and-javascript/)) 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 fragmen](https://developer.mozilla.org/en-US/docs/DOM/DocumentFragment)t 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](https://developer.mozilla.org/en-US/docs/DOM/HTMLCollection), [node list](https://developer.mozilla.org/en-US/docs/DOM/NodeList), array, string[selector](http://www.quirksmode.org/css/contents.html), or an object created from *dom()*. If its a string selector, a node list is created by calling the*queryselectorAll()* 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](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 the*GetOrMakeDom* constructor function. The code below setups the *each()* method. github code: [https://github.com/codylindley/domjs/blob/master/builds/dom.js](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 *getOrMakeDom*object 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 *getOrMakeDom*function. github code: [https://github.com/codylindley/domjs/blob/master/builds/dom.js](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 the*innerHTML* 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](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](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](https://github.com/codylindley/domjs/tree/master/test) that we are now going to run outside of the testing framework. However, you can also [run the testing framework](https://github.com/codylindley/domjs/blob/master/test/index.html) to see dom.js in action. The follow code demostrates the code create in this chapter. live code: [http://jsfiddle.net/domenlightenment/7aqKm](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](https://github.com/codylindley/hippojs), which is an exercise in re-creating the jQuery DOM methods for modern browsers. Both [dom.js](https://github.com/codylindley/domjs) and [hippo.js](https://github.com/codylindley/hippojs) make use of [grunt](http://gruntjs.com/), [QUnit](http://qunitjs.com/), and[JS Hint](http://jshint.com/) 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](http://coding.smashingmagazine.com/2012/10/09/designing-javascript-apis-usability/)". Now go build something for the DOM.
';