tags:

views:

1431

answers:

7

According to spec, only the BODY and FRAMESET elements provide an "onload" event to attach to, but I would like to know when a dynamically-created DOM element has been added to the DOM in JavaScript.

The super-naive heuristics I am currently using, which work, are as follows:

  • Traverse the parentNode property of the element back until I find the ultimate ancestor (i.e. parentNode.parentNode.parentNode.etc until parentNode is null)

  • If the ultimate ancestor has a defined, non-null body property

    • assume the element in question is part of the dom
  • else

    • repeat these steps again in 100 milliseconds

What I am after is either confirmation that what I am doing is sufficient (again, it is working in both IE7 and FF3) or a better solution that, for whatever reason, I have been completely oblivious to; perhaps other properties I should be checking, etc.


EDIT: I want a browser-agnostic way of doing this, I don't live in a one-browser world, unfortunately; that said, browser-specific information is appreciated, but please note which browser you know that it does work in. Thanks!

A: 

You want the DOMNodeInserted event (or DOMNodeInsertedIntoDocument).

Edit: It is entirely possible these events are not supported by IE, but I can't test that right now.

eyelidlessness
Thanks - am I right to assume that this is a Mozilla/Firefox/Gecko specific idiom?
Jason Bunting
No, you have no this right. This is DOM-Events standard idiom.
Sergey Ilinsky
Got it. That said, I don't think IE (the red-headed step-child of browsers) supports it.
Jason Bunting
+6  A: 

Can you no do a document.getElementById('newElementId'); and see if that returns true. If not, like you say, wait 100ms and try again?

Darryl Hein
What if it does not have an id? Not going to work then, and this may be the case. Good answer otherwise...maybe I will see if this will work for some of my needs, and use the other method in case I don't have an id.
Jason Bunting
Why not just add an id? You can add a unique id using some kind of random string and then just look for that one. I know you don't like jQuery, but with jQuery you could easily find it based on maybe a class, parent or child.
Darryl Hein
i think this might be the simplest solution you'll find.
nickf
True, this may be a good way to do it - I will have to think about it a bit though...thanks. LOL @ me not liking jQuery. I do like it, I just don't like when people think it is the answer to every JavaScript question around. :)
Jason Bunting
So, I like this and Borgar's ideas too. I could test to see if the element under question has an ID property; if so, use that with getElementById, else create a random, hopefully-unique ID and assign it; when getElementById finds it, remove the originally-non-existent ID and go forward from there.
Jason Bunting
I like this idea. It's succinct. It's practically a certainty that document.getElementById() will work, if anything does. :-)
Borgar
True that - simplicity, consistency and reliability across browsers would make this the better solution; I really don't want to write browser-specific code so I will change the approved answer.
Jason Bunting
A: 

Neither of DOMNodeInserted or DOMNodeInsertedIntoDocument events is supported in IE. Another missing from IE [only] feature of DOM is compareDocumentPosition function that is designed to do what its name suggests.

As for the suggested solution, I think there is no better one (unless you do some dirty tricks like overwriting native DOM members implementations)

Also, correcting the title of the question: dynamically created Element is part of the DOM from the moment it is created, it can be appended to a document fragment, or to another element, but it might not be appended to a document.

I also think that the problem you described is not the problem that you have, this one looks to be rather one of the potential solutions to you problem. Can you probably explain in details the use case?

Sergey Ilinsky
Thanks for responding - the problem I described *is* the problem I am having, but thanks for trying to read my mind regardless. :) I have abstracted the details of my problem to the point that an answer to this question will be sufficient to solve my more specific problem.
Jason Bunting
+1  A: 

You could query document.getElementsByTagName("*").length or create a custom appendChild function like the folllowing:

var append = function(parent, child, onAppend) {
  parent.appendChild(child);
  if (onAppend) onAppend(child);
}

//inserts a div into body and adds the class "created" upon insertion
append(document.body, document.createElement("div"), function(el) {
  el.className = "created";
});

Update

By request, adding the information from my comments into my post

There was a comment by John Resig on the Peppy library on Ajaxian today that seemed to suggest that his Sizzle library might be able to handle DOM insertion events. I'm curious to see if there will be code to handle IE as well

Following on the idea of polling, I've read that some element properties are not available until the element has been appended to the document (for example element.scrollTop), maybe you could poll that instead of doing all the DOM traversal.

One last thing: in IE, one approach that might be worth exploring is to play with the onpropertychange event. I reckon appending it to the document is bound to trigger that event for at least one property.

