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.
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).
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.
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 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.