An Alternative DynWrite function

The Original DynWrite function.

The quick answer in section 4.15 of the FAQ defines a function called DynWrite that will insert HTML content into an HTML page by writing a string of HTML to the innerHTML property of an IDed element, but only on browsers that fully support the (Microsoft IE originated) innerHTML extension.

On browsers that do not provide a suitable mechanism for accessing the element, do not implement the innerHTML extension and browsers that implement innerHTML as a read-only property (along with Javascript incapable/disabled browsers) no content will be inserted with this function.

On browsers that lack the element retrieval mechanism the DynWrite function is created as a simple function that just returns false. That would allow code that called DynWrite to check the value returned from a call to DynWrite and, if it was false, know that there would be no point in continuing to use the function as content will never be inserted into the document.

Unfortunately the other branch, when an element retrieval mechanism is available, produces a function that will retrieve the element and write to its innerHTML property, returning true. On the browsers that do not implement, or fully implement, innerHTML this action should have no harmful side effects, merely creating a new innerHTML property on the element in browsers that do not implement the extension at all and leaving read-only innerHTML properties unchanged. But the true return value cannot be taken as an indicator of the success of the action.

Ideally the return value from DynWrite should be a reasonable indicator of the success of the attempt to insert contents into the HTML as that would facilitate controlled degradation on un-supporting browsers.

However, it is necessary to have a reference to an element in order to examine it to see if it does support innerHTML and the testing would alter the contents of that element (though possibly only temporarily). As a page loads elements usually become available (or, at least, referencable) after their opening HTML tag has been parsed, so code defined (or imported) in the HEAD section of a page could access the HTML element and the HEAD element along with, possibly, the TITLE and any other preceding elements.

It would be theoretically possible acquire a reference to one of these elements and determine whether it has an innerHTML property and maybe then attempt to write to it to see if that had the desired effect. In reality there is at least one innerHTML supporting browser that will allow the dynamic modification of the content of any element within the BODY but generates run-time errors if an attempt is made to test, read or modify the innerHTML property of elements within the HEAD. That rules out examining the elements within the HEAD for innerHTML support and means that when DynWrite is initially configured it cannot know whether it is going to be effective or not.

It would, in principle, be possible to write the DynWrite functions so that it tests the element that it is asked to insert HTML into prior to making the attempt so that it could act (or not) and then return true/false based on the result of those tests. But that would be inefficient as it would require that the test be carried out on every invocation of DynWrite. Iit may also be visually undesirable as the testing strategy (described below) involves setting the innerHTML to a test value resulting in two HTML modification per write operation (which may become visually apparent).

An alternative is to exploit the flexibility of Javascript by initially assigning a function to DynWrite that could carry out the required tests and then re-assign DynWrite to a function that returned a true/false value that more accurately reflected the browser's support for the innerHTML extension. Allowing the tests to be performed on an element that should be available at the time of the test and the content of which is intended that it be replaced so it will not matter if it is replaced with the test value because if it was successfully replaced for the test it would then be re-replaced with the HTML that was intended to be inserted. It also allows the test to only be performed once as success or failure on the first attempt should indicate the same outcome on subsequent attempts.

DynWrite is initially configured based on the browser's apparent ability to retrieve a reference to a DOM element given its ID in the form of a string. On modern browsers the document.getElementById function can be used to return a reference to an element given the ID as a string. Older browsers may not implement the getElementById function, but some may provide an alternative mechanism such as document.all.

Element retrieval given the element ID as a string.

Several approaches can be taken towards maximising the ability to retrieve references to DOM elements.

1. Writing a general element retrieval function such as:-

function getElementWithId(id){
    var obj = null;
    if(document.getElementById){
        /* Prefer the widely supported W3C DOM method, if
           available:-
        */
        obj = document.getElementById(id);
    }else if(document.all){
        /* Branch to use document.all on document.all only
           browsers. Requires that IDs are unique to the page
           and do not coincide with NAME attributes on other
           elements:-
        */
        obj = document.all[id];
    }
    /* If no appropriate element retrieval mechanism exists on
       this browser this function always returns null:-
    */
    return obj;
}

