How useful would it be to have NodeList inherit from Node/Element/etc


#1

So this would allow anything that can be done to a Node, be done to every Node in a NodeList, here’s a repository on it.

I apolgize early it’s not the best repository you’ve ever read, first readme I’ve written for others to see, so bare with me

Here’s a quick example:

var buttons = document.querySelectorAll(".buttons");
buttons.addEventListener('click', function(evt) {
  //do something
});

#2

The DOM, and JS in general, has avoided this kind of overloaded semantics, where a list of things can be operated on as if it was a single one of the things in the list. It’s a useful and popular pattern (appearing in jQuery, d3, and plenty more), but it’s also often confusing, and you have to use getter/setter methods rather than properties, which doesn’t match the current DOM design very well.

With arrow functions this kind of thing becomes a bit easier to write:

var buttons = [].slice.call(document.querySelectorAll(".buttons"));
buttons.forEach(el=>el.addEventListener('click', function(evt) {...});

If NodeLists ever get upgraded to an Array subclass, or if you use the .query() function from DOM instead (which returns an Elements rather than a NodeList, which is an Array subclass), you can avoid the [].slice.call() hack too.


Selector-based event listeners
#3

Which doesn’t match the current DOM design very well.

You’re right about that. But I mentioned in the repository that, my script was sort of a hack.

So yes in my script I used getters and setters. But that was for it to only work in ES5.

What I have in mind is to use ES6 proxies(should probably post in readme):

Where every instance of NodeList is a proxied NodeList:

var divs = document.querySelectorAll('div');
// But this is what querySelectorAll would return:
new Proxy(divs, {
  set(target, prop, value) {
    for(var element of target) {
        if(prop in element) element[prop] = value;
    }
  },
  get(target, prop) {
    //same thing in my script, doesn't make sense to show here without surrounding context
  }
});

Also divs will have the other methods as well. Like inheriting the HTMLElement methods:

$('div').addEventListener('click', callback);
$('div').href = 'whatever'; // wont be set because of the `prop in element` check
$('div').set('randProp', 'randValue'); // this will set it no matter what

#4

Also looking at the Node.prototype has a lot of getters and setters what’s wrong with NodeList.prototype having getters and setters


#5

To me the fundamental flaw with this idea is that it goes against the very nature of inheritance in object-oriented programming. That is, the statement “A NodeList is a Node” is false, so having NodeList inherit from Node makes no sense. See also: Liskov Substitution Principle.


#6

@domenic I see what you’re saying, but what if it doesn’t inherit, as I mentioned what if they are returned proxies which loop through the nodes and sets those on each NodeList.

Is it the fact that $('div').textContent = 'Hello'; Seems like it’s affecting the NodeList making it seem like a Node. Also the methods $('div').setAttribute()? The reason for that is the following wouldn’t look good for a native implementation as the following:

$('div').set('textContent', 'Hello'); // It looks like jQuery, which does this already

Now I’m debating with myself whether I’d make this into a library, and I’m thinking no mainly because jQuery already exist and is widely used already.

So what if everything was defined manually each method (renamed to match a NodeList)?

The only thing I can think of right now is plurals: addEventListener to addEventListener(s).

Perhaps setAttribute to setAttributeOnEach. Ok now that I think of this it looks horrible but perhaps others have better naming conventions.

But again I believe it would solve so many problems, Like most people would like jQuery implemented into browsers but I say no because jQuery does a lot more than manipulate the dom. So this would be my proposal for a better NodeList implementation. BTW I’m not sure if you read my repository so here’s a quick (not useful, but usecase) jsbin example


#7

@tabatkins NodeList is iterable, so using a for-of produces the most readable code, I think.

let buttons = document.querySelectorAll('button');

for (let button of buttons) {  
  button.addEventListener('click', (e) => { /* handler */ });  
}

Live demo: http://jsbin.com/xikise/edit?js,output#J:L2 (supported in Firefox and Chrome today, via Babel)


#8

@simevidas Sure, reasonable people can differ on what they find most readable. :slight_smile:


#9

My main worry with all of this is that it will then expand into, can we have all DOM APIs available to use. Like not all elements support .submit() but I feel like supporting these would be the thin wedge to making that demand.

var elements = document.querySelectorAll('.thing');
elements.submit();

That is the kind of API you expect from jQuery but not from the DOM.

If it is enough of an issue why not create your own wrapper to document.querySelectorAll like document.proxiedQuerySelectorAll which returns a modified prototype of NodeList which proxies the function calls over perhaps?


#10

Yes I’ve thought about this and tried. the only thing I had in mind is:

var elements = document.querySelectorAll('.thing');
elements.callOnEach('submit'); // but that's very weird and most likely wouldn't be accepted neither would I want it to.

elements.callOnEach('method', args); // So no.

I also tried making a prollyfill for document.querySelectorAll. What I don’t understand is function calls on proxies? So let’s say in my code how would I have a trap for a function call in a proxy:

elements.setAttribute('class', 'hi');

How would I set a trap for the above, anybody has any reference for specifically function call traps in ES6 proxies?


#11

So this would be what I meant by proxying:

NodeList.prototype.setAttribute = function () {
  var elements = [].slice.call(this);
  var results = [];
  for (var i = 0; i < this.length; ++i) {
    results.push(this[i].setAttribute.apply(this[i], arguments));
  }
  return results;
};

However it would be safer to invent your own method like:

document.proxiedQuerySelectorAll = function (){
  var ret = document.querySelectorAll.apply(document, arguments);
  ret.setAttribute = function setAttribute() {
    var elements = [].slice.call(this);
    var results = [];
    for (var i = 0; i < this.length; ++i) {
      results.push(this[i].setAttribute.apply(this[i], arguments));
    }
    return results;
  };
  return ret;
};


var elements = document.proxiedQuerySelectorAll('div');

elements.setAttribute('height', '200px');

This is ‘proxying’ as the Gang of Four call it, however as @tabatkins said this is also overloading.


#12

@jonathank Have you looked at the actual source code I already do the first one. What I was talking about for ES6 proxies is not having to use getters and setters because that’s what @tabatkins wanted :

So all methods in the DOM that return a NodeList would return something like:

document.querySelectorAll = function(selector) {
  var nodes = Document.prototype.querySelectorAll.call(document, selector);
   var proxiedNodes = new Proxy(nodes, {
     set(target, prop, value) {
       for(var element of target) {
          if(prop in element) element[prop] = value;
       }
     },
     get(target, prop) {
        //use same method I use in source code, just changed a bit to fit surrounding context
     }
   });
}

#13

Nope I had not, until just now. However you asked how to proxy setAttribute and I misunderstood what you were getting at.

Pretty sure the behaviour for doing that is still under discussion.


#14

Oh ok thanks. Then that’s why I couldn’t figure it out. I didn’t find anything on function call traps.


#15

No, the behavior is not under discussion.

To intercept a property that happens to be a function, like elements.setAttribute, you use the same trap that you use to intercept any other property: the get trap.


#16

@domenic that’s if a object has a method. For example:

var proxy = new Proxy({
   m() { console.log('this is called'); }
}, {
   get(target, prop) {
      if(target[prop].constructor == Function) {
        console.log('it's a function');
        return target[prop];
      }
   }
});

proxy.m; // would log it's a function and return the method
proxy.m(); // .m returns it so () calls it logging "this is called"

Now with a proxied nodeList I want to be able to just trap method calls that are not a part of the nodeList (I’ll use an array literal here)

var p = new Proxy([document.body], {
  get(target, prop) {
    if(!target[prop]) {
       return function() { console.log("it's called"); };
    }
  }
});
p.m(); // logs it's called;
p.whatever(); // logs it's called

Ok this works because Im checking if property doesn’t exist yet I’ll be doing that for everything else as well. Like getting textContent it will return an array of textContent for each node which is not a property of the NodeList.

I could just loop through each node and check if it’s a property or a method of each node, but that would be complicated because what if I query every element and try calling submit I’d have to do something really complicated and confusing? that in the long run would most likely be bad. So perhaps a proposal on function call traps? or is there already a discussion?

I could also just check HTMLElement.prototype[prop] to see if it’s a method or property but then again with certain elements they have their own methods and properties.

Proxies would be easier, but I think manually implementing every method and property would be most beneficial.


#17

You can have proxies for functions themselves, but methods are just treated as normal properties of objects. There is no separate method-invoke trap from the get trap, and there is no such trap under discussion.


#18

Ik this:

but methods are just treated as normal properties of objects. There is no separate method-invoke trap from the get trap.

But what do you mean by:

You can have proxies for functions themselves

can you show a quick small example code.


#19
var p = new Proxy(function () { }, {
  apply(target, thisArg, args) {
    // ...
  }
});

#20

By just looking at it, seems like you want me to do that for every method, but then again I would end up having to check if $('div').submit() is a method in each node, for that I’d just do everything inside the handler.get of the proxy which again will be very complicated. So right now I’m going to try to write a prollyfill for document.querySelectorAll and when I’m done post it here lmk what you think.