Extending HTML elements with custom methods using prototyping have long been supported by FireFox. Although it’s not a particularly common feature to use it can be very useful in a variety of situations.
Say we want to provide a single method for expanding and/or collapsing items in a list. In IE we can do something like
var collection = document.getElementsByTagName(‘ul’);
for(var i=0; i<collection.length; i++)
collection[i].expandCollapse = someFunction;
In Firefox however you can achieve the same effect simply by extending the HTMLUListElement object as in
HTMLUlistElement.prototype.expandCollapse = someFunction;
In this instance all UL objects will have the same event handler. This is true for all UL objects created prior to the declaration and any subsequent created UL objects. If we change the function at runtime it changes for all UL objects, not just the ones created after the change. The first part is really pretty simple to mimic while the second is much harder.
Part 1
We want to have a mechanism for extending an element with custom methods. The mechanism should be transparent to the programmer as to not require any special syntax or semantics. In short it should be unobtrusive. The element inheritance hierarchy for documents is as follow
A specific element, say a DIV element inherits from the generic HTMLElement. This in turn inherits from the Element object which inherits from the Node object which finally inherits from the Object object.
We do not need to replicate the entire inheritance chain in order to achieve what we want. It might be useful for more complicated task to extend the inheritance but for now I’ll just work with the HTMLElement and HTMLDivElement objects. First we define a HTMLElement object
function HTMLElement() {
}
It’s not meant to be instantiated but we need a prototype object to inherit from. Second we define a specific object
function HTMLDivElement() {
}
which inherits from HTMLElement as
HTMLDivElement.prototype = new HTMLElement();
HTMLDivElement.prototype.constructor = HTMLDivElement;
Now we can extend any HTML element by defining functions in the HTMLElement.prototype object as
HTMLElement.prototype.forAll = function(){alert(‘This function is defined for all elements’);};
or we can extend solely a specific element as in
HTMLDivElement.prototype.forDiv = function(){alert(‘This function is only defined for DIV elements’);};
or we can define both.
While this just defines our inheritance chain it does not extend the elements in a document. To do this we must first redefine the native document.createElement method. We do this simply be saving a reference to the original one and defining our own
// Our namespace
var EPE = {}
// Save reference to native method
EPE.IECreateElement = document.createElement;
// Declare our own
EPE.createElement =
function(tag) {
var el = EPE.IECreateElement(tag);
EPE.extendElement(el);
return el;
};
// Must be assigned after the declaration of EPE.createElement
document.createElement = EPE.createElement;
Although we usually don’t have to care about when we define objects this approach will not work if the native function is replaced by a custom function before the custom function have been declared. I can’t find an explanation for this so if you know the answer please share the information
When the user executes the statement
var el = document.createElement(‘div’);
the EPE.createElement method is invoked. This will call the native createElement which actually creates the element and then call the function EPE.extendElement which looks like this.
EPE.extendElement =
function(el) {
var htmlConstructor = EPE.getConstructor(el.tagName);
// If a constructor could be found
if (htmlConstructor) {
for(var p in htmlConstructor.prototype)
el[p] = htmlConstructor.prototype[p];
}
};
The EPE.getConstructor method is merely a method for translating whatever tag is provided into a suitable constructor function. For instance if you want to create a TD element you have to create a HTMLTableCellElement. In our case we simply need to translate the Div tag and EPE.getConstructor returns a reference to the HTMLDivElement function.
Finally the for loop will copy any functions defined in the prototype chain of the HTMLDivElement to our newly created Div element.
In this way, by a transparent use of document.createElement, the user can extend any HTML element as much as he like. Creating a Div element will create an element with both the forAll and the forDiv methods while creating a Table element will provide the forAll method and any methods defined on the HTMLTableElement prototype object.
Any elements created prior to the loading of the document can be extended using the document.all collection like this
window.onload =
function() {
var l = document.all.length;
for (var i=0; i<l; i++)
EPE.extendElement(document.all[i]);
};
Getting the prototype functions to the DOM elements is thus pretty straight forward, but changing the prototype objects at runtime and reflecting those changes proves to be quite a challenge.
Part 2
We want to reflect any changes in the HTMLElement prototype objects to the instantiated objects at runtime.
For this we need a mechanism for monitoring changes in the prototype object or a way for the DOM node to look up the method in an associated prototype object on error.
I tried accomplishing this using various approaches but to no prevail.
IE does not have the very useful Mozilla watch/unwatch methods. The similar onpropertychange event only applies to DOM nodes and not custom created objects.
That leaves us with the setExpression method for dynamic properties for monitoring.
The setExpression method however is not designed for this purpose and from preliminary testing looks rather futile.
It might be possible using IE behaviors (i.e. htc files). If one of you intelligent readers can come up with a solution please let us know.
Summary
You can argue whether it is a good idea or not to change prototype objects at runtime. It seems a bad design decision yet it might prove very flexible in other situations.
Never the less it is still very useful to have a mechanism for extending the HTML elements in a document. The solution presented is easy, simple and completely unobtrusive.
Seneste Kommentarer