published on 09.09.2007 16:06.

refactoring five year old javascript code to use the prototype.js library

i’ve long been skeptical of javascript libraries. web 1.0’s myriads of “dhtml” / cross browser libraries didn’t improve the javascript programming experience much. at least there was no library i found so useful that i’d use it in all my projects. so all i had was a couple of snippets picked up here and there, like addEvent() / removeEvent() by the good ole Scott Andrew.

web 2.0 brought a lot of new buzzwords, and more frontend fun. and frontend fun requires: new javascript libraries! fortunately, these libraries seem to be different:

  • they are professionally written, complete with unit-tests
  • their developers don’t have to waste their time coding for horribly bad, pre-DOM browsers (Netscape 4, IE/mac anyone?).
  • they are better integrated in the core of the language
  • their developers have learned from their and other’s experience
  • there are a handful of popular libraries that have been widely adopted, which are continuously developed and improved, producing high quality, reliable code.
  • they focus on developer productivity, instead of eye-candy and supplying drop-in high-level components
  • computers have become faster, allowing to write code that makes developers more productive, although it requires more cpu

ENTER PROTOTYPE.JS

as an experiment, i wanted to refactor a web page i created in 2002, thomasgraggaber.com, to use a current javascript library. since i’ve already used prototype.js, the library is well-established, well-documented, robust (and bundled with ruby on rails), i decided to use it for the experiment.

so here are the stats for the “application” javascript, i.e. everything that’s specific for the site:

  • original lines of code: 571 (65 of them in cross-browser helper methods)
  • lines of code after refactoring to use prototype.js: 445. that’s 22% less code.
  • the cross-browser helper methods were “library code” as well. ignoring those it’s still 10% less code.

of course you add 3271 lines of code for prototype.js (in v. 1.5.1). what’s important however: you usually don’t have to maintain, debug and completely understand the internals of prototype.js when creating your application.

the major code changes are:

Iterators, $$ and Event.observe

before:

var allDivs = document.getElementsByTagName('DIV');
for (i=0; i < allDivs.length; i++) {
    // ...
    else if(allDivs[i].className == 'sidebar')
    {
        addEvent(allDivs[i],"mouseup",dragManager);
        addEvent(allDivs[i],"mousedown",dragManager);
    }
    // ...
}

after:

$$('.sidebar').each(function(div) {
    Event.observe(div, 'mouseup', dragManager);
    Event.observe(div, 'mousedown', dragManager);
});
  • $$() is the swiss army knife for selecting elements in a document. give it a css selector and it will return all matching items in a collection. it probably isn’t the most efficient thing to use, but fine here, and is short. prototype’s document.getElementsByClassName() could also have been used.
  • the temp variable allDivs can be removed, since the iterator function #each works with the result of $$’s return value directly.
  • Event.observe is prototype’s way of registering an event handler cross-browser.

another example. nothing new here, but it takes even more advantage of the iterators:

// photosThumbs viewer
var photosThumbsNodeList = contentDivs['photosThumbs'].getElementsByTagName('IMG');
for (i=0; i < photosThumbsNodeList.length; i++)
{
    addEvent(photosThumbsNodeList.item(i),"click",photoManager);
    addEvent(photosThumbsNodeList.item(i),"mouseover",photoManager);
    addEvent(photosThumbsNodeList.item(i),"mouseout",photoManager);
}
var newsThumbsNodeList = contentDivs['news'].getElementsByTagName('IMG');
for (i=0; i < newsThumbsNodeList.length; i++)
{
    addEvent(newsThumbsNodeList.item(i),"click",photoManager);
    addEvent(newsThumbsNodeList.item(i),"mouseover",photoManager);
    addEvent(newsThumbsNodeList.item(i),"mouseout",photoManager);
}
var portraitThumbsNodeList = contentDivs['portrait'].getElementsByTagName('IMG');
for (i=0; i < portraitThumbsNodeList.length; i++)
{
    addEvent(portraitThumbsNodeList.item(i),"click",photoManager);
    addEvent(portraitThumbsNodeList.item(i),"mouseover",photoManager);
    addEvent(portraitThumbsNodeList.item(i),"mouseout",photoManager);
}

becomes

// photosThumbs viewer
$w('photosThumbs news portrait').each(function(divName) {
    $$("div#" + divName + " img").each(function(img) {
        $w('click mouseover mouseout').each(function(eventName) {
            Event.observe(img, eventName, photoManager);
        });
    });
});
  • $$() is used again. it could be replaced by “$A($(‘border’).getElementsByTagName(‘div’))” if it gets too slow.

the $w function

before:

// add event handlers
var naviDivs = document.getElementById('border').getElementsByTagName('DIV');
for (i=0; i < naviDivs.length; i++)
{
    addEvent(naviDivs.item(i),"click",ggNaviManager);
    addEvent(naviDivs.item(i),"dblclick",ggNaviManager);
    addEvent(naviDivs.item(i),"mouseover",ggNaviManager);
    addEvent(naviDivs.item(i),"mouseout",ggNaviManager);
    addEvent(naviDivs.item(i),"mousedown",ggNaviManager);
    addEvent(naviDivs.item(i),"mouseup",ggNaviManager);
}

after

// add event handlers
$$('div#border div').each(function(naviDiv) {
    $w('click dblclick mouseover mouseout mousedown mouseup').each(function(eventName) {
        Event.observe(naviDiv, eventName, ggNaviManager);
    });
});
  • $w is a nice adoption from ruby (where it’s %w). it splits the string argument by whitespace, returning an array of the items (“words”). very readable in this case.

Event.element(e), and returning only element nodes

e = (e) ? e : ((window.event) ? window.event : '');
eTarget = e.srcElement ? e.srcElement : e.target;   
while(eTarget.nodeName != 'DIV') {
    eTarget = eTarget.parentNode;
}

becomes

eTarget = Event.element(e);

nice.

  • prototype handles finding the event target in a cross-browser way. (note: prototype 1.6.0 will make this even simpler. the event handler will be bound to the event target, so the event target will be available in the “this” variable.)
  • prototype only returns element nodes, so the while() loop looking upwards for a div element becomes obsolete. it was required since it’s possible that the text node inside the element was clicked. this behavior allowed removal of a couple of these loops.

“DOM walking” with up(), down()

scrolledDiv = first_child(node_after(eTarget.parentNode.parentNode));

becomes:

scrolledDiv = eTarget.up('.box').down('.content')
  • firstchild() and nodeafter() were some of the utility methods to walk through the DOM, and already ignoring whitespace and text nodes. element.up(…).down(…) do the same, but are nicer, shorter, clearer to use.

show() and hide()

before:

function displayNavi()
{
    document.getElementById('sponsorFlash').style.display = 'none';
    document.getElementById('border').style.display = 'block';  
}

after:

function displayNavi()
{
    $('sponsorFlash').hide();
    $('border').show();
}
  • the code becomes more concise, and more expressive.
  • i don’t have to remember which elements require which display style in order to be displayed correctly (block/table/inline elements have different values).

conclusions

  • by using prototype.js i was able to remove 10% of the code
  • at the same time, the code is more readable than before
  • as an additional benefit, the site now probablyworks in safari (and all browsers that prototype supports)!

prototype.js rocks, because it’s developed by smart people, and because javascript itself rocks!

Posted in ,  | Tags , , , , ,  | no comments