Unobtrusive Javascript Tutorial - ΩJr. Software Articles and Products

This information lives on a web page hosted at the following web address: 'https://omegajunior.globat.com/code/'.

There are scripts we wish to use only in certain browsers... mostly because other browsers don't need them. And when we do need those scripts, they shouldn't obtrude on the other browsers (nor should they annoy the visitors).

The methods used to supply scripts this way are called "unobtrusive". Let's find out how that works.

A.E.Veltstra
Tuesday, May 29, 2007

There are scripts we wish to use only in certain browsers... mostly because other browsers don't need them. And when we do need those scripts, they shouldn't obtruse on the other browsers. The methods used to supply scripts this way are called "unobtrusive".

Why?

Because we do want to separate behaviour from mark-up and we don't want browsers yelling at us about stuff they don't need anyway.

Like what?



How?

First we remove any and all in-line javascript handlers from the (x)html source. If the other browsers aren't going to use it, they shouldn't see it.

Then we specify that only the addressed browser is going to read the script. Since in most cases MSIE is the only odd one out, MS has been helpful enough to provide Conditional Comments, that are used by MSIE only. They work like this:
<!--[ if lte IE 7 ]
<script type="text/javascript" src="MSIEScript.js"></script>
[ endif ]-->

(Note that the actual conditional comments leave out the spaces immediately after the opening bracket and before the closig bracket; it was necessary to add a space due to the text parser is website uses.)

Of course this works with in-page script as well:
<!--[ if lte IE 7 ]
<script type="text/javascript">
/* <![ CDATA[ */
alert('MSIE-only XHTML-javascript');
/* ]]> */
</script>
[ endif ]-->


or, for good-ole html:
<!--[ if lte IE 7 ]
<script type="text/javascript">
alert('MSIE-only HTML-javascript');
</script>
[ endif ]-->


Thirdly, we reattach the events we removed in the first step. All modern DOM2 browsers use the W3's addEventListener for that, except MSIE, whose programmers in all their wisdom decided to create their own implementation known as attachEvent. And no, they didn't add addEventListener as a courtesy method either.

So we figure out which method is used by the browser. A nice way for doing this was described by someone else on the net: JavaScript Tip #1: Speed Up Object Detection. I'll just take it one step further:
<script type="text/javascript">
var addEvent;
if (document.addEventListener) {
 addEvent = function(element, type, handler) {
  element.addEventListener(type, handler, false);
 };
} else if (document.attachEvent) {
 addEvent = function(element, type, handler) {
  element.attachEvent("on" + type, handler);
 };
} else {
 addEvent = function(element, type, handler) {
  element["on" + type] = handler;
 };
}</script>

(Hint: we have supplied this ready for use in our cross-browser zjrJS library: zjrJS.Doc.aE().)

Will this work? Not all the time. There's a problem with parameters: the above-mentioned methods don't allow the immediate passing-through of parameters. So we have to find a way to read them from the window.event object. Especially when we're hovering or clicking and we want to know which element was hovered or clicked. Unfortunately, MSIE doesn't follow the DOM2 recommendation, so once more we need an exception.
<script type="text/javascript">
function readEventTarget(e){
 if(!e){
  if((window)&&(window.event)){
   e=window.event;
  }else{
   return false;
  }
 }
 if(e.target){return e.target;}
 if(e.srcElement){return e.srcElement;}
 return false;
}</script>

(Hint: we have supplied this ready for use in our cross-browser zjrJS library: zjrJS.Doc.eS().)

Will this work? Again, not all the time. In MSIE and Firefox, in case of an LI-element with only block-level elements inside it, the hover and click will only reach the block-level elements and not the LI-element. In Opera however, the interaction will reach the LI-element. So once more we need to open our bag-o-tricks:

Instead of adding static events and figuring out who called it, we can add "live" events. This is done by creating functions on the fly. Disadvantage: memory footprint.
<script type="text/javascript">
function init(){
  if((window)&&(document)&&(document.childNodes)){
    var c,d=document.childNodes;
    for(var i=0; i<d.length; i++){
      c=d[ i ];
      if((c)&&(c.tagName)&&(c.tagName=='UL')){
        c.onmouseover=new function(){
          window.status=('Number of items in list: '+this.length);
        };
      }
      c=null;
    }
  }
}</script>


(Of course we can combine this with a static event by calling it from inside the new function.)

See that 'this' thing in there? Why can't we simply ask for c.length, since it's supposedly the same object? Because that 'c' is a variable that holds a value only inside that loop. Once the new function gets called, the loop no longer exists, thus the 'c' variable no longer points to anything. Querying a length of nothing results in an error.

Instead we query the length of the 'this' object. At the moment of function execution, 'this' refers to the executioner, which happens to be the UL.

This by the way can lead to a whole bunch of confusion, especially if it isn't apparent which object is the executioner.

There's a lot more to say about unobtrusive javascript. The main idea is not to bog down a browser with functions it either can't use anyway, or supports natively.

Need problem solving?

Talk to me. Let's meet for coffee or over lunch. Mail me at “omegajunior at protonmail dot com”.