27

I've been using jQuery to do this:

$element.find("*").each(function() {
    var $this = $(this);

    $this.removeAttr("style width align");

    if ($this.is("embed")) {
        $element.append("<div class='video'></div>");
        $this.attr("width", 640).attr("height", 360).parent().appendTo("#" + element + " .video");
    };
});

But I've been reading that jQuery's .each() method is quite slow when compared to a simple for loop (jsPerf). My question is how can I mimic this with pure JS? Find all elements within a div, then loop through the nodes.

I've tried to search for this but all I can seem to find are jQuery answers - everywhere.

I've tried other things but this was as close as I got to selecting all descendants:

var children = document.getElementById('id').getElementsByTagName('*');

for( var i = 0; i<children.lengtth; i++){
    children[i].removeAttribute("style");
    console.log(children[i]);
}
Jonny Sooter
  • 2,417
  • 1
  • 24
  • 40
  • the reason for the existence of jquery is exactly this... it may be slower (how cares...) but it will work on all browsers... do you really want to iterate the dom tree and make it work in all browsers? – Rufinus Jul 23 '13 at 21:43
  • [].slice.call(elm.children) gives you an array of element children, while [].slice.call(elm.childNodes) includes text nodes. getElementsByTagName() will go deeper than direct children, if that's what you want. – dandavis Jul 23 '13 at 21:45
  • @Rufinus I don't really want to, but people who are in charge have poor feelings towards jQuery. And in this case, I'm looping through quite a few elements (5k+), so I care if it's slower. – Jonny Sooter Jul 23 '13 at 21:52
  • look into Zepto, maybe they will like that, or at least it's spirit, and you will like "using jquery" to code... – dandavis Jul 23 '13 at 21:55
  • why are you moving `$this.parent()` - surely that must be `$element`, which is the element whose children you're iterating over. – Alnitak Jul 23 '13 at 23:10
  • @Rufinus—the alternative JS code in the OP will also work in any browser in use, there are no cross–browser issues. – RobG Jul 23 '13 at 23:48
  • @Alnitak the `.find("*")` method will get ALL elements inside `$element`, not only the direct children. So parent of `$this` doesn't necessarily mean `$element`. – Jonny Sooter Jul 24 '13 at 00:38
  • @JonnySooter I did suspect that's what you intended, but you had (incorrectly) used the word _children_, rather than _descendants_. – Alnitak Jul 24 '13 at 06:41

4 Answers4

42

You're already doing it right

var ancestor = document.getElementById('id'),
    descendents = ancestor.getElementsByTagName('*');
    // gets all descendent of ancestor

Now you just need to loop over children

var i, e, d;
for (i = 0; i < descendents.length; ++i) {
    e = descendents[i];
    e.removeAttribute('style');
    e.removeAttribute('width');
    e.removeAttribute('align');
    if (e.tagName === 'EMBED') {
        d = document.createElement('div');
        d.setAttribute('class', 'video');
        ancestor.appendChild(d);
    }
}

Depending on what you're doing, because you're using getElementsByTagName to get descendents, descendents is a live NodeList, so it's length will change as you add more Nodes to ancestor. If you need to avoid this, convert it to an Array before the loop

decendents = Array.prototype.slice.call(decendents);

See this gist for a reusable function.

Web_Designer
  • 72,308
  • 93
  • 206
  • 262
