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

Provide a mechanism for allowing EventTarget trees to make use of bubbling #1146

Closed
keithamus opened this issue Jan 24, 2023 · 2 comments
Closed

Comments

@keithamus
Copy link

Subclassable EventTargets (and Events) are very useful for creating ad-hoc event systems, but they come with some restrictions on feature set, namely propagation through a tree via capturing/bubbling.

When creating a tree structure of EventTarget subclasses, it can be useful to emit events that bubble through the tree but to do so is awkward if not impossible. Consider:

class MyNode extends EventTarget {
  children = []

  add(node) {
    this.children.push(node)
    node.dispatchEvent(new Event('added', { bubbles: true }))
  }

}

const root = new MyNode()

root.add(new MyNode())

Events cannot be re-dispatched while they are dispatching, so it is not possible to to alter add to be this:

  add(node) {
    this.children.push(node)
    // Bubble `added` events
    node.addEventListener('added', e => {
      if (e.bubbles) this.dispatchEvent(e)
    })
    node.dispatchEvent(new Event('added', { bubbles: true }))
  }

One way around this is to wait a Promise tick to allow the Event to settle, at which point its eventPhase will be 0 and it won't have a target, and it can be redispatched:

  add(node) {
    this.children.push(node)
    // Bubble `added` events
    node.addEventListener('added', async e => {
      await Promise.resolve()
      if (e.bubbles) this.dispatchEvent(e)
    })
    node.dispatchEvent(new Event('added', { bubbles: true }))
  }

This is far from ideal as this means the event propagation in this system is asynchronous. Asynchronous events mean preventDefault() would no longer work as intended, as well as myriad other external problems.

One possible solution that avoids asynchronous redispatches is to re-create the event each time, but this means additional work to capture data, and side effects like defaultPrevented:

  add(node) {
    this.children.push(node)
    // Bubble `added` events
    node.addEventListener('added', e => {
      if (e.bubbles) {
        const event = new Event('added', { bubbles: true })
        if (e.defaultPrevented) event.preventDefault()
        this.dispatchEvent(event)
       }
    })
    node.dispatchEvent(new Event('added', { bubbles: true }))
  }

Of course if the originally dispatched event is a subclass with different arguments then this system gets far more complex.

None of the above examples also handle stopping of propagation. Today, there is no (non deprecated) way to determine if an Event has had its propagation stopped (.cancelBubble can be checked but it's a deprecated field so should be avoided).

In addition, none of the examples I've included handle the capturing phase either (elided for brevity).


It would be great to introduce some kind of mechanism for EventTargets to associate with one another to allow for event bubbling and capturing. Perhaps EventTarget could have something like associateAncestor(ancestorEventTarget, { capture: true, bubble: true }) which could do the necessary internal wiring to make an event targets event capture & bubble upward to an ancestor? Existing EventTargets could throw when this is called, but it would allow for designs in userland to make use of a fairly large feature within EventTarget.

@domenic
Copy link
Member

domenic commented Jan 25, 2023

Dupe of #583, I'm pretty sure.

@domenic domenic closed this as completed Jan 25, 2023
@keithamus
Copy link
Author

Thanks, that's correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants