...

View Full Version : Using the DOM before window.onload (Part II)



brothercake
04-07-2005, 06:36 AM
There was a part one, but I can't find the thread now...

Anyway, brief precis - DOM scripts can't be called before window.onload, because you can't rely on the DOM being useable until it's closed (the parser has finished parsing the document). However the window.onload event is synchronous with images and external dependencies, so it doesn't fire until they've all resolved (or timed out). What we really want is an event that fires when the DOM ready, without reference to external dependencies. But there isn't one.

I'd been messing about with image onload events - trying to introduce some server latency in order to buy the time we need between the rendering of an object at the end of the </body> (the image with the onload handler) and the DOM being ready. It works, but it's a messy hack, and anyway <img onload> has been deprecated in XHTML.

So, then I thought ... what about a timer that checks for the document and then a DOM method, and if it exists we know the DOM is ready so we can safely call our function. Like this [where the eventual function we're calling is iotbs()]


//DOM-ready initialisation call object (timeout-counter, wait function)
var domReady = { 'time' : 0, 'wait' : window.setInterval(function()
{
//increment the timeout-counter
domReady.time ++;

//if we get to 100 (10 seconds)
if(domReady.time >= 100)
{
//timeout the request
clearInterval(domReady.wait);
}

//otherwise if the document and a DOM method exists
else if(typeof document != 'undefined' && typeof document.getElementsByTagName != 'undefined')
{
//call IOTBS initialisation function, if it exists
if(typeof iotbs == 'function') { iotbs(); }

//timeout the request
clearInterval(domReady.wait);
};

},100) };

Have I missed anything there? Is it right to assume, from the document and getElementsByTagName function existing, that the DOM is ready and all methods are available as normal, or is that a fallacy? Is there any other way it could fail?

codegoboom
04-07-2005, 09:50 AM
I don't know, but isn't the DOM available when a script is placed just prior to </body>? :)

Kor
04-07-2005, 09:57 AM
a silly question... is it 100 equal with 10 seconds? Aren't there 100 miliseconds ( 0.1 seconds)?

On the other hand... same quest as codegoboom: is'n DOM available anyway when prior to content?

brothercake
04-07-2005, 11:32 AM
a silly question... is it 100 equal with 10 seconds? Aren't there 100 miliseconds ( 0.1 seconds)?

Yes .. but my timer iterates every 100 milliseconds, and it counts up 1, so 100 iterations of 100 milliseconds = 10000 milliseconds = 10 seconds ;)

But now I think again, maybe 15 seconds would be better.



On the other hand... same quest as codegoboom: is'n DOM available anyway when prior to content?
No, you can't rely on methods of the DOM being available until the document has closed, which in the case of HTML means *after* the </html> tag has rendered, and the parser declares the document readyState to be "4". Shame it doesn't just report that, since all browsers have it (or something equivalent) in their internal event architecture ...

But anyway .. if you try manipulating the DOM before then it might fail; in most cases it actually won't fail, it will work fine - but that's just luck - interpretor latency and similar delays, which are not predictable in our favour.

Kor
04-07-2005, 11:46 AM
yeap. sounds logical...

Now, with no direct link with the thread, I wish to ask something else. I remember that you (or it was not you? - I am not able to find that thread again) showd us a tricky way to a real preloader of images. I have noticed that all the preloading code I have seen looks and acts on the presumtion that onload will fire the function when the whole page is loaded. I guess that most of the time the page loading time is not the same with full images loading time, so that those prelaoding codes are not relyable to me...

Is there a true method to verify if a SRC element has been really loaded into the cache?

Kor
04-07-2005, 11:51 AM
...and I remeber that I have recently noticed that thare is even a difference between browsers in loading sequencely the DOM

http://www.codingforums.com/showthread.php?t=55690

brothercake
04-12-2005, 08:55 PM
Okay .. I've had to restructure this construct quite a lot - the way it was before it was causing a crash bug in IE5.0 on Windows 2K and 98SE. It was the use of setInterval that was doing it - setTimeout was not a problem - so I've changed it to use that, but that of course means restarting the timer manually, so I need a reference to it, and hence a named (rather than anonymous) function. I've used the timeout syntax ("function()", n) rather than (function, n), because the latter doesn't work in Safari 1.0