2. Assigning one of many functions tailored to element retrieval on the current browser to a global variable that can then be used to call the element retrieval function.

var getElementWithId;
if(document.getElementById){
    /* Prefer the widely supported W3C DOM method, if 
       available:-
    */
    getElementWithId = function(id){
        return document.getElementById(id);
    }
}else if(document.all){
    /* Branch to use document.all on document.all only
       browsers. Requires that IDs are unique to the page
       and do not coincide with NAME attributes on other
       elements:-
    */
    getElementWithId = function(id){
        return document.all[id];
    }
}else{
    /* No appropriate element retrieval mechanism exists on
       this browser. So assign this function as a safe dummy.
       Values returned form calls to getElementWithId probably
       should be tested to ensure that they are non-null prior
       to use anyway so this branch always returns null:-
    */
    getElementWithId = function(id){
        return null;
    }
}

3. Using a feature of a browser to emulate getElementById on a browser that does not implement it. So that getElementById can be used as a general element reference retrieval method.

/* Emulate getElementById on document.all only browsers. Requires
   that IDs are unique to the page and do not coincide with NAME
   attributes on other elements:-
*/
if((!document.getElementById) && document.all){
    document.getElementById = function(id){return document.all[id];};
}

The reason for the comment about the use of ID and NAME attributes is that the document.all collection does not have exactly analogous behaviour with the document.getElementById method. If multiple elements have the same ID (or share an ID with the NAME of other elements) then document.all returns a collection instead of an individual element, while document.getElementById only ever returns an individual element or null. However, ID attributes are supposed to be unique to an HTML page if that page is valid HTML 4 so multiple identical IDs should not be a problem. The issue with IDs coinciding with NAMEs remains, though it would not be good HTML design to provoke that problem.

Also, when a string used to refer to a property of the document.all collection does not refer to an element or a collection of elements undefined is returned by the above function. In most type-converting tests undefined and null behave the same (they both convert to boolean false) so the distinction is not necessarily important.

However, a more cautious but slower getElementById emulation could be used. Including tests that ensure that its behaviour exactly matched the W3C getElementById method.

/* Emulate getElementById on document.all only browsers. */
if((!document.getElementById) && document.all){
    document.getElementById = function(id){
        var tempEl = null, el = document.all[id];
        if(el){ //document.all returned something.
            if((!el.id)||(el.id != id)){
                /* Either this is a collection or the only element
                   available under the property name provided as the
                   - id - parameter is a named element:
                */
                if(el.length){ //assume it is a collection.
                    /* But it might be an element with a NAME
                       corresponding with the id parameter that has
                       collection-like behaviour such as a form or a
                       select element so proceed with caution:
                    */
                    for(var c = 0;c < el.length;c++){
                        if((el[c].id)&&(el[c].id == id)){
                            /* Set tempEl to the first match and
                               break out of the - for - loop:
                            */
                            tempEl = el[c];
                            break;
                        }
                    }
                    /* el will be set to null if the loop did not
                       find an element with the corresponding ID
                       because the default null value of tempEl
                       will not have changed:
                    */
                    el = tempEl;
                }else{ //only a named element is available for id.
                    /* getElementById should not return named elements
                       only an IDed element so set el to null:
                    */
                    el = null;
                }
            } //else we have our element (the ID matches).
        }else{ //el is undefined so make it null;
            el = null;
        }
        /* The returned value will be the first element confirmed as
           having the corresponding ID or it will be null:
        */
        return el;
    };
}

One of the consequences of creating a function to emulate getElementById is that other scripts that use the existence of document.getElementById to infer the existence of features of the browser beyond the getElementById method may wrongly infer that a browser that is provided with the emulation may have features that it does not have (unless they have also been emulated). This should not be a problem as using a test on one feature to infer the existence of other features is a fatally flawed technique, the use of which is discouraged.

I will be using the third approach, emulating the document.getElementById method, in the modified DynWrite function (with notes on what would differ with either of the first two).

innerHTML Testing Strategy.

