Separation of CSS and Javascript

You might have noticed in the previous examples that we are not exactly practising what we preach.

Our mistake

True, we separated structure from behaviour, but we also defined presentation via Javascript, which is just as bad from a semantic point of view.

We defined presentation by accessing the style property of elements:

 if(!document.getElementById('errormsg')){
  var em=document.createElement('p');
  em.id='errormsg';
  em.style.border='2px solid #c00';
  em.style.padding='5px';
  em.style.width='20em';
  em.appendChild(document.createTextNode('Please enter 
  or change the fields marked with a '))
  i=document.createElement('img');
  i.src='img/alert.gif';
  i.alt='Error';
  i.title='This field has an error!';
  em.appendChild(i);
}

This means that whenever we want to change the look of our enhancement, we'd have to change the Javascript, and that is neither practical nor clever.

There are several ways to apply presentation defined in the CSS to an element via Javascript. One would be to apply an ID to the element by changing its ID attribute. A much more versatile way is to apply a class to the element, especially as elements can have more than one class.

Multiple class syntax

For an element to have more than one class, we simply separate the class names with a space: <div class="special highlight kids">. This is supported by most modern browsers, some of which show some bugs, though. IE on a Mac does not like several classes when one class contains the name of the other, and behaves badly when the class attribute starts or ends with a space.

Applying classes via Javascript

To add a class to a certain element, we change its className attribute. So if we want to change a navigation only when Javascript and DOM is available, we can do this:

HTML:
<ul id="nav">
  [...]
</ul>
Javascript:
if(document.getElementById && document.createTextNode)
{
  if(document.getElementById('nav'))
  {
    document.getElementById('nav').className='isDOM';
  }
}

This allows us to define two different states in our CSS:

ul#nav{
  [...]
}
ul#nav.isDOM{
  [...]
}

This would overwrite any class applied to the element though. That is why we need to check if there is already a class applied and add the new one preceeded by a space if that is the case:

if(document.getElementById && document.createTextNode)
{
  var n=document.getElementById('nav');
  if(n)
  {
    n.className+=n.className?' isDOM':'isDOM';
  }
}

The same applies when we want to remove classes that have been added dynamically, as some browsers don't allow for something like class="foo bar ". This can be pretty annoying, and it is easier to re-use a function that does this for us (<- denotes a linewrap):

function jscss(a,o,c1,c2)
{
  switch (a){
    case 'swap':
      o.className=!jscss('check',o,c1)?o.className.replace(c2,c1): <-
      o.className.replace(c1,c2);
    break;
    case 'add':
      if(!jscss('check',o,c1)){o.className+=o.className?' '+c1:c1;}
    break;
    case 'remove':
      var rep=o.className.match(' '+c1)?' '+c1:c1;
      o.className=o.className.replace(rep,'');
    break;
    case 'check':
      return new RegExp('\\b'+c1+'\\b').test(o.className)
    break;
  }
}

This example function takes four parameters:

a
defines the action you want the function to perform.
o
the object in question.
c1
the name of the first class
c2
the name of the second class

Possible actions are:

swap
replaces class c1 with class c2 in object o.
add
adds class c1 to the object o.
remove
removes class c1 from the object o.
check
test if class c1 is already applied to object o and returns true or false.

Let's have an example how to use it. We want all headlines of the second level expand and collapse their next sibling element. A class should be applied to the headline to indicate that it is a dynamic element and to the next sibling to hide it. Once the headline gets activated, it should get another style and the sibling element should get shown. To make things more interesting, we also want a rollover effect on the headline.

CSS:
.hidden{
  display:none;
}
.shown{
  display:block;
}
.trigger{
  background:#ccf;
}
.open{
  background:#66f;
}
.hover{
  background:#99c;
}
JS:
function collapse()
{
// check if DOM is available, return if not
  if(!document.createTextNode){return;}

// create new paragraph explaining that the headlines
// are clickable
  var p=document.createElement('p');
  p.appendChild(document.createTextNode('Click on the headlines to 
  collapse and expand the section'));

// loop over all headlines of the second level
  var heads=document.getElementsByTagName('h2');
  for(var i=0;i<heads.length;i++)
  {
// grab the next sibling (the loop is needed because
// of whitespace issues
      var tohide=heads[i].nextSibling;
      while(tohide.nodeType!=1)
      {
        tohide=tohide.nextSibling;
      }
// hide the sibling by applying the class 'hidden' and 
// show that the headline is clickable by applying 
// the class 'trigger'
      cssjs('add',tohide,'hidden')
      cssjs('add',heads[i],'trigger')
// store the element to be hidden in an attribute
      heads[i].tohide=tohide;
// add the class 'hover' when the mouse touches the headline
      heads[i].onmouseover=function()
      {
        cssjs('add',this,'hover');
      }
// remove the class 'hover' when the mouse leaves the headline
      heads[i].onmouseout=function()
      {
        cssjs('remove',this,'hover');
      }
// if the user activates the headline
      heads[i].onclick=function()
      {
// test if the class 'hidden' is already applied to the 
// next sibling
        if(cssjs('check',this.tohide,'hidden'))
        {
// if that is the case, replace it with shown and 
// the headline class with open
          cssjs('swap',this,'trigger','open');      
          cssjs('swap',this.tohide,'hidden','shown');      
        } else {
// and vice versa
          cssjs('swap',this,'open','trigger');      
          cssjs('swap',this.tohide,'shown','hidden');      
        }
      }
// insert the new paragraph before the first h2.
  document.body.insertBefore(p,document.getElementsByTagName('h2')[0]);
  }
  function cssjs(a,o,c1,c2)
  {
    [...]
  }
}
window.onload=collapse;
See it in action here.

That way we successfully separated structure, presentation and behaviour and created a rather complex effect. Maintenance of this effect is easy, and does not require any Javascript knowledge.

Save css.js for your own use by saving the following link's target to your computer. css.js

Creative Commons License Unless otherwise expressly stated, all original material of whatever nature created by Christian Heilmann and included in this web site and any related pages is licensed under the Creative Commons License.