I also increased the attempt time to 15 seconds, and slowed it down to 250ms iterations to be less intensive. And I've added another object test, for the body element, so that the scripting can go in the head section, and because without that there were occassional errors in IE and Konqueror from the body not existing (as I suppose there would be).

Here's the revised code. Anything else I haven't thought of that could make it untenable?



//DOM-ready watcher
function domReady()
{
//start or increment the counter
this.n = typeof this.n == 'undefined' ? 0 : this.n + 1;

//if the document, a DOM method, and the body all exist
if(typeof document != 'undefined' && typeof document.getElementsByTagName != 'undefined' && document.getElementsByTagName('body')[0] != null)
{
//>>>-------------------------<<<
//>>> DOM SCRIPTING GOES HERE <<<



//>>>-------------------------<<<
}

//otherwise if we haven't reached 60 (so timeout after 15 seconds)
else if(this.n < 60)
{
//restart the watcher
setTimeout('domReady()', 250);
}
};
//start the watcher
domReady();

In tests, this almost always runs after the first iteration, except: in IE6 roughly 1 time in 20 it takes 2 or 3 iterations; and in Konqueror it occassionally takes 7 or 8 iterations. So far it's never failed - it's never tried to run a DOM script and timed out before running, or run it and caused errors. In old browser that don't support DOM scripting it should fail cleanly - timeout after 15 seconds and do nothing; but I still have to test that in a range of legacy browsers, to make sure there's nothing evil going on.

brothercake
04-13-2005, 05:52 PM
Oh and another thing - if the scripting is going to modify any pre-existing HTML elements, the test condition also has to check for that:


//DOM-ready watcher
function domReady()
{
//start or increment the counter
this.n = typeof this.n == 'undefined' ? 0 : this.n + 1;

//if the document, a DOM method, and the body all exist
//>>> and check for any pre-existing elements the script needs: && obj != null
if
(
typeof document != 'undefined'
&& typeof document.getElementsByTagName != 'undefined'
&& document.getElementsByTagName('body')[0] != null
&& document.getElementById('something') != null
)
{//>>>-- DOM SCRIPTING GOES HERE --<<<



}//>>>-----------------------------<<<

//otherwise if we haven't reached 60 (so timeout after 15 seconds)
else if(this.n < 60)
{
//restart the watcher
setTimeout('domReady()', 250);
}
};
//start the watcher
domReady();

fci
04-16-2005, 05:01 AM
I do this:

function Loader() {
this.action = [];
this.add = function(obj) {
this.action.push(obj);
}
this.exec = function() {
var i;
for (i in this.action) {
this.action[i]();
}
}
}
var loader = new Loader();
window.onload = function() { loader.exec(); };


then when I need something to run when the page is fully loaded:

loader.add(function() {
/* you can do this multiple times. */
});

brothercake
04-17-2005, 05:06 AM
A neat technique :) But it still has the same core "problem" as all such wrappers - it doesn't fire until images and other dependencies have resolved or timed out, so if you have a less-than-reliable ad server, or your pages are just graphics heavy, you can be sitting there waiting for your scripts to start, when they could have started already ... if only they'd known ..

That's the issue I'm trying to address :thumbsup:


I've been doing a lot of tweaking and refinement on this idea, putting it into practise with different scripts, and chatting with jkd about it, and I've discovered some interesting things:

I'd been assuming that an element object doesn't exist until its close tag has rendered, but in fact that's not the case - an object exists as soon as its open tag exists, so a test inside an element can test for the element itself. This is fab - it means that the typeof tests for "document" and "getElementsByTagName" are unecessary, I can go straight to testing for the BODY. However I did have to keep the typeof test anyway, because that's what filters out non-DOM browsers.

Okay, so then I discovered that in older gecko builds (testing ns7.1), if the script is in the HEAD section, the reference "document.getElementsByTagName('body')" is always a collection with zero items, and item(0) is always "undefined" (not null, curiously). If the script is put in the BODY, it can retrieve the BODY using getElementsByTagName; but in the HEAD it can only retrieve the HEAD (even after 60 iterations, and the page has long since stopped processing anything else)

But even stranger, although this is the case, it's not the case for the reference "document.body" - that references comes back as and when expected. Bizarre. So, I had to supplement the test for getElementsByTagName('body') with this additional test for document.body, arranged so that if document.body doesn't exist at all (eg, in some browsers in XHTML mode) then it won't err or break, it will just use the first test; it will always use the first test, unless that fails; so the only gap there is old gecko builds in XHTML mode - but that's not really an issue, partly because those older builds have largely fallen by the wayside, but mostly because they retain that particular shortcut object while in XHTML mode anyway. So no worries sheila :)

Ho hum .. enough of my rambling :p Here's the new code, first with original whitespace and commenting:


//DOM-ready watcher
function domReady()
{
//start or increment the counter
this.n = typeof this.n == 'undefined' ? 0 : this.n + 1;

//if DOM methods are supported, and the body element exists
//(using a double-check including document.body, for the benefit of older moz builds [eg ns7.1]
//in which getElementsByTagName('body')[0] is undefined, unless this script is in the body section)
//>>> and any elements the script is going to manipulate exist
if
(
typeof document.getElementsByTagName != 'undefined'
&& (document.getElementsByTagName('body')[0] != null || document.body != null)
//>>> && document.getElementById('something') != null
)
{
//>>>-- DOM SCRIPTING GOES HERE --<<<


alert("The DOM is ready!");


//>>>-----------------------------<<<
}

//otherwise if we haven't reached 60 (so timeout after 15 seconds)
//in practise, I've never seen this take longer than 7 iterations [in kde 3.2.2
//in second place was IE6, which takes 2 or 3 iterations roughly 5% of the time]
else if(this.n < 60)
{
//restart the watcher
//using the syntax ('domReady()', n) rather than (domReady, n)
//because the latter doesn't work in Safari 1.0
setTimeout('domReady()', 250);
}
};
//start the watcher
domReady();


And here's the code again formatted for production use, with minimal whitespace and no comments:


function domReady(){this.n=typeof this.n=='undefined'?0:this.n+1;if(typeof document.getElementsByTagName!='undefined'&&(document.getElementsByTagName('body')[0]!=null||document.body!=null))
{

alert("The DOM is ready!");

}
else if(this.n<60){setTimeout('domReady()',250);}};domReady();


I've tested it in every version of every browser I have (and that's a lot...) and it works in all DOM-capable browsers, or fails cleanly.

fci
04-17-2005, 07:50 AM
in my case, I could move this call to the to the bottom(to before the </body> tag or whatever is 'standard'), loader.exec(), or would that not solve the problem?

brothercake
04-18-2005, 03:34 PM
Well, before this thread I would have said no, that won't work - but in fact it probably will, contrary to what I thought before: if your script is immmediately before the </body> tag and doesn't manipulate any other elements, you should be fine.

The advantages of my method are its rechecking - so if an object doesn't exist it can wait and try again - and the fact that it can go anywhere in the document.

fci
04-18-2005, 05:20 PM
have you thought of allowing your domReady function to accept a function as an argument(similar to how mine does) ? I deal more with dynamic(php, bleh) stuff. if I did static pages(for example) then your function would make more sense to me (just trying to explain my perception of the problem)

brothercake
04-18-2005, 07:01 PM
have you thought of allowing your domReady function to accept a function as an argument(similar to how mine does) ? I deal more with dynamic(php, bleh) stuff. if I did static pages(for example) then your function would make more sense to me (just trying to explain my perception of the problem)
Oh .. you mean so that it can be used a library script without having to have multiple individually-edited versions.

So a construct that could be used like this:


domReady(funcName);

Or like this


domReady(function()
{

});

Is that what you mean?

fci
04-19-2005, 12:01 PM
yes. your current script isn't flexible enough for the dynamic needs that exist(in my case, at least). I'll probably add a DOM check in what I wrote as a result of this thread. you'll also need to modify your setTimeout call.

brothercake
04-22-2005, 10:40 AM
Right then, I've converted this to an OO library script as you suggested (good idea :)). The library code looks like this:


//DOM-ready watcher
function domFunction(f, a)
{
//initialise the counter
var n = 0;

//start the timer
var t = setInterval(function()
{
//continue flag indicates whether to continue to the next iteration
//assume that we are going unless specified otherwise
var c = true;

//increase the counter
n++;

//if DOM methods are supported, and the body element exists
//(using a double-check including document.body, for the benefit of older moz builds [eg ns7.1]
//in which getElementsByTagName('body')[0] is undefined, unless this script is in the body section)
if(typeof document.getElementsByTagName != 'undefined' && (document.getElementsByTagName('body')[0] != null || document.body != null))
{
//set the continue flag to false
//because other things being equal, we're not going to continue
c = false;

//but ... if the arguments object is there
if(typeof a == 'object')
{
//iterate through the object
for(var i in a)
{
//if its value is "id" and the element with the given ID doesn't exist
//or its value is "tag" and the specified collection has no members
if
(
(a[i] == 'id' && document.getElementById(i) == null)
||
(a[i] == 'tag' && document.getElementsByTagName(i).length < 1)
)
{
//set the continue flag back to true
//because a specific element or collection doesn't exist
c = true;

//no need to continue
break;
}
}
}

//if we're not continuing
//we can call the argument function and clear the timer
if(!c) { f(); clearInterval(t); }
}

//if the timer has reached 60 (so timeout after 15 seconds)
//in practise, I've never seen this take longer than 7 iterations [in kde 3.2.2
//in second place was IE6, which takes 2 or 3 iterations roughly 5% of the time]
if(n >= 60)
{
//clear the timer
clearInterval(t);
}

}, 250);
};

And is called in either of these two ways:


function myFunction()
{
alert("The DOM is ready [named function]");
};
var foobar = new domFunction(myFunction, { 'h1' : 'tag', 'poster' : 'id' });



var foobar = new domFunction(function()
{
alert("The DOM is ready [anonymous function]");

}, { 'h1' : 'tag', 'poster' : 'id' });

Where the second argument is an object-literal defining the elements which must exist - either an ID attribute then "id" for an individual element, or a tag name then "tag" for a collection

As a side effect of converting to this construct we've lost some browsers - Safari 1.0, Mac IE5, and Konqueror < 3.2 But that's probably a blessing all things considered ...

mindlessLemming
05-09-2005, 01:10 PM
This script r0cks! :thumbsup:
You should add it to the 'Post a javascript' forum aswell...

Kor
05-09-2005, 01:59 PM
Looks OK to me, as well, that's really neat, brothercake...

brothercake
06-16-2005, 12:14 PM
New complications ... consider:

You have a script that wants to iterate through <p> elements and do something with each one - you add 'p' : 'tag' as a collection to the domFunction constructor, and it checks that before allowing the script to proceed ... okay.

But - what if the page is large with many paragraphs, or each paragraph is dense and takes a comparitively long time to render? All the script does is check that the collection length is not zero, it doesn't know whether the collection is full, so the final function will be called with whatever collection is there at the time, which may or may not be all of them.

There are two ways I can think of to fix this - but one is an ugly hack, and the other doesn't really fix it:

The ugly hack is easy, just identify an element in the document that comes after the final element in the collection we're interested in. That works (and I've used it) but it requires a dependency on the HTML, and that's a bad thing.

So then I thought, track the length of the collection, and don't call the function until the collection is stable - the same length, and greater than zero, for two consecutive iterations. Like this in fact:


//DOM-ready watcher function
function domFunction(f, a)
{
//initialise the counter
var n = 0;

//create an object parallel to the arguments object
//which will be used to store the length of each argument collection
//so that we can compare lengths at each iteration to see if the collection is full
var p = {};

//start the timer
var t = setInterval(function()
{
//continue flag indicates whether to continue to the next iteration
//our initial assumption is that the DOM is not ready and we will continue
var c = true;

//increase the counter
n++;

//if DOM methods are supported, and the body element exists
//(using a double-check including document.body, for the benefit of older moz builds [eg ns7.1]
//in which getElementsByTagName('body')[0] is undefined, unless this script is in the body section)
if(typeof document.getElementsByTagName != 'undefined' && (document.getElementsByTagName('body')[0] != null || document.body != null))
{
//set the continue flag to false; so now our asssumption is that
//the DOM is ready and we're not going to continue
c = false;

//but ... if the arguments object is there
if(typeof a == 'object')
{
//iterate through the object
for(var i in a)
{
//if the value is "id" and the element with the given ID doesn't exist
if(a[i ] == 'id' && document.getElementById(i) == null)
{
//set the continue flag back to true
c = true;

//no need to finish this loop
break;
}

//otherwise if the value is "tag"
else if(a[i ] == 'tag')
{
//store collection length
var l = document.getElementsByTagName(i).length;

//if we haven't defined a length in the parallel object
//or we have, but it's not equal to the current collection length
//or the current collection length is less than 1
//then this collection is either empty, still growing, or non-existent
if(typeof p[i ] == 'undefined' || p[i ] != l || l < 1)
{
//save length value to parallel object
p[i ] = l;

//set the continue flag back to true
c = true;

//no need to finish this loop
break;
}
}
}
}

//if we're not continuing
//we can call the argument function and clear the timer
if(!c) { f(); clearInterval(t); }
}

//stop if the timer reaches 30 (so timeout after 36 seconds)
if(n >= 30)
{
//clear the timer
clearInterval(t);
}

}, 1200);
};

The problem is that I've had to set a very slow timeout speed to make it work - to avoid false-matches just because one element took some time to render, or the interpretor had to pause for one of a million reasons. But even with that slow timeout, it's not a failsafe method.

And anyway, having such a slow timer means that on very lightweight pages, the function could actually take longer to be executed than a simple window.onload! I did think to track it over a larger number iterations of a smaller length, but whatever the numbers, that still isn't reliable, it's just linearly less likely to fail.

So the question is - how can I establish that a collection is full with any degree of certainty?

enumerator
06-21-2005, 01:57 AM
attachNotification Method (http://msdn.microsoft.com/library/en-us/script56/html/letmthattachN.asp): documentReady precedes window.onload :cool:

brothercake
06-21-2005, 03:30 PM
Cheers :) But you know what I'm gonna say ;)

Kor
06-21-2005, 04:19 PM
Cheers But you know what I'm gonna say

:D :D :D

enumerator
06-21-2005, 07:52 PM
Hypothetically, getElementsByTagName doesn't give partial results, so length will be either accurate or zero... how about that? ;)

jkd
06-21-2005, 09:30 PM
Hypothetically, getElementsByTagName doesn't give partial results, so length will be either accurate or zero... how about that? ;)

Uh, no? Not in Gecko-based browsers, at least. getElementsByTagName() only returns elements which exist. A page doesn't go from no elements to all elements, it parses and renders incrementally.



<html><head><title>partial test</title></head><body>

<ul id="test"><li>1</li><li>2<script>alert(document.getElementById("test").childNodes.length)</script></li><li>3</li><li>4</li><li>5</li></ul>

</body></html>

enumerator
06-21-2005, 09:52 PM
What about calling the method on document.body... once you've got the object, it has been parsed, no? (That's where I was coming from).

jkd
06-21-2005, 10:20 PM
What about calling the method on document.body... once you've got the object, it has been parsed, no? (That's where I was coming from).

Once again, no. See my code above - the reference is to the <ul>. It hasn't finished parsing yet, but it still exists in the DOM - to the point as far as it has been parsed. (The </script> tag)

enumerator
06-21-2005, 10:32 PM
Yeah, I got that (from the second post in this thread), but it seemed to work differently w/ setInterval (actually, I had too few elements), so... for my next guess, we'll have to get the actual source code, count the tags, and then compare the collection length. :thumbsup:

enumerator
06-24-2005, 11:59 PM
Right, should've quit while I was ahead (Kor made me do it!)... :D

the parser declares the document readyState to be "4". Shame it doesn't just report that, since all browsers have it (or something equivalent) in their internal event architecture ...
I agree, the DOM needs notifications.

brothercake
07-07-2005, 05:18 PM
A page doesn't go from no elements to all elements, it parses and renders incrementally.
...
See my code above - the reference is to the <ul>. It hasn't finished parsing yet, but it still exists in the DOM - to the point as far as it has been parsed. (The </script> tag)
Exactly, so knowing that a collection exists is not enough to assert that it's full. Without that assertion, the robustness of the script is compromised - all it can reliably say is that a particular element exists.


