From Lit

Events

@event bindings don't change. The handler is an F# function: Ev gives it its event's type, and Event.customEvent builds a custom event that crosses shadow roots.

Listening in a template

Lit calls a listener method with this set to the element. In F#, pass the method itself: Ev.keyboard this.OnKey turns it into a function and tells F# the event is a KeyboardEvent.

Lit
import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';

@customElement('quick-add')
export class QuickAdd extends LitElement {
  @state() private items: string[] = [];

  private onKey(e: KeyboardEvent) {
    const input = e.target as HTMLInputElement;
    const text = input.value;
    if (e.key === 'Enter' && text !== '') {
      this.items = [...this.items, text];
      input.value = '';
    }
  }

  render() {
    const item = (i: string) => html`<li>${i}</li>`;
    return html`
      <input @keydown=${this.onKey}>
      <ul>${this.items.map(item)}</ul>`;
  }
}
Firelight
open Fable.Core
open Browser.Types
open Firelight
open type Firelight.Lit

[<AttachMembers>]
type QuickAdd() =
    inherit LitElement()

    static member properties =
        PropertyDeclarations.create [
            "items",
            PropertyDeclaration<string[]>(state = true)
        ]

    member val private items: string[] = [||] with get, set

    member private this.OnKey(e: KeyboardEvent) =
        let input = e.target :?> HTMLInputElement
        let text = input.value

        if e.key = "Enter" && text <> "" then
            this.items <- Array.append this.items [| text |]
            input.value <- ""

    override this.render() =
        let item i = html $"<li>{i}</li>"

        html $"""
        <input @keydown={Ev.keyboard this.OnKey}>
        <ul>{this.items |> Array.map item}</ul>"""

defineElement<QuickAdd> "quick-add"

Ev.mouse, Ev.pointer, Ev.focus, Ev.input and the others do the same for their event types. Ev.value and Ev.checked' pass the handler the field's value instead of the event; see Templates. Nothing checks the type against the event's name: @click={Ev.keyboard ...} compiles.

Dispatching a custom event

Event.customEvent sets bubbles and composed to true for you. this.dispatch is dispatchEvent without its result, whether the event went uncancelled, which F# would make you ignore. For an event a listener can cancel, pass cancelable = true and call dispatchEvent.

Lit
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('swatch-row')
export class SwatchRow extends LitElement {
  private pick(color: string) {
    this.dispatchEvent(new CustomEvent('color-picked', {
      detail: color, bubbles: true, composed: true,
    }));
  }

  render() {
    return ['red', 'teal'].map((c) => html`
      <button @click=${() => this.pick(c)}>${c}</button>`);
  }
}
Firelight
open Fable.Core
open Browser.Types
open Firelight
open type Firelight.Lit

[<AttachMembers>]
type SwatchRow() =
    inherit LitElement()

    member private this.Pick(color: string) =
        this.dispatch (Event.customEvent ("color-picked", color))

    override this.render() =
        let swatch c =
            let pick _ = this.Pick c
            html $"""<button @click={pick}>{c}</button>"""

        html $"""{[ "red"; "teal" ] |> List.map swatch}"""

defineElement<SwatchRow> "swatch-row"

render in TypeScript can return the array itself. F#'s render returns a ChildRenderable, which a list isn't, so the list goes in a template.

Listening for a custom event

Ev.custom types the event as a CustomEvent<'T>. Its detail is a 'T option, where TypeScript has a plain T.

Lit
import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';

@customElement('color-chooser')
export class ColorChooser extends LitElement {
  @state() private color = 'none';

  private onPicked(e: CustomEvent<string>) {
    this.color = e.detail;
  }

  render() {
    return html`
      <p>Picked: ${this.color}</p>
      <swatch-row @color-picked=${this.onPicked}>
      </swatch-row>`;
  }
}
Firelight
open Fable.Core
open Browser.Types
open Firelight
open type Firelight.Lit

[<AttachMembers>]
type ColorChooser() =
    inherit LitElement()

    static member properties =
        PropertyDeclarations.create [
            "color",
            PropertyDeclaration<string>(state = true)
        ]

    member val private color = "none" with get, set

    member private this.OnPicked(e: CustomEvent<string>) =
        e.detail |> Option.iter (fun c -> this.color <- c)

    override this.render() =
        html $"""
        <p>Picked: {this.color}</p>
        <swatch-row @color-picked={Ev.custom this.OnPicked}>
        </swatch-row>"""

defineElement<ColorChooser> "color-chooser"

Nothing checks that the event's detail is the type you name, in either language.

Listening on the element itself

The F# class has no addEventListener: Fable's HTMLElement is an interface, which LitElement can't inherit. this.element is the component as an HTMLElement, a cast that compiles to this.

Lit
import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';

@customElement('click-total')
export class ClickTotal extends LitElement {
  @state() private clicks = 0;

  constructor() {
    super();
    this.addEventListener('click', () => this.clicks++);
  }

  render() {
    return html`<slot></slot> (${this.clicks})`;
  }
}
Firelight
open Fable.Core
open Browser.Types
open Firelight
open type Firelight.Lit

[<AttachMembers>]
type ClickTotal() as this =
    inherit LitElement()

    do
        let count _ = this.Count()
        this.element.addEventListener ("click", count)

    static member properties =
        PropertyDeclarations.create [
            "clicks", PropertyDeclaration<int>(state = true)
        ]

    member val private clicks = 0 with get, set

    member private this.Count() =
        this.clicks <- this.clicks + 1

    override this.render() =
        html $"<slot></slot> ({this.clicks})"

defineElement<ClickTotal> "click-total"

as this names the instance in the constructor, where do runs.

No direct equivalent

The Events guide covers the Firelight side in more depth.