Initially testing an element to see if innerHTML is supported simply involves using the typeof operator to see if it returns "string". That test will identify browsers such as Opera 6, where the innerHTML property is undefined.

The second test is based on the fact that when the innerHTML property of an element is read on a supporting browser the value returned is normalised HTML and not the literal HTML source code. However, all browsers seem to take a different attitude when generating that normalised source. So given the original HTML source:-
<td CLASS="obj"><U>firstChild</U></td>
the corresponding innerHTML of the parent element reads
<TD CLASS='obj'><U>firstChild</U></TD>
on Opera 7.11,
<TD class=obj><U>firstChild</U></TD>
on IE 5.0 and
<td class="obj"><u>firstChild</u></td>
on Mozilla 1.2.

None of these correspond with the original HTML but they are also different from each other. What is needed for the test is HTML source that all supporting browsers will return as different text from the source assigned. For that task a mixed case HTML string with additional unnecessary whitespace characters is used. That test HTML is appended to the original innerHTML value to ensure that the test string will never be equivalent to either the original HTML or the normalised result of reading the innnerHTML property after the assignment, even by accident. The fact that the browser has normalised the source is taken as indicating that the browser supports the innerHTML extension.

After writing the test HTML to the innerHTML property it is possible to determine whether the property is read only by comparing a stored copy of the original value against the retrieved value. This is also necessary because otherwise the fact that the returned HTML does not correspond with the testHTML would be taken as indicating that it had been normalised.

Finally, it is necessary to determine whether inserting the test HTML has added the element that it defines to the DOM for the page.

When those tests are passed correctly the DynWrite function can be replaced with a new function that does not bother repeating the tests for subsequent assignments to innerHTML. If the tests are failed then the DynWrite function replaces itself with one that does no more than return false to indicate its failure.

Alternative DynWrite function.

The modified, and commented, alternative DynWrite function incorporating these tests is as follows:-

