When it comes to DHTML, Mozilla and the W3C DOM is lagging behind Internet Explorer 4 but when it comes to Javascript Mozilla is way ahead. This and a proprietary method for text ranges allows us to emulate the convenient innerHTML property for HTML elements in Mozilla.

Native JS Dom

In Mozilla all objects of the DOM are native JS objects. This means that objects like window and document are true JS objects and as such follow all the rules for objects in object oriented javascript. The most usefull thing one gets from this is the availability of the constructors for these objects. When we have these constructors we can easily extend the class making all objects of that class benefit from the extension.

Below is a simple example where we extend the constructor that is used for creating all HTML element with a new method.

HTMLElement.prototype.setBackgroundColor = function (sColor) {
   this.style.backgroundColor = sColor;
   return sColor;
}

The constructor for HTML elements is called HTMLElement (see W3C DOM documentation) and we extend it by manipulating the prototype property. We add a method with the name setBackgroundColor that takes one argument and then just changes the style accordingly. Notice that this points to the object that was instanciated from this constructor. In this case a HTML element.

Once we have added this method all elements have this new method. This is a very important (and superior) compared to expando methods where only one element is affected. Below is the code for the expando method for comparison.

var element = document.getElementById("myHTMLElement");
element.setBackgroundColor = function (sColor) {
   this.style.backgroundColor = sColor;
   return sColor;
}

Getters and Setters

In a JS object, a read or write does not allow side effects but let's take a clear example where such a side effect is needed. Take for example an image in a web page. By setting the src property the actual image is changed to show the uri provided by the src. In Internet Explorer there is no way to achieve this effect in a JS object. (It can be done in a COM object and IE5 has added an event called onpropertychange to all HTML elements.) In Netscape Navigator it can be achieved with the watch method but it is pretty limited.

In Mozilla (Javascript 1.5) getters and setters can be assigned to all js objects. Using getters and setters we redirect the read and write to a function instead. In this function we can do all the side effects we want. Below is a very simple example:

var o = {_name : "Start name",
         writes : 0,
         reads : 0,
         name getter : function () {this.reads++; return _name;},
         name setter : function (n) {this.writes++; return this._name = n;}
        }

This code creates an object, o, with a field called _name. This field is used for storing the actual name. There is also two counters used to keep track of accesses. Then we have the interesting parts, the getter and the setter. When the script tries to read o.name the getter function is called and the reads counter is incremented. Same applies for writing to o.name. The following code will alert "Reads: 2, Writes 1".

o.name = o.name + " changed";  // One read and one write
var s =o.name;                 // One read
alert("Reads: " + o.reads + ", Writes: " + o.writes);

Try this! Mozilla needed

Parsing HTML

In the W3C DOM there is no way to parse a HTML string and the Mozilla team held firm for a long time that only standards were going to be included but I guess they realized that the standards were not complete enough for any real world application. To parse a HTML string in Mozilla one uses the method createContextualFragment of the range object. This method returns a DocumentFragment (see W3C DOM). I've handled ranges a little in the Rich Editor Component article but IE's implementation does not follow the standard in too many cases.

To create a range with the W3C DOM use the method createRange of the document object. This range has a lot of methods for selecting nodes in different ways (but unfortunately no way to get the user selection). The methods we need for this is positioning the range at the right position (using selectNodeContents and setStartBefore) and removing the selection (this is done using deleteContents in setInnerHTML). Once we have positioned the range in the correct place we can create a new document fragment using createContextualFragment and then insert it in a normal way.

// setInnerHTML
   var r = this.ownerDocument.createRange();
   r.selectNodeContents(this);
   r.deleteContents();
   var df = r.createContextualFragment(str);
   this.appendChild(df);

// setOuterHTML
   var r = this.ownerDocument.createRange();
   r.setStartBefore(this);
   var df = r.createContextualFragment(str);
   this.parentNode.replaceChild(df, this);

Issues: Ranges need to operate on a document object and so does the creation of a new element (document.createRange() and document.createElement(sTagName)). The problem is that these two must operate on the same document but node.ownerDocument is null in Mozilla until the node has been inserted into a document. This is a bug in current build. This will lead to a problem where the node has not yet been inserted into the document.

Traversing the DOM tree

Unfortunately there is no direct way to get the HTML content of an element so we will have to do this manually by traversing the tree. This is (in my opinion) very simple but I'll give a short description. First create a function called getOuterHTML(node) that returns the outer HTML code. This is done by recreating the tag using element.tagName and looping over element.attributes. Then we loop over all childNodes and add their outer HTML and finally we add the end tag. In these functions we need to find what kind of node we are at (TextNode, ElementNode, EntityReference, CommentNode and maybe even a ProcessInstruction node). Take a look at the Node documentation in the W3C DOM level 1 core.

Show me how!

Combining everything

Now we have all the tools we nee to create an emulation of IE's proprietary property called innerHTML. This is going to be added to the HTMLElement constructor. To set the innerHTML we use the createContextualContent method, and to get the innerHTML we traverse the tree.

HTMLElement.prototype.innerHTML setter = function (str) {
   var r = this.ownerDocument.createRange();
   r.selectNodeContents(this);
   r.deleteContents();
   var df = r.createContextualFragment(str);
   this.appendChild(df);
   return str;
}

HTMLElement.prototype.outerHTML setter = function (str) {
   var r = this.ownerDocument.createRange();
   r.setStartBefore(this);
   var df = r.createContextualFragment(str);
   this.parentNode.replaceChild(df, this);
   return str;
}


HTMLElement.prototype.innerHTML getter = function () {
   return getInnerHTML(this);
}

HTMLElement.prototype.outerHTML getter = function () {
   return getOuterHTML(this)
}

To include this in a cross browser page you'll need to take special caution. IE does not recognize the syntax of the getter/setter so it is just not enough to do a browser test. There are two ways (at least) to make this work. The easiest is to do a browser test and if it is Mozilla do a document write to include the file.

if (/Mozilla\/5\.0/.test(navigator.userAgent))
   document.write('<script type="text/javascript" src="mozInnerHTML.js"></sc' + 'ript>');

The other way is to use the eval function and a browser test. Eval takes one argument and that is the string to be interpreted as a script. This prevents browsers that does not understand the getter/setter syntax to skip it entirely.

View the entire source
Test this Mozilla needed

Author: Erik Arvidsson