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.
2004's state-of-the-art Javascript should be unobtrusive and easily maintainable. In essence, this means:
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.
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:
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
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
..or class prefixes
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
H2 Classes
If you don't want to apply the behaviour to all h2s then you can change the
initfunction 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
Another way
No one has thought of using this?
//All h2s with class="o" var h2s = cssQuery("h2.o");but what about
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
Couple of comments:
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?