Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does replaceChild() behave oddly when replacing one kind of element with another?

Tags:

javascript

I am relatively new at javascript, and found an interesting behavior that I can't explain today. I have a custom <hr> (with an image) on a website, which displays oddly in IE7 and below. To overcome this, I wanted to use replaceChild() in combination with getElementsByTag(). Initially, I simply tried to loop over the list, so:

var hrules = document.getElementsByTagName('hr');
for (var i=0; i < hrules.length; i++) {

   var newHrule = document.createElement("div");
   newHrule.className = 'myHr';

   hrules[i].parentNode.replaceChild(newHrule, hrules[i]);

   document.write(i);
}

However, this does not work: it actually only gets half the elements, skipping every other one. Printing i gives half-integer values of the actual number of <hr> elements in the document (e.g. if there are 7 <hr/> elements, it prints 4. By contrast, the following does work:

var hrules = document.getElementsByTagName('hr');
var i = 0;
while (i < hrules.length) {

   var newHrule = document.createElement("div");
   newHrule.className = 'myHr';

   hrules[i].parentNode.replaceChild(newHrule, hrules[i]);

   document.write(i);
}

i is printed the same number of times as there are hrules in the document (but of course is always 0, since I'm not incrementing it), and the hrules are replaced correctly. I recognize that the while here might as well be while(true)--it's just going until it runs out of <hr> elements, but appears to stop after that (it's not printing any more 0s).

I've tried this with a number of different types of elements, and observed that this only occurs when replacing one kind of element with another. I.e., replacing p with div, span with p, etc. If I replace p with p, div with div, etc. the original example works correctly.

Nothing in the documentation I've found (w3schools, various Google search, here, etc.) suggests an obvious answer.

What is going on here? First, why does the second example I offered work - is replaceChild() iterating over the elements automatically? Second, why is the behavior different for different types of element?

like image 333
Chris Krycho Avatar asked Jan 05 '11 16:01

Chris Krycho


2 Answers

document.getElementsByTagName is a live access to all the HR elements in the document - it's updated whenever you change the document. You don't get a snapshot of all the HRs in the document whenever you call it.

So, with the first code, you are both incrementing i and reducing the size of hrules.length each time round the loop. This explains why you only see half the steps you expect.

like image 84
Matthew Wilson Avatar answered Oct 16 '22 22:10

Matthew Wilson


Here's the solution I ended up using, in case anyone else (like @Pav above) is curious.

var hrules = document.getElementsByTagName('hr');

/* Each repetition will delete an element from the list */
while (hrules.length) {
   var newHrule = document.createElement("div");
   newHrule.className = 'ieHr';

   /* Each iteration, change the first element in the list to a div
    * (which will remove it from the list and thereby advance the "head"
    * position forward. */
   hrules[0].parentNode.replaceChild(newHrule, hrules[0]);
}

Essentially, what happens is you get a list of all the hrules in the document. This list is dynamically updated as you interact with it (see Matthew Wilson's answer). Each time you change the first element of the list to a div, it gets removed from the list, and the list is updated accordingly. The result is that you simply need to act on the first element of the list each time until the length of the list is 0.

That's admittedly a little counterintuitive, but it's how the list works.

like image 25
Chris Krycho Avatar answered Oct 16 '22 21:10

Chris Krycho