Leo
As I mentioned in the comments that appear directly under my question, I may not be in control of when the object is added to the DOM, otherwise I wouldn't even ask this question. But thank you for responding!
Jason Bunting
As for your first idea, that isn't as narrow a heuristic as I need - other code could add DOM elements and I would have to assume that when the length changed, it was because *my* element was being added...such an assumption is antithetical to the determinism I need.
Jason Bunting
I see. There was a comment by John Resig on the Peppy library on Ajaxian today that seemed to suggest that his Sizzle library might be able to handle DOM insertion events. I'm curious to see if there will be code to handle IE as well.
Leo
Following on the idea of polling, I've read that some element properties are not available until the element has been appended to the document (for example element.scrollTop), maybe you could poll that instead of doing all the DOM traversal.
Leo
Yeah, something like that might be nice, although I thought the Sizzle project was simply to provide for a faster selector implementation, not anything more. Either way, Resig is a pimp.
Jason Bunting
One last thing: in IE, one approach that might be worth exploring is to play with the onpropertychange event. I reckon appending it to the document is bound to trigger that event for at least one property.
Leo
Interesting idea - I might check into that too, thanks lhorie; when I get time I will research this a bit more, but for now, at least, I have sufficient; traversing and polling definitely sucks though!
Jason Bunting
Thank lhorie, good ideas. :)
Jason Bunting
lhorie, would you mind adding these ideas to your actual answer (just edit it)? I think they add value, but since they are comments (which can be deleted easily by just about anyone with the rep) they are not as potentially useful. Thanks!
Jason Bunting
+1  A: 

In a perfect world you could hook the mutation events. I have doubts that they work reliably even on standards browsers. It sounds like you've already implemented a mutation event so you could possibly add this to use a native mutation event rather than timeout polling on browsers that support those.

I've seen DOM-change implementations that monitor changes by comparing document.body.innerHTML to last saved .innerHTML, which isn't exactly elegant (but works). Since you're ultimately going to check if a specific node has been added yet, then you're better off just checking that each interrupt.

Improvements I can think of are using .offsetParent rather than .parentNode as it will likely cut a few parents out of your loop (see comments). Or using compareDocumentIndex() on all but IE and testing .sourceIndex propery for IE (should be -1 if node isn't in the DOM).

This might also help: X browser compareDocumentIndex implementaion by John Resig.

Borgar
Sweet - thank you, this is good info.
Jason Bunting
Turns out that offsetParent will not always work very well...it doesn't always get back to the Document itself, whereas traversing up the tree with parentNode will, if the node is part of the DOM tree; else it ultimately returns null. Not all paths up the tree with offsetParent work like this.
Jason Bunting
Also, offsetParent isn't part of the DOM spec, apparently: https://developer.mozilla.org/En/DOM/Element.offsetParent What's more, it "returns null when the element has style.display set to 'none'" which would screw things up a bit for me.
Jason Bunting
Yeah, that's not going to work well for this problem. :-P
Borgar
+4  A: 

UPDATE: For anyone interested in it, here is the implementation I finally used:

function isInDOMTree(node) {
   // If the farthest-back ancestor of our node has a "body"
   // property (that node would be the document itself), 
   // we assume it is in the page's DOM tree.
   return !!(findUltimateAncestor(node).body);
}
function findUltimateAncestor(node) {
   // Walk up the DOM tree until we are at the top (parentNode 
   // will return null at that point).
   // NOTE: this will return the same node that was passed in 
   // if it has no ancestors.
   var ancestor = node;
   while(ancestor.parentNode) {
      ancestor = ancestor.parentNode;
   }
   return ancestor;
}

The reason I wanted this is to provide a way of synthesizing the onload event for DOM elements. Here is that function (although I am using something slightly different because I am using it in conjunction with MochiKit):

function executeOnLoad(node, func) {
   // This function will check, every tenth of a second, to see if 
   // our element is a part of the DOM tree - as soon as we know 
   // that it is, we execute the provided function.
   if(isInDOMTree(node)) {
      func();
   } else {
      setTimeout(function() { executeOnLoad(node, func); }, 100);
   }
}

For an example, this setup could be used as follows:

var mySpan = document.createElement("span");
mySpan.innerHTML = "Hello world!";
executeOnLoad(mySpan, function(node) { 
   alert('Added to DOM tree. ' + node.innerHTML);
});

// now, at some point later in code, this
// node would be appended to the document
document.body.appendChild(mySpan);

// sometime after this is executed, but no more than 100 ms after,
// the anonymous function I passed to executeOnLoad() would execute

Hope that is useful to someone.

NOTE: the reason I ended up with this solution rather than Darryl's answer was because the getElementById technique only works if you are within the same document; I have some iframes on a page and the pages communicate between each other in some complex ways - when I tried this, the problem was that it couldn't find the element because it was part of a different document than the code it was executing in.

Jason Bunting
A: 

Here is another solution to the problem extracted from the jQuery code. The following function can be used to check if a node is attached to the DOM or if it is "disconnected".

function isDisconnected( node ) {
    return !node || !node.parentNode || node.parentNode.nodeType === 11;
}

A nodeType === 11 defines a DOCUMENT_FRAGMENT_NODE, which is a node not connected to the document object.

For further information see the following article by John Resig: http://ejohn.org/blog/dom-documentfragments/

nik