Jan 8, 2008

Extend the elements

  1. Introduction
  2. How to extend the elements : available possibilities
    1. Prototype extension
    2. Extension when selecting
    3. Create a wrapper class
  3. Conclusion

It can sometimes be hard to make cross platform applications because of the different implementations. Attaching an event on a object can become hell if you don't use an abstraction layer. First of all, let's see the normal cross-browser approach :

function myEventFunc ( aeEvent )
{
  // just get the event if we are using IE
  var _eEvent = aeEvent || window.event ;
 
  // now do what we want to do
  /* ... */
}

/* now attach this function on the elements */
if( document.getElementById )
{
 var _eElt = document.getElementById ('myElement');
 if( _eElt.addEventListener ) // W3C (Gecko, webkit, Opera)
 {
   _eElt.addEventListener( "myEventName", myEventFunc, false );
 }
 else if( _eElt.attachEvent ) // MSIE
 {
    _eElt.attachEvent ( "myEventName", myEventFunc, false );
 }
 else
 {
   throw new Error('Your browser is very old ! please upgrade.');
 }
}
else
{
 throw new Error('Your browser is very old ! please upgrade.');
}

Well, let's see what solutions we have to avoid such a big amount of code.

Table of content

Every library used a way to get rid of such compatibility problems, and are using a single interface to do stuff.

First thing that comes to mind to add new behavior to an existing class is the prototype extension. Even if it can pose problems sometimes (such as for arrays), it can be a nice way to achieve our goal. The HTML elements class is supposed to be HTMLElement, so extending the HTMLElement.prototype should do the trick.

If quite every browser allows this extension, Internet Explorer doesn't like to make web developer's life easy, so this can do the stuff in quite every case, except for the major browser.

You can find more informations and workaround at following locations :

Table of content

As the prototype extension isn't supported natively for every browser, let's see another way. We still need to extend elements, so when will be the perfect time ? When using them of course. Which process is always used before using an HTMLElement ? selection !

To extend elements we'll need a namespace with all our functions and a selection function. Let's have a piece of code to illustrate :

/**
* the application namespace
*/
var App = {};
/**
* the element functions namespace
*/
App.HTMLElement = {
   addEvent: function(aElement, asEvent, acCallback, abBubbles){
     var _bBubbles = abBubbles || false;
     if(aElement.addEventListener ) // W3C (Gecko, webkit, Opera)
     {
       aElement.addEventListener( asEvent, acCallback, _bBubbles );
     }
     else if( aElement.attachEvent ) // MSIE
     {
        aElement.attachEvent ( 'on'+asEvent, acCallback, _bBubbles );
     }
     else
     {
       throw new Error('Your browser is very old ! please upgrade.');
     }
   }
};
/**
* our simple selector
* @param {String} asElementId the id of the element
* @return {HTMLElement}
*/
App.get = function(asElementId){
   var _eElement = document.getElementById(asElementId);
   if( ! _eElement.extended )
   {
     for(var _i in App.HTMLElement)
     {
       _eElement[_i] = (function(aMethod,eElement){
          var _method = aMethod;
          var _elt = eElement;
          return function(){
             /* transforms the arguments into an array */
             var _args = [_elt];
             for(var _j in arguments)
             {
                _args.push( arguments[ _j ] );
             }
             /* forces the context */
             return _method.apply( _elt, _args );
          }
       })(App.HTMLElement[_i], _eElement);
     }
      _eElement.extended = true;
   }
   return _eElement;
};

Well, this piece of code should work, let's try it :

Pass the mouse over me.

Here is the code I used to set this behavior up

 
/**
 * load event to initialize the div
 */
function extends_elts_test1_init(){
  App.get('extend_elts_test_1').addEvent('mouseout', extends_elts_test1_out).addEvent('mouseover',extends_elts_test1_over);
}
/**
 * function attached to the mouseout event of the element
 */
function extends_elts_test1_out(){
  App.get('extend_elts_test_1').style.backgroundColor = "#ff0";
}
/**
 * function attached to the mouseover event of the element
 */
function extends_elts_test1_over(){
  App.get('extend_elts_test_1').style.backgroundColor = "#0ff";
}

/* now register the load event */
if(window.addEventListener)
{
  window.addEventListener('load',extends_elts_test1_init,false);
}
else if(window.attachEvent)
{
  window.attachEvent('onload',extends_elts_test1_init,false);
}

To conclude on the elements extension, let's say that's prototype's way of doing. You can improve this code by mixing it with the HTMLElement.prototype extension. Just call the namespace HTMLElement, declare it as a new object and extend the prototype if it isn't natively done.

Well, this method is nice, but making a closure for every function every time an object is extended seems to be a bit a heavy way isn't it ? let's see what else can be done.

Table of content

Previous approach was based on selection, let's keep it, but this time we'll build a complete wrapper around it, a wrapper that references our element and implements our new methods.

/**
 * our namespace, as usual
 */
var App = {};
/**
 * the elements methods
 */
App.Element = function( anElement ){
  this._element = anElement ;
};
App.Element.prototype = {
  /**
   * the element instance
   * @var {HTMLElement}
   */
  _element : null,
  /**
   * cross-platform event observer
   * @param {String}   asEvent    the event name
   * @param {Function} acCallback the callback
   * @param {Boolean}  abBubbles  the bubbling flag [optionnal]
   *
   * @return {App2.Element}
   */
  addEvent: function( asEvent, acCallback, abBubbles){
     var _bBubbles = abBubbles || false;
     if(this._element.addEventListener ) // W3C (Gecko, webkit, Opera)
     {
       this._element.addEventListener( asEvent, acCallback, _bBubbles );
     }
     else if( this._element.attachEvent ) // MSIE
     {
        this._element.attachEvent ( 'on'+asEvent, acCallback, _bBubbles );
     }
     else
     {
       throw new Error('Your browser is very old ! please upgrade.');
     }
     return this;
  },
  /** 
   * set a style property. needed for the demo.
   * @param {String} asProperty the property name
   * @param {String} asValue    the new property value
   *
   * @return {App2.Element}
   */
  setStyle:function( asProperty, asValue ){
    this._element.style[asProperty] = asValue ;
    return this;
  }
};
/**
 * once again the selection function
 * @param {String} asElementId the element to select id
 * @return {App.Element}
 */
App.get = function( asElementId ){
  var _Element = document.getElementById( asElementId );
  return new App.Element( _Element );
}

Want a try ? let's go using quite the same code as earlier, except for the style property setting, I used the newly created wrapper.

Pass the mouse over me.

This way is cleaner, but needs more code to be done, because you have to implement every method, none are given by default !
This approach is the one chose by Ext framework, and seems to be the fastest at execution time ( I'm still impressed with the result they achieved with this framework ).

Table of content

Any method you choose, first improvement will be to add a method for registering new "native" methods. Whatever, except if you are embarrassed with some library functionalities (such as prototype Array extension), or if application is so specific that no library fit your needs, it is always better to rely on existing maintained code.

Table of content

No comments: