#7695·fast

feat: add event listener options to declarative templates

Author: janechuCreated Sep 10, 2026Updated Sep 10, 2026
Labelsfeaturestatus:plannedarea:fast-elementarea:ssrfast-element-v3

Feature Request

Allow declarative <f-template> event bindings to specify AddEventListenerOptions, especially capture, while preserving server rendering and hydration behavior.

Expected Behavior

Declarative event bindings should support static listener options comparable to the imperative listener(expression, options) API. One possible syntax would be:

xml
<div
  @pointerenter.capture="{handlePointerEnter($e)}"
  @pointerleave.capture="{handlePointerLeave($e)}"
  @wheel.passive="{handleWheel($e)}"
  @animationend.once="{handleAnimationEnd($e)}"
></div>

The exact syntax is open to design, but capture, passive, and once should be representable. FAST should continue to own listener cleanup when the view unbinds.

Current Behavior

FAST 3 declarative templates support client-only event bindings such as @click="{handleClick($e)}", but the declarative parser creates the event binding without listener options. Capture-phase delegation therefore cannot be expressed. Consumers must attach listeners manually in connectedCallback() with an AbortController, or duplicate handlers on every relevant descendant.

The runtime already supports this: listener(expression, options?: AddEventListenerOptions) stores options on the binding, and HTMLBindingDirective.bind() passes dataBinding.options to addEventListener(). The missing piece appears to be declarative syntax and parser/schema support.

Possible Solution

Support static event modifiers such as @pointerenter.capture and combinations such as @wheel.capture.passive. The parser could translate recognized modifiers into AddEventListenerOptions; unknown modifiers should produce a clear template error.

Context

A component delegates pointerenter and pointerleave from shadow-root content so dynamically repeated descendants can request menu data and coordinate a flyout. These events do not bubble, so the root listener requires { capture: true }. Declarative <f-template> markup is required because the same template is consumed by WebUI server rendering and FAST hydration. Replacing it with an imperative TypeScript html template solely to call listener(..., { capture: true }) would give up that pipeline.

Document-level outside-click listeners are a separate concern; this request is specifically about options for events whose target is represented in the declarative template.

Examples

Current workaround:

typescript
public connectedCallback(): void {
  super.connectedCallback();
  this.abortController = new AbortController();
  this.shadowRoot?.addEventListener(
    "pointerenter",
    this.handlePointerEnter,
    { capture: true, signal: this.abortController.signal }
  );
}

public disconnectedCallback(): void {
  this.abortController?.abort();
  super.disconnectedCallback();
}

Desired outcome: keep the listener on the template root, retain capture semantics for descendants, and let FAST bind and unbind it with the view.