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 colours
var 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 trigger
function 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 headline
function 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 trigger
function 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 headline
function 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 presentation

Becoming even classier

One thing that is amazingly enough still not too commonly known:

Elements can have more than one class. Something like <h2 class="normal enhanced"> 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 trigger
function 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 headline
function 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.

Comments

Function-itis?

Nice article. My only problem with it is that the functions seem to have very generic names, and there seems to be an excessive amount of them, which could easily lead to collisions with other scripts.

Generic Names

You are right, however this is just an example how to apply the classes idea and use juggleClass and checkClass, which are not that common names :-)

..or class prefixes

Great article.

I'm so sick of seeing script's that have 500 or more variables to customize just to get things to look the way you want. That never made sense to me. The past 20 or so scripts I've written have had nothing included to modify style. I just use class name prefixes though, like "dSS_" (drop search show) & "dSH_" (drop search hide). That way there's no possible chance that they'll already be using one of the scripts class names and either have to change their CSS or change the script. They could name it "dSS_foo" or "dSS_bar", either way the script will find it.

I'm also starting to enclose all my scripts in their own objects to avoid namespace pollution...

Anyway, nice article. Someone needed to write it.

haidary

good idea, haven't thought of prefixes yet. OO Javascript is a bit over the top IMHO though, especially when you have a script that other, more junior developers might have to alter.

H2 Classes

If you don't want to apply the behaviour to all h2s then you can change the init function to:

	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++)
		{
	// check for class 
		if (h2s[i].className=="showhide") {
	// 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
				juggleClass(tohide,'shHidden',1);
				juggleClass(h2s[i],'shNormal',1);
				h2s[i].onclick=function(){collapse(this,this.tohideobj);return false}
			}
		}
    }

Then, all you have to do is ensure that the h2s that you want to use as showhide titles have class="showhide".

checking for classes

right, Martin, however, it is better to check if the className _contains_ the class rather than being the class, as the markup might have multiple classes already. You can use the checkClass function for that :-)

Another way

No one has thought of using this?

//All h2s with class="o"
var h2s = cssQuery("h2.o");

but what about

This does pose the question "when is enough enough". I have seen some amazing things done with java and java script but the cost is page size. this is a large amount of code just for one affect. this code by itself would not cause a page to be too large, but a couple of these scripts and the page becomes too large for people on 28.8 (yes they are still out there, many rural residents are on a "carrier line" making 22.8 thier max speed). Many fantastic pages are too large for anyone but broadband users. I do not say this just to complain but to suggest that coding should be written to maximize effect while keeping page size in mind.

Dante and Drax

Dante, apparently Dean Edwards has, eh? Many roads lead to rome. :-)

Furthermore, this is totally beside the point of this article. The H2 example was just taken to show how to get to a clean separation of behaviour and presentation by clearing out an originally inline Javascript. cssQuery is fun, but I consider it much too powerful for a single script. In a DOM web application, it can save a lot of typing and headache. Please consider the topic of an article before just adding a comment which is nice but off-topic.

Drax, JS files when being linked to, are cached and have to be loaded only once, so no harm done even to people with slow lines. That is one of the beauties of javascript. If you put all your code inline, you haven't grasped it.

Enhancing the checkClass function

function checkClass(o,c)
{
	var re=new RegExp('\\b'+c+'\\b');
	return re.test(o.className);
}

Will also differentiate between classes "foo" and "foobar"

should I expect this to work in Firefox

The second example (http://www.icant.co.uk/articleassets/h2test2.html) works fine, but the third one (http://www.icant.co.uk/articleassets/h2test3.html) not so much.

I'm stretching to learn some stuff that's new for me with this article, and am not really able yet to figure out for myself why #2 works but not #3. Is there just something about Firefox such that I shouldn't expect it to work?

elishat

works fine here in firefox. What does not work and which version of firefox?

Couple of comments:

Example #2 works great. Example #3 works, but after clicking a headline to expand it and then clicking the headline to collaspe it -> Now the headline loses it hover state. Can you have 2 links in your page that say "expand all headlines" and "collaspe all headlines?" How would you get this to work?

gman and others

I found the bug that caused Firefox not to work properly, and added the function to hide and show all of the elements on the demopage.

The error was that juggleClass did not check if the class already existed, and added it twice.

	function juggleClass(o,c,s)
	{
		if(s==0) 
		{
			o.className=o.className.replace(' '+c,'');	
		}
		if (s==1 && !checkClass(o,c))
		{
			o.className+=' '+c
		}
	}

Demo Page?

What happend to the demo page with the hide all and show all function, and the corrected to code to work in Firefox?