2

I am writing a function for a library that takes an object of elements and removes it from the DOM. I have this working fine but was wondering if there is a way to do a single for loop? I found that NodeLists and HTMLCollections can't use the same for loop so I build an array then remove the elements with the same loop.

   _remove = function(elem) {
      if(!elem) { return false; }
      // getElementById
      if(!elem.length) {
        if(elem.parentNode) {
          elem.parentNode.removeChild(elem);
        }
      // querySelectorAll, jquery, getElementsByClassName, getElementsByTagName
      } else {
        var elems = [];
        for(var j = 0; j<elem.length; j++) {
          if(elem[j]) {
            elems.push(elem[j]);
          }
        }
        for(var i=0; i<elems.length; i++) {
          if(elems[i].parentNode) {
            elems[i].parentNode.removeChild(elems[i]);
          }
        }
        return false;
      }
    }

called by:

_remove(document.getElementById('someId'));
_remove(document.getElementsByClassName('someClass'));
_remove(document.getElementsByTagName('tag'));
_remove($('#someId));
_remove($('.someClass));
_remove(document.querySelectorAll('someID or Class or Tag));
lapin
  • 2,098
  • 2
  • 21
  • 30
user1572796
  • 1,057
  • 2
  • 21
  • 46
  • Iterate in reverse, and you'll be covered for both types of lists. –  Oct 31 '14 at 01:01
  • can you give an example? I used both types of loops because while NodeLists were working with the 2nd loop by itself HTMLCollections were removing items reducing i as the loop was running...so half the items in the collection were not getting removed – user1572796 Oct 31 '14 at 01:02
  • Start with `i` as the last index instead of the first, and decrement instead of incrementing. –  Oct 31 '14 at 01:03

2 Answers2

5

Some of these functions return a live NodeList, which means that when you change the DOM, the collection changes immediately to reflect this. That's why a normal for loop doesn't work: when you remove elem[0], all the other entries in the collection move down. Then when you increment the index, you skip over the new elem[0].

The easiest way to work around this is to loop from the high end down, instead of from 0 up.

for (var j = elem.length-1; j >= 0; j--) {
    if (elem[j].parentNode) {
        elem[j].parentNode.removeChild(elem[j]);
    }
}
Barmar
  • 741,623
  • 53
  • 500
  • 612
  • 1
    `gEBTN` and `gEBCN` return live lists, but `qSA` doesn't. –  Oct 31 '14 at 01:02
  • not removing the parent, that is just the native js way of removing an element, select its parent then remove the child – user1572796 Oct 31 '14 at 01:09
  • typo in parentNode so won't let me edit but other than that works great, thanks! – user1572796 Oct 31 '14 at 01:11
  • @jfriend00: I don't think that'll be an issue since document order dictates a depth-first ordering, so going in reverse, children will always be removed before parents. If I'm mistaken, can you provide an example? –  Oct 31 '14 at 01:14
  • @jfriend00 How would that be a problem? I think you'd need to handle duplicates though. – 1983 Oct 31 '14 at 01:15
  • @mintsauce: That's an interesting thought about duplicates, though native DOM selection should never include duplicates, and non-native lists will be covered with the `if (elem[j].parentNode)` test. –  Oct 31 '14 at 01:17
  • @squint Yeah, I think you're right: it looks like duplicates aren't a problem. – 1983 Oct 31 '14 at 01:40
  • @squint Thanks for the correction. I've changed the answer to be less specific. For the purposes of this question, it's not important precisely which lists are live, as this solution works for both live and static NodeLists. – Barmar Oct 31 '14 at 15:26
  • Right, just better to head off misleading information. I'd have changed it myself, but some people get touchy about that. –  Oct 31 '14 at 16:32
  • @squint I understand -- I limit my edits to obvious typos and formatting, anything more substantial gets a comment. – Barmar Oct 31 '14 at 18:14
2

I would recommend a bit more robust implementation for these reasons:

  1. If it's a jQuery object passed in, then you should call the jQuery .remove() because jQuery may leak memory for data and event listeners if you don't let jQuery remove DOM elements that jQuery operations such as .data() or .on() have been used with.

  2. It's not enough to just do if (elem.length) because it could still be a nodeList or an Array with a .length of 0. I changed it to test to see if that property actually exists. FYI, if you want a more robust test to see if it's an actual single DOM node, there's a robust function to do that here, though I'm not sure that's needed here.

  3. To protect against a dynamic nodeList changing out from under you or children and parents both being in the list (you don't want to remove parent before child), it is best to iterate the nodeList backwards from back to front (you don't have to make a copy).

  4. If this is just a normal array of DOM elements that are not necessarily in document order and there are parent/child elements both in the list and out of order or anyone passes you an array with any old DOM elements in it, you could end up getting an exception on a .removeChild() operation so it is best to just catch any exceptions that might happen there so the operation can always complete through the whole list, even if there is a bad element in the list.

  5. Your code did not have a consistent return value. It had one path that wasn't returning anything. I changed it to return false if nothing elem was falsey and true if it was processed. You can obviously modify this as desired, but if you're going to have a meaningful return value, you should have a return value for all code paths (you didn't have one if it was a single DOM node).

Recommended code:

// _remove(elem) - remove one or more DOM nodes from their DOM hierarchy
// elem can be an Array of DOM nodes or a pseudo-array like a nodeList 
// or elem can be a single DOM element

function _remove(elem) {
    if (!elem) {
        return false;
    } else if (typeof elem.jquery === "string" && typeof elem.remove === "function") {
        // if a jQuery object, it should be removed with jQuery 
        // so jQuery data and event listener stuff will get cleaned up
        // there are more comprehensive ways to check for a jQuery object,
        // but those are probably not needed here
        elem.remove();
    } else if (elem.nodeType) {
        // single DOM node
        // could also do more comprehensive test to see if it's really a DOM node, 
        // but probably not needed
        if (elem.parentNode) {
            elem.parentNode.removeChild(elem);
        }
    } else if (typeof elem.length === "number") {
        // array or pseudo-array here
        // querySelectorAll, getElementsByClassName, getElementsByTagName or 
        // an array of DOM elements assembled by any other code
        // iterate backwards so if it is a dynamic nodeList, then children will be
        // be removed before parents and any nodeList changes will happen after our
        // iteration point
        for (var i = elem.length - 1; i >= 0; i--) {
            // catch errors in case anything has already been deleted
            // from a prior parent delete so we don't abort early
            try {
                elem[i].parentNode.removeChild(elem[i]);
            } catch(e) { }
        }
    } else {
        // don't know what was passed here - not something we were expecting
        return false;
    }
    return true;
}
Community
  • 1
  • 1
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • You should drop the `typeof elem.length === "undefined"` part, or rework it some other way. Some elements are both an element and a collection, for example `
    ` and `
    –  Oct 31 '14 at 16:25
  • ...but good point about using jQuery's `.remove()` for safety. –  Oct 31 '14 at 16:26
  • 1
    @squint - good point about `.length`. I removed that check and just check for the `.nodeType` property. – jfriend00 Oct 31 '14 at 17:10