Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow custom activation behavior to be added to an event #1320

Open
jakearchibald opened this issue Oct 31, 2024 · 6 comments
Open

Allow custom activation behavior to be added to an event #1320

jakearchibald opened this issue Oct 31, 2024 · 6 comments
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: events

Comments

@jakearchibald
Copy link
Collaborator

jakearchibald commented Oct 31, 2024

What problem are you trying to solve?

const button = document.querySelector('button');

button.addEventListener('click', (event) => {
  // …
});

I want to add activation behavior to the above button, such as "when this button is clicked, toggle this checkbox". As expected on the platform, I don't want this action to happen if a listener calls event.preventDefault().

What solutions exist today?

const button = document.querySelector('button');

button.addEventListener('click', (event) => {
  if (event.defaultPrevented) return;
  checkCustomCheckbox();
});

This isn't great because the default may be prevented by a listener further along the path.

const button = document.querySelector('button');

button.addEventListener('click', (event) => {
  setTimeout(() => {
    if (event.defaultPrevented) return;
    checkCustomCheckbox();
  }, 0);  
});

This isn't great because the effect may happen a frame later (rAF isn't a reliable solution here due to whatwg/html#10113). It's also observably async:

button.click();
console.log(customCheckboxChecked); // expected true, but got false

Other techniques, like adding listeners to window may be missed due to stopPropagation(), and may still be 'beaten' by another listener.

How would you solve it?

Something like:

const button = document.querySelector('button');

button.addEventListener('click', (event) => {
  event.addActivationBehavior(() => {
    checkCustomCheckbox();
  });
});

Anything else?

No response

@jakearchibald jakearchibald added needs implementer interest Moving the issue forward requires implementers to express interest addition/proposal New features or enhancements labels Oct 31, 2024
@annevk
Copy link
Member

annevk commented Nov 4, 2024

In the specification activation behavior is associated with the event target, not the event. And it's very much tied to click events as well.

(This reminds me a bit of the "event group" concept in https://www.w3.org/TR/2003/NOTE-DOM-Level-3-Events-20031107/events.html#Event-groups.)

cc @smaug---- @rniwa

@smaug----
Copy link
Collaborator

Gecko has a variant of "event group", so called system event group, and listeners in that are called after the normal DOM event dispatch. (And it was also exposed to XBL)
But I wouldn't add anything like that to the web, too complicated.

EventTargets could perhaps have some callback for activation behavior, and that could either override the native activation behavior, or other option is to control that using .preventDefault() if the callback gets the event as param. Certain cases like nesting inside an anchor element might complicate things - which all activation behaviors should be triggered?

@jakearchibald
Copy link
Collaborator Author

EventTargets could perhaps have some callback for activation behavior, and that could either override the native activation behavior

Yeah, I like that idea.

or other option is to control that using .preventDefault() if the callback gets the event as param

I don't think that works, because you want the activation behaviour to not-run if the default is prevented.

@Krinkle
Copy link
Member

Krinkle commented Nov 5, 2024

For input fields and forms, we have "change" and "submit" events as happening after processing a lower level event like click, keydown, keyup, Enter, drag-and-drop, clipboard paste, etc.

What if anchor elements emitted an event like "navigate" or "activate" which wouldn't be cancellable, to happen when performing the action, regardless of how it was triggered and whether it was cancelled?

  • This would have the benefit of allowing you to listen declaratively (instead of attaching it just-in-time).
  • Naturally sharing and reusing the function in memory.
  • Allows for delegation, to listen from a parent or ancestor element for this event across a wider range of elements, without needing to select and attach on each one.
  • Simpler API for beginners, by using the event model people know instead of having to know about this specific method.

@jakearchibald
Copy link
Collaborator Author

For input fields and forms, we have "change" and "submit"

"submit" is another problem event here, as it can be cancelled.

@Krinkle
Copy link
Member

Krinkle commented Nov 13, 2024

@jakearchibald I see. Another example is HTMLElement.click() on a <summary> element, in relation to the toggle event on the parent <details> element. The toggle event is non-cancellable and emitted after, during the activation (but before the next frame, I think?).

I suspect we would see people use event.addActivationBehavior() also on non-submit buttons and JS-only forms, where there is no default behaviour, as a way to wait for the user's JS application to have finished doing something and thus register a "late" or "after" listener. Is that something we want to encourage? Basically a way to express priority or ordering between event handlers.

I see this come up a fair bit in developer code at Wikimedia, where event+timeout is used as a way to plug into something at a point where there is no DOM event or app-provided callback (yet). Even more so in asynchronous code, where the synchronous assumptions by event handlers (apart from eg. in Service Workers), would be insufficient anyway. Because fireEvent naturally would not wait for the completion of any async addEventListener(async function…). In this case we also tend to see, eventually, given a large enough code base, code that nests two setTimeout's in order to implicitly wait for another handler that does one setTimeout and/or is async. It's an arms race.

For simple one-to-one and synchronous use cases, event.addActivationBehavior() would allow decoupling and coorporation between event handlers which is great. For the more complex cases, it seems event.addActivationBehavior() might underfit. B would perhaps be coupling and assuming specific things about what A will have done by then. This would be an unstable contract and likely insufficient. It's not uncommon to see B "chase" A (e.g. attach to click, change, submit, and whatever else may trigger the same application behaviour). In such a situation, would we recommend developers instead emit their own non-cancellable DOM event and/or JS callback, to represent the logical application behaviour?

If so, I wonder if the the platform should follow that pattern too? We have this for toggle and for a few other things like it. I would consider pageshow, pagehide, unload to fit in this category as well. They emit regardless of how something it happens, it just happens. I can't think of a good name for click and submit activations, but perhaps a non-cancellable activateclick and activatesubmit event would make sense (better name welcome!). This would behave very similar to your example, except for the callstack which would presumably be after button.click() has completed in the next tick. Or not? I mean, nested events do exist right? E.g. we have change/blur/focus which afaik involve some nesting where a single behaviour trigger ends up synchronously emitting multiple events. Either way makes sense, but if it's a requirement that it happen as part of element.click() then perhaps that would be the better choice.

The reason I'm saying all this, is because I think re-using the event system is easier to learn, easier to extend and adopt for applications with their own events, scales to async behaviours, and perhaps also easier to integrate into frameworks and abstractions that already understand events.

I also see value in being able to listen to these through propagation, without needing to attach a synchronous listener to (one or more) possible earlier events that lead to the activation. Think about scroll events and the jank they caused. In other words addActivationBehavior() during click, vs listening for activateclick. It's probably a micro-optimisation in the case of click, but I like the declarative nature of this and the performance potential it has.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: events
Development

No branches or pull requests

4 participants