/* As written here, to support older browsers like IE 4, the emulation
   of getElementById MUST HAVE BEEN EXECUTED _PRIOR_ TO THE FIRST CALL
   TO THIS FUNCTION. If either of the alternative element retrieval
   methods are to be used the noted changes also need to be made to
   this function and the alternative method MUST have been set-up
   prior to the first use of this function.
*/
function DynWrite(id, S){ //Generates a successor when first called!
    /* Define local variables:-
    */
    var testH, newH, inH, testID;
    /* Set the default value for the body text of the function that
       will be created to replace this function as the DynWrite
       function:-
    */
    var funcBody = "return false;"
    /* This ensures that getElementById is available (or emulated)
       on this browser prior to calling it:-
    */
    var el = (document.getElementById)?document.getElementById(id):null;
    /* If one of the other element retrieval strategies was used,
       creating a getElementWithId function, then because that function
       will return null when an element cannot be found, it is
       practical to just call that function as -
       var el = getElementWithId(id);
       - as subsequent tests result in an appropriate response.
    */
    /* This ensures that the referenced element has been successfully
       retrieved and tests to verify that its innerHTML property is
       a string (as opposed to being undefined):-
    */
    if((el)&&(typeof el.innerHTML == 'string')){
        /* Arbitrary string to use as an ID for an element that
           should be created as a result of writing to the innerHTML
           value (The string itself and the result of concatenating it
           to itself (repeatedly) should follow the rules for valid
           HTML ID attributes):-
        */
        testID = "tSt";
        /* This ensures that the test ID is not in use on the page by
           modifying it until an element cannot be retrieved using it:-
        */
        while(document.getElementById(testID)){
            /* If the getElementWithId function is being used instead
               of the getElementById emulation then the preceding test
               must also use that function.
            */
            testID += testID;
        }
        inH = el.innerHTML;    //Read the original innerHTML value.
        /* The following mixed case HTML string is _not_ an error.
        */
        /* Note also that the STRONG element inserted in the page
           contains the text "test", which may momentarily be
           visible to the user. It would probably not be a good
           idea to have no contents in the STRONG at all but &nbsp;
           could be used to reduce the potential visual impact:-
        */
        newH = inH+"<sTrOnG  Id='"+testID+"'  >test<\/StRoNg    >";
        el.innerHTML = newH;     //Assumes synchronous update of DOM.
        testH = el.innerHTML;    //Read innerHTML back for examination.
        if((testH != newH)&&     //Apparently normalised or unchanged.
           (testH != inH)&&      //Not unchanged (Not read-only).
           (document.getElementById(testID))){ //Element found in DOM.
            /* If the getElementWithId function is being used instead
               of the getElementById emulation then the preceding test
               must also use that function.
            */
            /* TESTS PASSED! Replace the default function body string
               with code that will set the innerHTML property of the
               element and return true, based on the assumption that
               the assignment will be successful because this test was
               sucessful:-
            */
            funcBody = 
                "document.getElementById(id).innerHTML=S; return true";
            /* See additional notes[1] on the function body to use at
               this point.
            */
        }
    }
    /* Replace the DynWrite function with one determined by the results
       of the tests:-
    */
    DynWrite = new Function("id", "S", funcBody);
    /* Call the newly created DynWrite function and return its return
       value as the return value for this function call:-
    */
    return DynWrite(id, S);
}
/* [1] Notes on the body string to use if the tests are passed:-
   The existing function body string relies on the use of
   the getElementById emulation approach to retrieving DOM element
   with an ID. If the getElementWithId function was used the
   appropriate equivalent body string would be:-

"getElementWithId(id).innerHTML = S;return true;"

   However, if the ID passed as a parameter to DynWrite does not
   correspond with an existing IDed element then either of these two
   options would result in a function that would generate run-time
   errors. For development that is probably a good thing as the
   resulting error should be corrected in either the HTML or the
   script code by ensuring that the ID string provided does refer to a
   uniquely identified DOM element.  On the other hand, having fully
   tested the code, the body string might be best swapped in deployed
   code to a more cautious version that checked the result of the
   element retrieval call to ensure that it is a non-null object:-

"var el=document.getElementById(id);if(el){el.innerHTML=S;}return true;"

   - with the getElementById emulation or:-

"var el=getElementWithId(id);if(el){el.innerHTML=S;}return true;"

   - with the getElementWithId function.
   
   These function body strings are still returning true. How
   suited that is to the situation would depend on how the code
   intended to respond to the return value. The inability to resolve
   one ID does not indicate that innerHTML would not available on
   others if they could be resolved so code that decides to stop
   attempting to write to innerHTML upon the first false return
   value might be better off using the previous strings. Code that
   wanted to fall-back based on each attempt to write to innerHTML
   would be better using a function body string that returned
   true/false based on the success of each individual attempt. For
   the body string that tests to ensure that the element reference
   is recovered a boolean return value based on the success of the
   element retrieval would be best suited. That is simplest achieved
   with a double NOT operator - return !!el; - . A null value of
   - el - would return boolean false and an element reference would
   return true due to type-converting forced by the NOT operator:-

"var el=document.getElementById(id);if(el){el.innerHTML=S;}return !!el;"

   - with the getElementById emulation or:-

"var el=getElementWithId(id);if(el){el.innerHTML=S;}return !!el;"

   - with the getElementWithId function. 

*/

This design of the function does not involve any configuration tests as the page loads. It is one simple function definition. In principle calling the function with appropriate parameters will always return false if the content inserting is not possible and true otherwise. With the tests being performed only on the first invocation of the function.

Given the HTML: <div ID="anID">old <code>HTML</code></div> examples of usage might be:-

if(!DynWrite("anID", "new <code>HTML<\/code>")){
    ... // Handle the failure of the call to DynWrite.
}

- or -

if(DynWrite("anID", "new <code>HTML<\/code>")){
    ... // Action following the success of the DynWrite call.
}else{
    ... // Handle the failure of the call to DynWrite.
}

It has been observed that IE 4 errors if DynWrite is called before the onload event is triggered by the browser. So to maximise cross-browser support for this function it would be better not to use it prior to that point.

comp.lang.javascript FAQ notes T.O.C.