so... for my next guess, we'll have to get the actual source code, count the tags, and then compare the collection length. :thumbsup:
You can only read the source code as far as it's been rendered - so what do you compare against, other than itself? You'd get the same figure, but you wouldn't know whether that was all of them.

I made a test page, with 700 paragraphs of Lorem Ipsum, and in order for it to work in IE I had to set the timeout length longer than a second. But that's still not reliable generically - it's just what worked in that case. In theory, a single element could take several seconds to render, or 100 could be done in milliseconds.

So, since the time it will take to render a collection is unknowable, no timeout speed or number of iterations will ever be completely safe - the whole approach is unworkable. :(

But then I had another idea :) what about checking for siblings? If we had a element reference and we wanted to assert that it, and all its children, exist, could we make that assertion by the fact that:

1 - it has a nextSibling, or;
2 - it doesn't because it's the end of the document

Is that safe? Is the rendering of child elements synchronous in that way? In this case for example:


<div id="test1">
... lots of inner content
</div>
<div id="test2">
</div>

Does the existence of test2 imply the existence of all test1's children?

Willy Duitt
07-07-2005, 08:35 PM
How about checking for the lastChild of the body and then traverse backup thru all tags until you reach the head... Somewhere I once posted a prototype function I wrote to find the parentNode by tagName which should prove useful in such a situation...

.....Willy

Added link: http://www.devguru.com/Technologies/xmldom/quickref/node_lastChild.html

enumerator
07-07-2005, 09:06 PM
You can only read the source code as far as it's been rendered - so what do you compare against, other than itself?responseText of a synchronous request (I was joking, btw).

brothercake
07-07-2005, 10:52 PM
responseText of a synchronous request (I was joking, btw).
You may joke, but I considered that as well - make a request for the same page, and when that completes, you can pretty-reasonably assert that the host page has completed.

But that's not guaranteed either, and I was more concerned that it could end up taking longer than the original load event

enumerator
07-07-2005, 11:41 PM
If send() is synchronous, then further parsing would be precluded (until whenever)...

brothercake
07-08-2005, 03:45 AM
Right, but if the send is asynchronous, that request won't have completed before the host page has completed. Probably. But it might take significantly longer from the latency inherent in a server request.

Not sure really .. it's a task that "takes some time" but it isn't otherwise connected to the state we're testing.

enumerator
07-08-2005, 09:28 AM
Yeah, kinda lame... how about enclosing all content in a <noscript> tag, to allow for working with the unrendered DOM (via innerHTML, etc.), onload?

_com
07-08-2005, 11:32 AM
Is there anyway to do an image onload a simpler way? inline event handler is not an option here





<div id="wrapperlargeImage" style="z-index:1;">
<img id="largeImage" src="largeImage.jpg" width="732" height="460" alt="">
</div>
<script type="text/javascript">

function domReady(){this.n=typeof this.n=='undefined'?0:this.n+1;
if(typeof document.getElementsByTagName!='undefined'&&(document.getElementsByTagName('body')[0]!=null||document.body!=null))
{

document.getElementById('largeImage').onload=unhideSmallDivs;

}
else if(this.n<60){setTimeout('domReady()',250);}};domReady();


</script>

brothercake
07-08-2005, 02:51 PM
You shouldn't even need a construct like this to do that, you should just be able to do something like this:


<div id="wrapperlargeImage" style="z-index:1;">
<img id="largeImage" src="largeImage.jpg" width="732" height="460" alt="">
</div>
<script type="text/javascript">

document.getElementById('largeImage').onload = function()
{
//and so on...
}

</script>

brothercake
07-08-2005, 03:03 PM
Yeah, kinda lame... how about enclosing all content in a <noscript> tag, to allow for working with the unrendered DOM (via innerHTML, etc.), onload?
Not sure I follow you there ..?

enumerator
07-08-2005, 04:11 PM
Just a thought: <noscript> content is not DOM-inated, so onload fires immediately...

IE might be alone supporting noScript innerHTML--but if you had that, objects could be created/manipulated behind the scenes (before replacing the node to render).

which makes no sense... (oh well, that's all I had to say). :D

artimogrif
05-01-2007, 11:45 AM
Looks like IE 7 solved the issue. Regards!



EZ Archive Ads Plugin for vBulletin Copyright 2006 Computer Help Forum