Paul S.
  • 64,864
  • 9
  • 122
  • 138
  • Since it's a live list, do I have to convert to array or can I save the length just in time when I'm going to start looping? – Jonny Sooter Jul 23 '13 at 22:05
  • @JonnySooter if the fact it is live will cause problems, I'd suggest always converting it to an _Array_, with the exception of if you want to remove nodes in the list, where looping downwards instead may be easier. – Paul S. Jul 23 '13 at 22:07
  • @Paul S. Well I move the embed node from the ancestor list and append it to the `d`evice. Would that count? – Jonny Sooter Jul 23 '13 at 22:11
  • @Paul S. Why not clone the element and use a document fragment for inserts so you don't have to worry about interacting with live nodes? – stavarotti Jul 23 '13 at 22:12
  • @JonnySooter you're doing more than just removing nodes here, so I'd say convert to non-live and stop having to worrying about it. – Paul S. Jul 23 '13 at 22:14
  • @stavarotti I was trying to be as true to original code pattern as possible. If you think that the difference is important enough, post a new answer for me to +1 ^^. – Paul S. Jul 23 '13 at 22:14
  • personally I wouldn't use `getElementsByTagName('*')` - I'd just start at `ancestor.firstChild` and then follow the `.nextSibling` chain. – Alnitak Jul 23 '13 at 22:59
  • @PaulS—to avoid the live NodeList issue, you can loop backwards over the collection (provided you are adding nodes after the current node). But converting to an array first will run very much faster in most browsers as the collection doesn't need to be updated with changes to that part of the DOM tree. Oh, and *Array.prototype.slice.call* will fail in IE 8 and lower with host objects (like a NodeList), use a loop. – RobG Jul 23 '13 at 23:50
  • @RobG does _slice_ fail on _Objects_ like `{length: 3}`, as well? Sounds like a broken implementation which the client should be told about to encourage them to upgrade :) .. and perhaps for a shim to prevent the website from failing, but that's a different issue. EDIT: just tested with IE10 in IE8-modes; `"Array.prototype.slice: 'this' is not a JavaScript object"`. – Paul S. Jul 24 '13 at 00:31
  • Not a broken but per ECMA-262 which states that it doesn't apply to host objects. You might like to read [What’s wrong with extending the DOM: Host objects have no rules](http://perfectionkills.com/whats-wrong-with-extending-the-dom/#host_objects_have_no_rules). – RobG Jul 24 '13 at 08:20
  • @RobG Unless I've missed the definition of "Result", ECMA-262 3rd doesn't tell you to throw an error if it's a host object; [**15.4.4.10**](http://www.ecma-international.org/publications/files/ECMA-ST-ARCH/ECMA-262,%203rd%20edition,%20December%201999.pdf), but in `[MS-ES3].pdf`, [**JScript's version**](http://www.microsoft.com/en-gb/download/details.aspx?id=14170), the algorithm is different. I checked section 9.9 (ToObject) and it doesn't say throw for host, in either spec. – Paul S. Jul 24 '13 at 11:43
  • Both say `The slice function is intentionally generic; it does not require that its this value be an Array object. Therefore it can be transferred to other kinds of objects for use as a method. Whether the slice function can be applied` **`successfully`** `to a host object is implementation-dependent.` (though all it requires for it to work successfully is a `length` and no errors on [[GET]]s for keys `0..n` so unless the implementation changes `slice`'s algorithm away from spec-owait) But **only JScript adds** `JScript 5.x does not allow the slice function to be applied to a host object.` – Paul S. Jul 24 '13 at 11:46
  • @PaulS—ECMA-262 generally allows host objects to do what they want, however it does have some restrictions such as those listed in [§8.6.2](http://www.ecma-international.org/ecma-262/5.1/#sec-8.6.2) (values returned by the internal `[[CLass]]` property, certain types of error, and so on). – RobG Jul 24 '13 at 22:58
8

could you just use something as simple as this?

    // get a handle to the div you want.
var div = document.getElementById('someID'),
    // get an array of child nodes
    divChildren = div.childNodes;

for (var i=0; i<divChildren.length; i++) {
    divChildren[i].style.width = null;
    divChildren[i].style.textAlign = null;
}
nicksweet
  • 3,929
  • 1
  • 20
  • 22
4

You can use querySelectorAll's forEach function.

document.querySelectorAll('li').forEach(function(element) {
    console.log(element);
});
Daniel Delgado
  • 4,813
  • 5
  • 40
  • 48
2

I commented in @Paul S.'s answer that you could can also clone the node and use a document fragment to add new embeds. Here is an example:

HTML:

<div>
    <div id="container">
        <div align="right">child</div>
        <div align="center">child</div>
        <embed src="" width="0" height="0" />
        <div align="center">child</div>
        <div style="width: 40px">child</div>
        <div style="font-size: 100px">child</div>
        <div width="60%">child</div>
        <embed src="" width="0" height="0"/>
        <div width="60%">child</div>
    </div>
</div>

JS:

var elm,
    clone,
    doc,
    next,
    fragment,
    live = document.getElementById("container");

if (live !== null) {
    fragment = document.createDocumentFragment();
    clone = live.cloneNode(true);
    next = clone.firstChild;
    while(next !== null) {
        if (next.nodeName !== "#text") {
            next.removeAttribute('style');
            next.removeAttribute('width');
            next.removeAttribute('align');

            if (next.tagName === 'EMBED') {
                doc = document.createElement('div');
                doc.setAttribute('class', 'video');
                doc.innerHTML = "EMBED detected, adding div...";
                fragment.appendChild(doc);
            }
        }
        next = next.nextSibling;
    }
    clone.appendChild(fragment);
    live.parentNode.replaceChild(clone, live);
}

You can see the demo here.

Cloning the node prevents the live modification of the DOM since the style attribute can have properties that cause the browser to repaint numerous times.

stavarotti
  • 582
  • 5
  • 19