Thursday 2 February 2012

dojo.query -The Magical DOM Selector and Best practices

One of the most powerful tool which is a quintessential part of any javascript library is the DOM selector function in the library .
Given an HTML snippet as

<div id="menuList">
    <span class="sub">sm Item</span>
    <span class="sub active">
        <span class="superSub">smthing more </span>       
    </span>
    <span class="sub">sm Item</span>
    <span class="sub">sm Item</span>
</div>

Normally in simple javascript the way to get the DOM element would be something like this :
var menu=document.getElementById("menuList");
//Get sub elements
var subItems=menu.childNodes;

Then you run a loop on the subItems to check if it is an Element or Text node and then apply the busines logic :
for( var i =0 ;i<subItems.length;i++){
    if(subItems[i].nodeType==document.ELEMENT_NODE){
        //Do something with the node
    }
}

Now the same thing using Dojo would be
    dojo.query("#menuList > span").forEach(function(x){
        //Do something with this
    });

Test 1:
        Usecase :     Modify sub menu items under id="menuList"
        Features Used : dojo.query with forEach on it
Well that drastically simplifies the code and makes it more readable , but that is just the tip of the  iceberg
Test 2 :
    Usecase : Create a function which takes the index of the sub menu item to make active ( i.e add the class active to it ) . There can be only one active item

Simple Javascript :
-First we need to get all the span elements under the menu in the same way
 -then check for the class name , remove the class active without affecting the class="sub" which is already there
 -add the class active for the required inde
Code :
        var subItems=menu.childNodes;
        for( var i =0 ;i<subItems.length;i++){
            if(subItems[i].nodeType==document.ELEMENT_NODE){
                subItems[i].className=subItems[i].className.split("active").join("");           
                if(i==INDEX)
                    subItems[i].className=subItems[i].className+" active";
            }
        }   

Result :
Achieved with relative ease but with extremely inefficient (who cares about efficiency in JS ! :) ) string functions .Biggest issue is the possibility of bugs in the logic if the string "active" instead of " active" , the code would have failed.More possibilities of bugs are there in a relatively very common activity .
   
Dojo :
Similar procedure :
Imagine doing the same thing with dojo.addClass and dojo.removeClass .It makes it much more easier .
2 Ways :
        Way 1 :
            var i=0;
            dojo.query("#menuList > span").forEach(function(x){
                if(i!=INDEX)
                    dojo.removeClass(x,"active");               
                else
                    dojo.addClass(x,"active");               
                i++;
            });

           
        Way 2 :
  But the magic of dojo.query is that most DOM utility functions can be applied on returned array directly.So , Best practice :
      dojo.query("#menuList > span").removeClass("active");
   dojo.addClass(dojo.query("#menuList > span")[INDEX],("active"));

    Result : 
Well,thats 2 lines of code with not a for loop in sight (ok ,its inside the function ) and which is more intutive,readable and easier to maintain.The possibility of bugs are also reduced to a great extent .   
Now in the same way we could do various things like :
    -> dojo.query(".....").style(PropertyName,NewValue)
    -> dojo.query(".....").attr(PropertyName,NewValue)  - Especially useful when we set properties in HTML as custom attributes
    -> ofcourse dojo.query(".....").addClass , dojo.query(".....").removeClass , dojo.query(".....").toggleClass
    -> Attach EventListeners - the most useful aspect

    Example Usecase : Onclick of any submenu item should make them active and rest inactive  :
    Code :
            dojo.query("#menuList > span").onclick(function(){
                    dojo.query("#menuList > span").removeClass("active");
                    dojo.addClass(this,"active");
            });

 This leads to much cleaner code than mixing the onclick with the html and calling a function .
   
The Problem of the unique ID :
Normally when we initially start writing JS we tend to assign ID's in too many places.Every DOM element that we need gets assigned an ID adn we directly get that element and work with it in our JS function -animation , validation e.t.c.
But We soon run up into the following issues :
 -> Run out of unique ID's - For someone like me used to variable names like "kk" , "i" and "asd" ("foo" for many others) , the ID's start getting longer and
 the chance of duplicity increases and with it unknown bugs
 -> May not have control of the JSP ( or anyother thing for that matter ) for which the JS is to be written which means that we need to contantly trouble the other guy to add Id's or do code merges ( god save your code when merging it )
 -> Duplicity of code , we might not see the generalization of business logic and end up writing duplicate code for what could be essentially the same thing

Well , the best practice would be to seperate discrete UI components like Top Menu , Body ,Status Bar e.t.c with discrete ID's and tag similar UI compontents with similar class name.
Then, the DOM element which needs to be selected should be queried according to the DOM structure .
Example :
Given the snippet
<body>
    <div id="menuList">
        <span class="sub">sm Item</span>
        <span class="sub active">
            <span class="superSub">smthing more </span>       
        </span>
        <span class="sub">sm Item</span>
        <span class="sub">sm Item</span>
    </div>
    <div id="contentbody">
        ...
    </div>
    <div id="commentContainer">
        <div class="comment">....</div>
        <div class="comment">....</div>
        <div class="comment">....</div>
    </div>
    <div id="footer">
    </div>
</body>

   
In this way the HTML should have as few Id's as possible so that CSS and JS are lot simpler and generalized to avoid duplicity of codes which directly reduces the number of bugs (lesser work for the M&E team - seems bad for me in the long run  :) ) . Especially now that we have dojo.query , it is much better to stop avoiding the usage of a thousand id's and rather an expression to get to the DOM element .
Something like :
dojo.query("#idName class1 tag1.className2 > span")
and then use it to implement JS logic .

2 comments: