[whatwg/dom] Provide a mechanism for allowing EventTarget trees to make use of bubbling (Issue #1146)

Subclassable `EventTarget`s (and `Event`s) 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:

```js
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:

```js
  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:

```js
  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`:

```js
  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 `EventTarget`s could throw when this is called, but it would allow for designs in userland to make use of a fairly large feature within `EventTarget`.

-- 
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/dom/issues/1146
You are receiving this because you are subscribed to this thread.

Message ID: <whatwg/dom/issues/1146@github.com>

Received on Tuesday, 24 January 2023 16:44:36 UTC