Main Page Content
A Touch Of Class Skinable Javascript
Ok, let's repeat the trinity of web standards: Markup is the structure,
Style Sheets are the presentation and Javascript is the behaviour. That much should be common sense by now. New starters in web development have the chance to follow these rules easily - no more annoying IE4s and Netscape Communicators to worry about - modern browsers can do the CSS and xHTML dance quite well. The separation of markup and presentation is practised a lot, and loads of tutorials and templates help the developer to do so.With Javascript, it is still a bit dodgy, as too many good resources get lost
in the mass of bad or outdated ones.Look - new Javascript!
2004's state-of-the-art Javascript should be unobtrusive and easily maintainable.
In essence, this means:- Only apply itself when it is possible to do so.
- Use the markup as it is without the need to add inline event calls.
- Be independent of the input device (insofar this is possible).
- Play along well with other Javascripts (no common global variables, no event hijacking).
- Keep the visuals to the CSS, to avoid double maintenance.
The last item is what this article is about. Many a time,
you encounter scripts that are nice in themselves, but fail to completely separate the presentation from the behaviour. We cannot assume that every web developer knows Javascript - and its syntax. To avoid frustration using a script, we need to find a way to allow the developer to skin the outcome of this behaviour with CSS. This also makes maintenance easier, as you don't need to check in two places, if you have to change the visiual outcome - all it needs is a change of the CSS.We want clickable headlines - now!
Let's take a look at an example. Say we want to make every second level headline
in a document clickable. What it should do is collapse and expand an adjacent element.Headers as they are are not clickable elements, so we need to make the user
aware that ours are. We do this by adding a > and a hover state.In the world of non-Internet Explorer, the hover could be done via CSS, but
in the real world, we do it via Javascript to ensure that the majority of users will see the effect.When the header was clicked, we want to give it a selected state.
Now, if we kept the design in the Javascript, this could be done like this:
// Define coloursvar hovercolour='#ffc';var normalcolour='#fff';var highlight='#69c';function init(){ var h2s,i,tohide,tohideobj,isexpanded;// grab all second level headlines and loop over them h2s=document.getElementsByTagName('h2'); for (i=0;i<h2s.length;i++) {// get next sibling (the element to hide, check that it is an element tohide=h2s[i].nextSibling; while(tohide.nodeType!=1) { tohide=tohide.nextSibling; } h2s[i].tohideobj=tohide;// add the hover function onmouseover and onmouseout h2s[i].onmouseover=function(){hover(this,this.tohideobj,1);} h2s[i].onmouseout=function(){hover(this,this.tohideobj,0);}// hide next element and add onclick event to show and hide it tohide.style.display='none'; h2s[i].onclick=function(){collapse(this,this.tohideobj);return false} h2s[i].insertBefore(document.createTextNode('>'),h2s[i].firstChild); } }// hover function, adds the hover colour // unless the headline is an active triggerfunction hover(o,ho,state){ if(ho.style.display=='none') { o.style.background=state==1?hovercolour:normalcolour; }}// collapse function, shows and hides the element and sets the highlight // colour of the headlinefunction collapse(o,ho){ ho.style.display=ho.style.display=='none'?'block':'none'; o.style.background=highlight;}window.onload=init;
It does work, but has some disadvantages:
- We need to know javascript (at least how to set variables) to change the look and feel.
- We can only change backgrounds, if we also wanted to change the colour, we'd need to set more variables and extend the hover and the collapse functions.
Becoming classy
What to do? Easy - put all the presentation in style sheet classes, and
use the className attribute to change them.This also means we won't have to add the > any longer, as that can be
done in a background image..hover{ padding-left:30px; background:url(arrow.gif) 5px 5px no-repeat #ffc; color:#000;}.highlight{ padding-left:30px; background:url(arrowon.gif) 5px 5px no-repeat #69c; color:#fff;}.normal{ padding-left:30px; background:url(arrow.gif) 5px 5px no-repeat #fff; color:#000;}.hidden{ display:none;}
Then we change the functions accordingly:
function init(){[...]// hide next element and add onclick event to show and hide it tohide.className='hidden'; h2s[i].className='normal';[...]}// hover function, adds the hover colour // unless the headline is an active triggerfunction hover(o,ho,state){ if(ho.className=='hidden') { o.className=state==1?'hover':'normal'; }}// collapse function, shows and hides the element and sets the highlight // colour of the headlinefunction collapse(o,ho){ ho.className=ho.className=='hidden'?'':'hidden'; o.className='highlight';}
Works a treat, but still has one problem:
If I already have a class on the headline, the script will remove that one,
which means once again Javascript meddling with presentationBecoming even classier
One thing that is amazingly enough still not too commonly known:
Elements can have more than one class. Something like
is perfectly valid HTML and is supported by modern browsers.Bearing this in mind, we can add our classes to the existing ones or remove
them. For this, we can use two small functions.function juggleClass(o,c,s){ o.className=s==1?o.className+' '+c:o.className.replace(' '+c,''); }function checkClass(o,c){ var isClassInObj=o.className.indexOf(c)!=-1?true:false; return isClassInObj;}
juggleClass adds the class c to the object o when s is 1, otherwise it removes the class
c from the object o.checkClass checks if the class c is one of the classes of the object o.
If we add that feature, our final function looks like this:
function init(){ var h2s,i,tohide,tohideobj,isexpanded;// grab all second level headlines and loop over them h2s=document.getElementsByTagName('h2'); for (i=0;i<h2s.length;i++) {// get next sibling (the element to hide, check that it is an element tohide=h2s[i].nextSibling; while(tohide.nodeType!=1) { tohide=tohide.nextSibling; } h2s[i].tohideobj=tohide;// add the hover function onmouseover and onmouseout h2s[i].onmouseover=function(){hover(this,this.tohideobj,1);} h2s[i].onmouseout=function(){hover(this,this.tohideobj,0);}// hide next element and add onclick event to the header to show and hide it juggleClass(tohide,'hidden',1); juggleClass(h2s[i],'normal',1); h2s[i].onclick=function(){collapse(this,this.tohideobj);return false} }}// hover function, adds the hover colour // unless the headline is an active triggerfunction hover(o,ho,state){ if(checkClass(ho,'hidden')) { if(state==1) { juggleClass(o,'normal',0); juggleClass(o,'hover',1); } else { juggleClass(o,'normal',1); juggleClass(o,'hover',0); } }}// collapse function, shows and hides the element and sets the highlight // colour of the headlinefunction collapse(o,ho){ if(checkClass(ho,'hidden')) { juggleClass(ho,'hidden',0); juggleClass(o,'normal',0); juggleClass(o,'highlight',1); }else{ juggleClass(ho,'hidden',1); juggleClass(o,'highlight',0); juggleClass(o,'normal',1); }}function juggleClass(o,c,s){ o.className=s==1?o.className+' '+c:o.className.replace(' '+c,''); }function checkClass(o,c){ var isClassInObj=o.className.indexOf(c)!=-1?true:false; return isClassInObj;}window.onload=init;
Works the same, but now features a full separation of
behaviour and presentation. Storing the class names in global variables would also enable us to change them easily.If we want to be independent of other Javascripts, we cannot use window.onload
of course but should use a more clever function instead that adds our function to the overall window.onload.