Skip to content

Problems with Looping Over querySelectorAll NodeLists in IE11

First, a few disclaimers:

  • I am a Javascript newbie.
  • I will be pointing out an error in a recent article on CSS-Tricks.com, which I owe a debt of gratitude to for years of excellent tips, instruction, and best practices.
  • This error relates to a problem that I have struggled to wrap my head around over the last year of learning JS, so I wanted to write a detailed response to help me solidify my understanding of the whole thing.

In a recent post on CSS-Tricks, Chris Coyier writes out a bunch of helpful options for a common Javascript problem: looping over querySelectorAll NodeLists. It’s a good, thoughtful read, but the last example caught my eye:

If you need maximum possible browser support, there is no shame in an ancient classic for loop:

for (let i = 0; i < buttons.length; ++i) {
  buttons[i].addEventListener('click', () => {
    console.log("for loop worked");
  });
}

As far as I can tell, there are two problems with this specific code as it relates to “maximum possible browser support”. (1)

Problem 1: IE11 does not support JS arrow functions

I noticed the arrow function (() => {}) used in the code, and an alert went off in my head — IE11 does not support arrow functions! 🚨 CSS-Tricks likely did not intend to exclude IE11 from this example code (the max browser support example). Let’s dig in to an IE11 friendly way of writing this function.

Problem 1 Workaround: use a regular function expression instead of an arrow function

While arrow functions are not just syntactic sugar for regular function expressions (see the MDN entry; look for how this keyword is handled differently!), in this case, we can change the arrow function to a function expression without making any… functional changes. Now the loop code looks like this:

for (let i = 0; i < buttons.length; ++i) {
  buttons[i].addEventListener('click', function() {
    console.log("for loop worked");
  });
}

However, this code still does not work properly in IE11. More info on why below.

Problem 2: IE11 does not properly support let.

In Internet Explorer 11, let variables are not bound separately to each iteration of for loops”. Essentially let is treated exactly the same as var, which is incorrect behavior, and will result in variables that are not properly bound.

Live Example

This live example attaches event listeners with the method described above. Clicking each button below will state which button was clicked based on the index. This works correctly in modern browsers, but does not work correctly in IE11. Since let does not bind separately to each iteration of the for loop that assigns event listeners, each element instead (in IE11) gets an event listener bound to the last element in the NodeList.

    Button clicked:

View an IE11 screen recording of let not working.

Problem 2 Workaround: capture that variable! — or be fancy and use polyfilled forEach (for IE11)

A common fix to the variable binding in a loop issue is to capture the variable in a closure. More on that method here and here (see under the Example 1 heading). However, this method feels to me like we are playing Hot-Potato-meets-Inception with variables. Not ideal.

Enter forEach(), which avoids our for() loop iterator variable binding issue. Fancy. And while IE11 does not support forEach() on NodeLists, it does support forEach() on arrays, so there is a remarkably straightforward polyfill, recommended & published by MDN, that we can employ:

if (window.NodeList && !NodeList.prototype.forEach) {
    NodeList.prototype.forEach = function (callback, thisArg) {
        thisArg = thisArg || window;
        for (var i = 0; i < this.length; i++) {
            callback.call(thisArg, this[i], i, this);
        }
    };
}

As MDN says, the above code will polyfill forEach() support on NodeLists to all browsers supporting ES5. That’s some good news! Now that we are cleared for using forEach(), this is what the CSS-Tricks JS looks like now:

buttons.forEach(function(element) {
  element.addEventListener('click', function() {
    console.log("for loop worked");
  });
});

Personally, I like how this code looks, and it makes sense to me. Huzzah!

Back to our live example. The example below uses the forEach() method to bind event listeners, and works correctly in IE11, thanks to the polyfill:

    Button clicked:

View an IE11 screen recording of forEach() working.

What about Babel? Can we use it instead?

Full Disclosure: I am not deeply experienced with Babel usage beyond some basic usage in a couple projects. But here’s what I know, and how I see it:

Short of summoning the Babel-Polyfill dump truck, Babel is not our friend here, since it will give us code that does not work properly. (At least, as far as I understand it…). Beyond that, I am loathe to recommend Babel when it is not actually needed. Babel is a fantastic collection of tools that have helped our industry make strides forward, but from where I stand, it’s just a bit too complex to recommend without reservations — especially when more straightforward options are available.


Footnotes:

  1. As of this writing, Microsoft continues to support Internet Explorer 11, which should be included in a “maximum possible” browser support situation. ↩︎

Post a Comment

Your email is kept private. Required fields are marked *