WHATWG DOM Events

Elide implements the WHATWG [DOM Living Standard][spec] event interfaces as globals: Event, CustomEvent, and EventTarget. They are also returned by built-in sources (e.g. AbortSignal extends EventTarget).

[spec]: https://dom.spec.whatwg.org/

Event

js
new Event(type, eventInitDict = {})
// eventInitDict: { bubbles: boolean = false, cancelable: boolean = false, composed: boolean = false }
Calling Event(...) without new throws TypeError — per WebIDL, interface objects with constructors must be invoked with new. The required first argument is coerced via ToString; a missing argument throws TypeError.
MemberTypeNotes
typestringReadonly; coerced from the constructor argument.
target`EventTarget \null`Readonly; set during dispatch, null before / after.
currentTarget`EventTarget \null`Readonly; set during listener invocation only.
eventPhasenumberReadonly; NONE (0) or AT_TARGET (2) in this non-tree runtime.
bubbles / cancelable / composedbooleanReadonly; from the init dict, default false.
defaultPreventedbooleanReadonly; true after preventDefault() on a cancelable event.
isTrustedbooleanReadonly; always false for script-created events.
timeStampnumberReadonly; milliseconds since the Unix epoch at construction.
cancelBubblebooleanHistorical alias — getter returns the stop-propagation flag; setter with true calls stopPropagation(), false is a no-op.
composedPath()EventTarget[][currentTarget] during dispatch; [] otherwise (single-target runtime).
stopPropagation()undefinedSets the stop-propagation flag.
stopImmediatePropagation()undefinedSets both stop flags — later listeners on the same target are skipped.
preventDefault()undefinedNo-op if cancelable is false or the active listener is passive.
Phase constants are installed on both the constructor function and the prototype:
js
Event.NONE            === 0
Event.CAPTURING_PHASE === 1
Event.AT_TARGET       === 2
Event.BUBBLING_PHASE  === 3

CustomEvent

js
new CustomEvent(type, customEventInitDict = {})
// customEventInitDict: { bubbles, cancelable, composed, detail: any = null }
Subclass of Event: customEvt instanceof Event is true, and every Event member works on a CustomEvent. Adds one readonly attribute:
MemberTypeNotes
detailanyReadonly; default null. The value is stored by reference, not cloned.

EventTarget

js
new EventTarget()
js
target.addEventListener(type, callback, options = {})
target.removeEventListener(type, callback, options = {})
target.dispatchEvent(event) // → boolean (!event.defaultPrevented)
  • callback may be a function or any object exposing a callable handleEvent property. Functions are invoked with this = currentTarget; handleEvent objects with this = callback.
  • null / undefined callbacks are silently ignored.
  • options accepts { capture, passive, once, signal } (booleans; plus signal: AbortSignal) or a plain boolean (interpreted as the capture shortcut).
  • Duplicate (type, callback, capture) registrations are silently ignored.
  • passive: true listeners' calls to preventDefault() are no-ops.
  • once: true listeners are removed immediately after their first invocation.
  • dispatchEvent(event) throws InvalidStateError DOMException if the event is already being dispatched. Listeners added or removed during dispatch do not affect the current dispatch (a snapshot is taken at entry); they take effect on the next one.
  • Exceptions thrown by a listener are written to the realm's error stream and do not abort the remaining listeners (the DOM "report the exception" algorithm).

Non-browser scope

Elide does not have a DOM tree, so every dispatch has exactly one invocation target:

  • composedPath() returns [currentTarget] during dispatch and [] otherwise.
  • eventPhase transitions NONE → AT_TARGET → NONE.
  • Capture/bubble flags are recorded (and honoured for listener dedup / removal) but do not affect the invocation order on a single target.

The signal: AbortSignal option on addEventListener is wired up: passing an AbortSignal as options.signal registers the listener normally and automatically removes it when the signal aborts. Passing an already-aborted signal skips registration entirely. Non-AbortSignal values are ignored, matching the behaviour of the AbortSignal option documented in WHATWG DOM Abort.

Example

js
const bus = new EventTarget();

bus.addEventListener('tick', (ev) => {
  console.log(ev.type, ev.detail);       // "tick" 42
});

bus.dispatchEvent(new CustomEvent('tick', { detail: 42 }));

// once + cancelable + preventDefault
const target = new EventTarget();
target.addEventListener('submit', (ev) => ev.preventDefault(), { once: true });
const ok = target.dispatchEvent(new Event('submit', { cancelable: true }));
console.log(ok);                         // false  — cancelled
console.log(target.dispatchEvent(new Event('submit', { cancelable: true })));
// true — the `once` listener is gone

Notes on the implementation

  • Plain-call Event(...) / CustomEvent(...) / EventTarget(...) are routed to TypeError in the generated JSEventClassBase.java / JSCustomEventClassBase.java / JSEventTargetClassBase.java under packages/generated/, with the same manual patch Elide uses for DOMException and the encoding classes. Upstream elide-dev/bindgen (JSConstructorCodegen.kt) emits the same branch on regeneration.
  • The shared per-event state (type, flags, targets, dispatch phase) lives in EventState, which is referenced by both EventInstance extends JSEventObject and CustomEventInstance extends JSCustomEventObject. The two concrete types implement an EventAccess interface so the prototype operations on JSEvent work against either without duplicated logic.
  • Listener lists are kept per event type in an insertion-ordered ArrayList. The dispatch loop iterates a snapshot taken at entry, so mutations inside a listener don't disturb the current round.