Selector-based event listeners


#1

What if browsers had first-class support for listeners that apply for elements based on selectors, the way libraries like jQuery implement event listeners? Would we potentially see significant performance over the existing performance of libraries like jQuery, Bean, and D3?


#2

Sounds good to me. :smile:

Something simpler I’d like to see is just a better syntax for this:

Array.prototype.forEach.call(document.querySelectorAll('ul.things > li > a.delete-button'), function(button) {
    button.addEventListener('click', function(){
        // Handle delete
    });
});

As a straw man, being able to do document.querySelectorAll(selector).addEventListener... and have that iterate over the elements for you would be wonderful. Having that selector be “live” would be cool too though.

[Edit: I guess @tabatkins already covered this in #862.]


#3

This is the main property I’d want to be gained from having this feature. Events that are based on selectors rather than adding/removing listeners directly fit two use cases (which are currently met for style needs by the selector-based Cascading Style Sheet system):

  • Allows for, essentially, registering prototypes for the document, based on classes and hierarchies. All future elements meeting a given selector pattern will automatically have the event apply.
  • Allows for registering modalities, where the selector pattern will match and events will apply only when a parent is in a certain state.

#4

Ik the following is not really the same, but I’m doubting it will actually happen since we have event delegation, and the matches() method:

document.body.addEventListener('click', function(evt) {
  if( evt.target.matches(selector) ) {
    // do something
  }
});

Use multiple if statements for multiple selectors.

You’re able to do it all in one event handler, and if you don’t want that just add more.

I probably shouldn’t promote my library here, but since it sort of relates to what you said @adamschwartz NodeList.js can do:

$$(selector).addEventListener('click', function(evt) {...});

It’s just not live


#5

An elegant solution, that flexes to more use-cases, would be to introduce a primitive for functional pseudos. Imagine if you could chain actions together in a generic and generative way:

new FunctionPseudo('delegate', function(fn, args, pseudo){
    if (!args[0].target.matches(pseudo.parameters[0])) {
        fn.apply(this, args);
    }
});

document.addEventListener('click', function `delegate('ul > li')` (e) { ... });

new FunctionPseudo('record', function(fn, args, pseudo){
  fetch(..., { recording: args });
  fn.apply(this, args);
});

function foo `record` (message){ alert(message); }

foo('bar') // both alerts and sends a post to record the message passed.

With Function Pseudos, you can even chain multiple actions together in a line of activities that can be assembled to form a more complex functional operation:

document.addEventListener('click', function `delegate('ul > li'):record` (e) { ... });

#6

That seems like an interesting ES7+ proposal which if you want, but with more proposal than the DOM, you could post at: https://esdiscuss.org


#7

I’m thinking it would be interesting to spec this. What would you call it? addSelectiveListener(selector, eventName, callback)?


#8

With the new ES2015/ES2016 features and the amazing async-csp, you can do the following, which is pretty clean:

import Channel from 'async-csp'

let elements = document.querySelectorAll('ul.things > li > a.delete-button')
let eventChannel = new Channel

[...elements].forEach(el => el.addEventListener('click', eventChannel.put))

// start an event loop (this is an immediately invoked async function)
~async function() {
    while (true) {
        let event = await eventChannel.take()
        let el = event.currentTarget
        // Handle delete
    }
}()

If you’re not using ES2016 async functions, you can also achieve the same with ES2015 generators (already supported natively in Chrome, Firefox, and Edge with preview mode!) and js-csp:

import {chan, put, take, go} from 'js-csp'

let elements = document.querySelectorAll('ul.things > li > a.delete-button')
let eventChannel = chan()

[...elements].forEach(el => el.addEventListener('click', event => put(eventChannel, event)))

// start an event loop (this is a generator function immediately invoked by the
// call to `go()`)
// Note: Although you can immediately invoke the generator youself, the `go()`
// wrapper is required for this to work
go(function*() {
    while (true) {
        let event = yield take(eventChannel)
        let el = event.currentTarget
        // Handle delete
    }
})

#9

I’m not talking about cleanliness of code, though (although that is a component of this proposal), I’m talking about avoiding large-scale ugly hacks to effectively duplicate the mechanisms of the browser’s selector engine in JS-land to handle events the way jQuery or D3 do.


#10

The problem with

document.addEventListener('click', function `delegate('ul > li')` (e) { ... });

is that the `delegate('ul > li')` portion is very unlikely to get accepted into JavaScript, because JavaScript is a language not specifically tied to DOM (as that expression implies). What does that mean in Node.js? What does that mean in Rhino? What does that mean in Qt’s JavaScript environment? A language feature like that, specific to DOM, isn’t likely to make it into the language. For the same reason, I believe Facebook’s JSX spec is unlikely to become part of the language.

On the other hand, if you can show how

function `foo`() {}

can be applied more generically, without specific ties to the environment that the language is used in, then that might have more chances. Can you come up with a generic useful ness of those backticks and examples not relating to DOM?


#11

Is this something like as proposed in @tabatkinshttps://tabatkins.github.io/specs/css-aliases/ ?


#12

“The problem with …DEMO… is that the delegate('ul > li') portion is very unlikely to get accepted into JavaScript, because JavaScript is a language not specifically tied to DOM” - this isn’t true, I believe you may be misunderstanding the code I wrote.

The Function Pseudo declaration is not bound to the DOM in any way. Function Pseudos would be a JS primitive that allows you do declare tokens that map to functions, which can be chained together, as seen in this example:

new FunctionPseudo('delegate', function(fn, args, pseudo){
    if (!args[0].target.matches(pseudo.parameters[0])) {
        fn.apply(this, args);
    }
});

new FunctionPseudo('once', function(fn, args, pseudo){
    // Prevent more than one call to the function chain
});

new FunctionPseudo('save', function(fn, args, pseudo){
    // Saves some aspect of the function call to somewhere else
});

document.addEventListener('click', function `once:save:delegate('ul > li')` (e) { ... });

Just because you touch the DOM or do a DOMy thing inside your Function Pseudo declaration doesn’t mean Function Pseudos, the JS primitive, are tied to the DOM. As you see in the example, I have chained the hypothetical pseudos once and save with delegate. The pseudo once would just execute any function once then prevent further calls - which has nothing to do with the DOM, and save can do whatever you want, maybe send a value to a server with an HTTP request.

This is no more DOM-bound than a traditional function. Just because you write function(){ ... } and happen to touch the DOM inside your function, does not mean the JS keyword function is tied to the DOM - understand what I mean?


#13

What Tab has proposed there is basically a CSS/DOM-centric API that does for CSS, in a similar style, what my proposed JS primitive would allow for any JS code, whether it lives in a browser, server, or other JS engine implementation.


#14

@trusktr I should also note that all delegate's Function Pseudo definition does is allow you to define the token delegate, and use whatever JS String is passed to it - in the case of the example, the code the user writes inside the definition is using the string as a CSS selector.

What you do with the arguments that are passed into your Function Pseudo is completely up to you.


#15

Ahhh, okay. That’s what I meant by when I said

if you can show how … DEMO … can be applied more generically…

which you did by describing Function Pseudos. Is that something you created? Does a proposal for function pseudos exist somewhere?


#16

Have you tried Atom (written with HTML/JavaScript/CSS)? It’s “command” system is exactly this, a selector-based event system, with selector-based key mappings for triggering those events (commands). How does that compare to what you’re thinking of here @stuartpb?

To try it out in Atom, what you can do is install a package (for example, the advanced-open-file package), then in the settings page for that package, look at the keybindings. The keybinding table shows you the command name and the selector associated with the keybinding. Those commands are already mapped to specific elements via selectors. Here’s how that is done in the advanced-open-file package, using atom.commands.add(). Then there’s also atom.commands.dispatch(), which can trigger those events imperatively if not using a keybinding. The first argument to atom.commands.add() can be an actual element, or a selector to match multiple elements (I’m not sure why the reciprocal atom.commands.dispatch() method doesn’t accept a selector, but if it did that might be nice too). For reference, here is Atom’s CommandRegistry class which is what atom.commands is an instance of. CommandRegistry is essentially an “event registry” if we rename it to match the idea here in this topic, but I think the concept with Atom’s CommandRegistry is basically what you describe here @stuartpb. The Atom API docs are good, be sure to check out the keymapping stuff to see how that ties together with commands (events).


#17

I’m just talking about the selector-based event model we’ve had for years with jQuery and its successive would-be usurpers. Atom might have something like it, but the definitive example is just a free-floating addEventListener where the first element is a CSS selector that listens to events from all matching elements. Just

EventTarget.prototype.whatStuartpbIsDescribing =
function addSelectiveEventListener(selector, type, listener, options) {
  return this.addEventListener(type,function selectorMatchFilter(event) {
    if (event.target.match(selector)) return listener.apply(this,arguments);
  }, options);
}

That’s all it is. Just that, as a native function. Maybe a corresponding function for removing them, too. None of this delegated functional pseudo package command registry mile-high sytems jargon. Just the two lines @Edwin_Reynoso posted a few hours after I opened the topic, running as optimized in-engine logic, instead of a bolted-on source of slowdown inconsistently implemented in black box scripts spread across half the pages on the web.


#18

I like this polyfill. And in my experience, to support selector-based event listeners, browser kernel engineers have to implement Element.match(CSSSelector) in native C++ to support live event trigger.


#19

Looping in the discussion that is happening on GitHub about this same concept (making it possible to add event listeners that are applied by selector).