Creating your own framework
That's all I want this example code to do which if you think about it is pretty powerful stuff anyway. The code format is loosely based on JQuery and if you have ever looked at JQueries source code and wondered what the hell is going on then this example might help you get your head round it.
Now this self calling anonymous function ensures that any objects defined within it are created and returned when the function runs. The open and close () brackets at the end of the anonymous function ensure this. Plus any variables declared within the function are only accessible inside the anonymous function which means we are not polluting the global namespace. To learn more about this modular method please read the following article:
The first test has to convert myVar to a string whereas the second test compares one undefined value against the other which is much quicker.
As you can see these methods are all chainable in that they can be referenced in the following manner:
I also extend my Getme.prototype object with Getme.funcs once both objects have been defined. This is to allow any Getme objects to have the benefit of having these methods added to their prototype.
Which passes off any selectors for browsers with no querySelectorAll support to Getme.find which is a pointer to the Sizzle function. If you scroll to the bottom of the Sizzle code you will see I have hooked in my Getme object in a similar way as JQuery hooks itself into Sizzle e.g
If you wanted to use Sizzle in your own framework or DOM manipulator library then its a simple case of referencing it in your code and either calling the Sizzle functions directly or doing what JQuery and Getme does and point your own functions at Sizzles own functions.
Lets build our own mini framework. Not to replace the many brilliant frameworks already out there but to educate ourselves in how we would go about such a task. Many of you maybe using JQuery, Prototype. YUI or MooTools but have no idea how that code actually works and looking at the source code may just put you off wanting to know. Therefore this article is going to show you how we can create our own basic framework which we could extend and use ourselves if we so wished or we could build it just for fun as a way to improve our JavaScript programming knowledge as well as helping us understand why frameworks are built the way they are.
If you want to jump straight in and have a look at the full source code first then you can download the latest release of the code from here: http://www.strictly-software.com/scripts/downloads/getme-1.0.4.js
Aims of our example framework.
1. Picking an eye catching name
All the current major frameworks have great names that are easy to remember such as JQuery, Dojo and MooTools.
I am going to call this framework Getme for the simple reason it Gets me what I want. I already use a function in a lot of my code including Strictly.js called getEl which has a short name of G so Getme seems like a logical progression.
Also I am currently in the process of building an AJAX based add-on that is based on accessing remote content and reformatting it for use on websites in various ways so the name Getme.js seems to be in keeping with the aims of this add-on which will no doubt implement this code or a variation of it when complete.
Also I am currently in the process of building an AJAX based add-on that is based on accessing remote content and reformatting it for use on websites in various ways so the name Getme.js seems to be in keeping with the aims of this add-on which will no doubt implement this code or a variation of it when complete.
One of the things I tend to do when creating a codebase or function libaray is give all my important functions two names. The first name is the more descriptive one such as Getme and the second one is usually a single capital letter which I will use to reference the function once the code has been compressed with my compressor tool e.g
G = getEl = function(id){ return document.getElementById(id); }
2. Main Functionality
The codebase is a very cut down version of a framework designed to show how you would go about creating your own as well as helping us understand what's going on when we look through the source code of other popular frameworks therefore the functionality will be very minimal. However it would be nice if we could do the following:
1. Return DOM nodes in a variety of manners using the same function. Similar to how JQuery lets you pass in CSS-Selectors to the $ function.
My function will do without the onDOMLoad function that JQuery supports with its main constructor when an anonymous function is passed in but it will offer the ability to return nodes in a variety of ways including by passing the constructor an #ID, .className, <tag>, a P:first-child > SPAN CSS Selector or an HTML string which can be converted into a node list.
My function will do without the onDOMLoad function that JQuery supports with its main constructor when an anonymous function is passed in but it will offer the ability to return nodes in a variety of ways including by passing the constructor an #ID, .className, <tag>, a P:first-child > SPAN CSS Selector or an HTML string which can be converted into a node list.
2. Enable chaining so that the result of a previous DOM manipulation can be passed to subsequent functions without having to pass the previous functions result as a parameter into the next function e.g
// instead of having to do this
Getme.setHTML(Getme('DIV > P.myClass'),"<p>New HTML</p>");
//we could do this
Getme('DIV > P.myClass').setHTML("<p>New HTML</p>");
Which would set the innerHTML of any Paragraphs with a className of myClass that are descendants of DIV tags to have the value <New HTML>
3. Make use of the CSS 3 Selectors that all the major frameworks use to return DOM elements e.g
DIV#myID > P.myClass:nth-child(3)
4. Make use of internal loops so that a method applied to the result of a selector acts on all the nodes returned for example:
// set the class to "blue" and set some style properties for all elements that match the selector e.g all P tags that are descendants of DIV's
Getme('DIV > P').setAtts({class:"blue",style:"color:red;font-weight:bold"});
5. The ability to extend our object very easily by passing another object to a method that will add any properties and functions from one object to the other.
That's all I want this example code to do which if you think about it is pretty powerful stuff anyway. The code format is loosely based on JQuery and if you have ever looked at JQueries source code and wondered what the hell is going on then this example might help you get your head round it.
Starting to build our Getme.js framework - The Wrapper function
Now you may have seen in JQuery and other frameworks or functions that the entire code is wrapped in a self calling anonymous function like so:
(function(){
// set some shortcuts to optimise references to global vars
var window = this, // pointing window to this (which refers to the global object) speeds up references to window
undefined, // this creates an undefined variable we can use to test other undefined variables against
query = !!document.querySelectorAll;
// rest of code e.g object definition, functions and methods
})()
Now this self calling anonymous function ensures that any objects defined within it are created and returned when the function runs. The open and close () brackets at the end of the anonymous function ensure this. Plus any variables declared within the function are only accessible inside the anonymous function which means we are not polluting the global namespace. To learn more about this modular method please read the following article:
http://yuiblog.com/blog/2007/06/12/module-pattern/
As you can see there are a few variables declared within this function that to the beginner may see pretty odd e.g
As you can see there are a few variables declared within this function that to the beginner may see pretty odd e.g
var window = this,
undefined,
How can you set window to be equal to this?
Well the this keyword is a special keyword that refers to the context object and defaults to the global context (window) when used outside an object. If I create an instance of my Getme object and use the this keyword this inside a Getme method then it refers to the current instance of the Getme object. Used outside any object it will refer to the window object.
If you don't know much about the this keyword I suggest reading up about it as there is a well known problem in Internet Explorer where the this keyword refers to the Window object in certain cases when it shouldn't such as when event listeners are added using IE's attachEvent function. A recent blog of mine detailed one such problem that explains this issue in detail: http://blog.strictly-software.com/2009/09/trouble-with-this-keyword-and.html
If you define a function outside a namespace then you would also be able to reference it by prefixing it with window e.g a function called myFunc could be called as window.myFunc. Outside objects and functions where scope has been passed with a call or apply method the this keyword refers to the global namespace therefore setting window = this is a way of accessing the global namespace quicker than referring to it through the keyword window.
The other variable that is declared but not set to anything is undefined. Because the variable hasn't had a value set it's basically undefined and this means any comparison tests with other variables can be speeded up by comparing the variable with this undefined variable rather than having to do an equality test such as:
var myvar;
if(myvar == "undefined") alert("undefined");
if(myvar === undefined) alert("undefined");
Now to the Getme.js source code
As most DOM manipulation involves the returning of one or more nodes from the DOM the aim of this object is to enable quick retrieval of DOM nodes by various methods. The main object will be called Getme and its primary property will be an array called this.nodes which will hold the current set of nodes. For functions that only return one node element such as getElementById then this node array will contain one item located at the first index e.g
this.nodes[0];
To enable the chaining of our object methods together each method will return a reference to the Getme object after any DOM manipulation has been carried out.
Lets add some code to instantiate our Getme object which we will use as our primary method of returning DOM nodes. We will also give it a short cut name of G (for Getme) so we can access DOM elements in a similar way that JQuery and the others use $ instead of jQuery.
// the G function is a short cut to instantiating with Getme object
G = function(sel, context){
return new Getme(sel, context);
}
// the Getme constuctor pass in a selector/context
// can handle multiple forms of selector
// ID - #ID to return element by id e.g G('#myID')
// CLASS - .myClass to return elements by className
// TAG - <P> or <SPAN> to return all elements of a certain node type
// SELECTOR - DIV P SPAN to return all elements that are SPANS decendants of P who are decendants of DIV
// NodeList - <DIV><SPAN>hello there</SPAN></DIV> will return a nodelist containing the elements specified by the HTML passed in
Getme = function(sel, context){
this.Getme = "Getme version 1.0.4",
this.nodes = this.nodes || [],
this.context = this.context || document; // default context to document
// main regEx to determine if selector is HTML string, ID or classname
var re_getme = /^<([^> ]+)[^>]*>(?:.|\n)+?<\/\1>$|^(\#([-\w]+)|\.(\w[-\w]+))$/i,
// regEx to match <SPAN> <P> <H1> tags
re_tag = /^<([a-z1-9]+?)>$/i,
match;
// if no selector passed in default to document
if(!sel) sel = document;
// if we have been passed a string then this could either be
// an id of an element, a class name, a tag or an HTML string
if(typeof(sel)==="string"){
// look for a single HTML tag e.g <P> or <SPAN>
match = re_tag.exec(sel);
if(match && match[1]){
// return all nodes matching the tag
this.nodes = this.context.getElementsByTagName(match[1]);
}else{
// run regex to look for ID, class or HTML string
// match[1] = HTML string - matching start and end tag
// match[3] = ID
// match[4] = class name
var match = re_getme.exec(sel)||[];
// if we have an ID with or without a # e.g #myid or myid treated as an ID
if(match[3]){
// get element by id
this.nodes[0] = document.getElementById(match[3]);
// if we have a class name e.g .myClass
}else if(match[4]){
// get elements by class name
this.nodes = Getme.funcs.getElementsByClassName( match[4],this.context);
// if we have a valid HTML string e.g <P><STRONG>hello</STRONG></P>
}else if(match[1]){
// create nodes from html
var div = document.createElement("DIV");
div.innerHTML = sel;
this.nodes = div.childNodes;
}else{
// if querySelectorAll is available for modern browsers we can use that e.g
// FF 3.2+, Safari 3.2+, Opera 10, Chrome 3, IE 8 (standards mode)
if(query && this.context === document){
this.nodes = Getme.funcs.selector(sel);
}else{
// otherwise revert to Sizzle which makes a good job of handling older browsers
this.nodes = Getme.find(sel,this.context);
}
}
}
}else if(sel.nodeType){
// already got a node add
this.nodes[0] = sel;
}else if(Getme.funcs.isArray(sel)){
this.nodes = sel;
}else{
this.nodes = Getme.funcs.makeArray(sel);
}
this.length = this.nodes.length;
return this;
}
This function Getme which can be called with the shortcut G is the key to the whole object. It accepts two parameters sel and context. Sel (which is short for selector) can be a number of parameter types such as:
- #myID which will return a reference to an element with the ID myID
- .myClass which will return a node list of all elements with the className myClass
- <span> or <p> which will return a node list of all elements with the tag specified e.g SPAN or P
- <div><span>hello</span></div> which will be used to create a node list from the HTML specified.
- An array of elements can be passed in gained from some other method or an object hash of elements which will be converted into an array.
- DIV#myID > P:first-child or any other CSS 3 Selector can be passed in and a nodelist will be returned if the browser in question supports document.querySelectorAll. Otherwise if Sizzle has been added to the codebase then it will handle older browsers without the use of XPath.
Also notice the internal properties this.context and this.nodes as these are very important in terms of storing the current context that any selections are being carried out against such as document or a particular node. The nodes property stores the current nodelist that any selector or function has retrieved. For example if you have just returned a list of P tags then the nodes array will hold references to these items. This allows subsequent methods to access the nodelist to do any work such as set styles, clear children etc.
The Getme function uses various different methods to return the required nodelists including document.getElementById, getElementsByTagName, getElementsByClassName and a method to return elements by CSS selectors.
In fact adding complex CSS-Selector functionality is quite a simple process and for those browsers that support it such as FireFox 3.1, Safari 3.1, Opera 10 and IE 8 in standards mode a function can return nodes matching CSS 3 Selectors with only the following few lines of code which I have placed in a method called selector:
// returns elements by selector e.g DIV P SPAN
// supported in later browsers IE 8 (standards mode), FF 3.1+, Safari 3.1+
selector : function(query){
try {
return document.querySelectorAll( query )
} catch(e){}
return [];
}
Now we have our main function that instansiates our Getme object and returns various node sets in a multitude of ways we can add our other methods which make use of Javascripts prototype inheritance to add methods to any Getme objects instantiated with the G function call.
All of these functions will return a reference to this (or the Getme object) so that further methods can be appended to the end in a chain. An example of three of these functions are below:
// returns the specified element from the current nodeList
get : function(idx){
if(this.nodes && this.nodes.length>0){
return (typeof(idx)=="number") ? this.nodes[idx] : this.nodes[0];
}else{
return null;
}
},
// sets the innerHTML of one or more nodes.
setHtml : function(content){
if(content){
// if a node has been passed in we take its innerHTML
if(content.nodeType){
html = content.innerHTML;
// otherwise use the string passed to us
}else if(typeof(content)==="string"){
html = content;
}else{
html = "";
}
// remove all existing html from this node as we are re-setting it
this.each(function(){
if ( this.hasChildNodes() ){
while ( this.childNodes.length >= 1 ){
this.removeChild( this.firstChild );
}
}
this.innerHTML = html;
})
}
return this;
},
// allows Getme object to reference the static foreach function automatically passing the current node list e.g G('SPAN.myclass').each(function(){})
each : function(callback){
this.foreach(this.nodes,callback);
return this;
},
// allows the setting of style values to the current nodelist
setStyle : function(style, val){
if(style){
var self = this,
atts;
// may have passed att=val pair or a object hash for atts
if(val !== undefined){
// create object hash
atts = eval('({'+style+':"'+val+'"})');
}else{
atts = style;
}
this.each(function(){
self.setProperties.call(this.style,atts);
})
}
return this;
}
As you can see these methods are all chainable in that they can be referenced in the following manner:
G('P > SPAN.info').setHTML('Use the help icon for more info.').setStyle({color:red,fontSize:8px})
I also have added another object called Getme.funcs which holds a number of utility functions that can be called statically without a Getme object reference having to be instantiated. For example in the Getme constructor I make a call to the getElementsByClass method in the following manner:
// get elements by class name
this.nodes = Getme.funcs.getElementsByClassName( match[4],this.context);
You can also call these functions yourself without having to first create a Getme object with the G constructor e.g
// loop through an array of items in arrEls calling a function for each node in the array passed to it
Getme.funcs.foreach(arrEls, function(){ somefunc(arrEls[this] );
I also extend my Getme.prototype object with Getme.funcs once both objects have been defined. This is to allow any Getme objects to have the benefit of having these methods added to their prototype.
The .each function is a good example of a method added to the Getme object as it calls the Getme.funcs.foreach method passing in the current node list stored in this.nodes and a callback function e.g:
// allows Getme object to reference the foreach function automatically passing the current node list e.g G('<pan>').each(function(){})
each : function(callback){
this.foreach(this.nodes,callback);
return this;
}
This allows you to easily call a function for each item in a nodelist and for other methods such as setStyle or setAtts the foreach method is called internally to allow any css style values or attributes to be set for each node element currently stored in the internal this.nodes member.
Although not even nearly complete this object serves as a basic example of how a framework like JQuery operates and you should use this as a guide to creating your own DOM manipulation framework. I have added some more methods to the object which you can see from the full source code which you can download here:
You should also note that to handle CSS 3 Selectors for older browsers such as IE 5-7, FF 1-3, Safari 1-3 etc I have used Sizzle.js which is the same selector engine that JQuery uses.
You can download the version of Sizzle that I have hooked into Getme here:
This brilliant CSS selector engine which was created by John Resig makes CSS 3 Selectors available for all those browsers that don't support the querySelectorAll method available in FireFox 3.2, Safari 3.2, IE 8, Opera 10 and Chrome 3.
You will notice in my Getme constructor that I have the following branch:
// if querySelectorAll is available for modern browsers we can use that e.g
// FF 3.2+, Safari 3.2+, Opera 10, Chrome 3, IE 8 (standards mode)
if(query && this.context === document){
this.nodes = Getme.funcs.selector(sel);
}else{
// otherwise revert to Sizzle which makes a good job of handling older browsers
this.nodes = Getme.find(sel,this.context);
}
Which passes off any selectors for browsers with no querySelectorAll support to Getme.find which is a pointer to the Sizzle function. If you scroll to the bottom of the Sizzle code you will see I have hooked in my Getme object in a similar way as JQuery hooks itself into Sizzle e.g
// EXPOSE Sizzle to Getme.js
window.Sizzle = Sizzle;
Getme.find = Sizzle;
Getme.filter = Sizzle.filter;
Getme.expr = Sizzle.selectors;
Getme.expr[":"] = Getme.expr.filters;
Hopefully this article has been helpful in explaining to new developers how a DOM manipulator library could be build with similar features to other frameworks such as JQuery.
This example Getme.js is not a complete framework and has not been built to become one rather its an example of how easy it is to utilise other useful libraries such as Sizzle and add them to your own code without having to worry about all the bloat that many frameworks come with. You can start off with a basic piece of code to manipulate and search for DOM nodes and then extend it as and when you require new functionality.
This example Getme.js is not a complete framework and has not been built to become one rather its an example of how easy it is to utilise other useful libraries such as Sizzle and add them to your own code without having to worry about all the bloat that many frameworks come with. You can start off with a basic piece of code to manipulate and search for DOM nodes and then extend it as and when you require new functionality.
10 comments:
This is great article just what I wanted to know. I have often tried to look at the source code in Jquery and just spent a lot of time scratching my head wondering what is going on.
I never realised that to return nodes by CSS 3 selectors was so easy in modern browsers.
I think I might even use this Getme.js as the basis of my own framework.
Thanks!
I'm glad you found the article helpful.
The code in Getme.js is only a small cut down version of what is possible and I am planning on using it as the base for my own framework that I am working on which is designed more for working with remote content and parsing, cleaning it for use on other sites e.g my www.hattrickheaven.com site. If you click on one of the news articles you will see a little loader icon whilst the content is taken from the remote URL, parsed and formatted in real time before being inserted into the DOM
Very helpful article, many thanks, I'm currently trying to put together a very small libray to do some iPhone web app work, and this is just the sort of think I needed to get me started.
BTW I tried using it in Safari and I got an error TypeError: Result of expression 'list.call' [undefined] is not a function. in line 341. Any ideas?
Can you give me an example of the code you are trying to pass to the Getme object or a link to the test page?
Line 341 is
// if typeof list is a function then run that to generate node list
if(typeof(list) == "function"){
list = list.call();
which is in the foreach function and is designed to create a nodelist from a function passed as a parameter. The output of this function is then used as the nodelist to run your callback functions against. Whereas if you just used .each it would automatically pass the current this.nodes property.
I would need to see the function you are passing into the .foreach method.
Also you could try replacing the line
list = list.call();
with
list = list();
to see if that works.
I don't know what version of Safari you are using or what function object support it has on the IPHONE.
If list = list(); works then you could try
if(Function.call){
list = list.call():
}else{
list = list();
}
As this would handle old and new browser / JS implementations.
Even if you get it working can you please sent me over the code either as a comment or to my email so I can test it myself on Safari and provide details of the webkit version etc.
Thanks
}
Hi,
I did a little checking myself on the Safari issue and I think I've fixed it. I wasn't doing anything difficult just trying to set the the content of a div tag to hello world using the setHtml function. Works fine in Firefox but produces a type of error in Safari.
This piece of code here produces the error:
if(typeof(list) == "function"){
list = list.call();
}
So I checked the result of typeof(list) in Firefox and it said 'Object' but Safari thought it (erroneously) was a 'Function'
A quick check on Google then produced this article:
http://juhukinners.com/2009/01/11/typeof-considered-useless-or-how-to-write-robust-type-checks/
which explains why typeof isn't a good way to check for Functions, since Safari has a problem with that. So I switched the code out for:
if((list) instanceof Function){
list = list.call();
}
like the article suggests and also in the isFunction part as well:
isFunction : function(o){
return ((o) instanceof Function);
},
and now it works. Cool :)
Chris.
Hi Chris
Can you show me how you were trying to call the setHtml function as by the sounds of it you were passing the results of a function in to foreach for the list parameter instead of an actual function.
For example the following code has been tested in FF 3, Safari 4, Chrome 3 and IE 8 and works fine
with my isFunction test just doing a test like so
return (typeof(o)=="function")
The foreach method is called by the each method (used by setStyle, setAtts etc) and the the list parameter can be passed as either a nodelist/array OR a function that returns a nodelist/array.
I read the article you mentioned and I know that Jquery does use a different method for checking for functions but this doesn't seem to work in IE 7 or 8.
For example if you run the following code
function test(){
isFunction1 = function( obj ) {
return toString.call(obj) === "[object Function]";
}
isFunction2 = function( obj ) {
return (typeof(obj)=="function")
}
r = function (){return Getme.funcs.getElementsByClassName("rob",null,"P")}
ShowDebug("typeof(r) = " + (typeof(r)))
ShowDebug("is r a function = " + isFunction2(r))
ShowDebug("is r a function = " + isFunction1(r))
}
// where ShowDebug is my function on the test page outputting messages to the main DIV or console.
I ran this code on the following browsers and got these results:
Firefox 3.5
1: typeof(r) = function
2: is r a function = true
3: is r a function = false
IE 7
1: typeof(r) = function
2: is r a function = true
object doesn't support this property or method toString.call(obj)
IE 8
1: typeof(r) = function
2: is r a function = true
object doesn't support this property or method toString.call(obj)
Chrome 3.0
1: typeof(r) = function
2: is r a function = true
3: is r a function = true
Safari 4.0.3
1: typeof(r) = function
2: is r a function = true
3: is r a function = true
Opera 10
1: typeof(r) = function
2: is r a function = true
3: is r a function = true
So it seems different browsers act in their own way and maybe both methods need to be modified to work cross browser?
You still didn't tell me which version of Safari you were using and was it on a Mac or PC??
Also show me some example code and I will have a look at it.
The code I used was:
G('div').setHtml('<p>Hello World</p>');
C.
And which version of Safari?
Was it on a PC or MAC?
I have been running it on Safari 3.2 > 4.0 and its worked fine for me.
Hi Chris
You still haven't got back to me about the computer you were running this on e.g Mac or PC.
If you were running it on an IPhone then I actually haven't tested Getme on that yet as I don't own one. However I know plenty of people that do.
Let me know so that I can resolve the issue.
Thanks
Rob
I managed to see the problem you were experiencing on a friends iphone with a new test page I created.
I have updated the Getme source code to use this new test for isFunction as well as replacing the code within the foreach method to use isFunction instead of doing its own check.
Post a Comment