From Lit

Signals

signal and computed keep their names. SignalWatcher(LitElement) becomes a registration call, LitSignals.defineElement, because F# can't apply a mixin to a base class.

Watching signals

A component registered with LitSignals.defineElement re-renders when a signal it read in render changes, as a SignalWatcher class does.

Lit
import { LitElement, html } from 'lit';
import {
  SignalWatcher, computed, signal,
} from '@lit-labs/signals';

const count = signal(0);
const doubled = computed(() => count.get() * 2);

export class CountButton extends SignalWatcher(LitElement) {
  render() {
    const add = () => count.set(count.get() + 1);
    return html`
      <button @click=${add}>${count.get()}</button>`;
  }
}
customElements.define('count-button', CountButton);

export class CountDouble extends SignalWatcher(LitElement) {
  render() {
    return html`<p>Doubled: ${doubled.get()}</p>`;
  }
}
customElements.define('count-double', CountDouble);
Firelight
open Fable.Core
open Firelight
open Firelight.Signals
open type Firelight.Lit

let count = LitSignals.signal 0
let doubled =
    LitSignals.computed (fun () -> count.get () * 2)

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

    override _.render() =
        let add _ = count.set (count.get () + 1)
        html $"<button @click={add}>{count.get ()}</button>"

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

    override _.render() =
        html $"<p>Doubled: {doubled.get ()}</p>"

LitSignals.defineElement<CountButton> "count-button"
LitSignals.defineElement<CountDouble> "count-double"

The two elements share no parent, property or event, only the signals. Options go in SignalOptions: signal(0, { equals: (a, b) => ... }) is signal (0, SignalOptions(equals = SignalEquals(fun a b -> ...))).

Updating only what changed

The html from @lit-labs/signals watches each signal placed in a hole, so a change updates that part of the template without running render again. In F# it's LitSignals.html, which open type LitSignals brings into scope as html.

Lit
import { LitElement } from 'lit';
import {
  SignalWatcher, html, signal,
} from '@lit-labs/signals';

const clicks = signal(0);

export class ClickPanel extends SignalWatcher(LitElement) {
  render() {
    const add = () => clicks.set(clicks.get() + 1);
    return html`
      <button @click=${add}>+1</button>
      <p>Clicks: ${clicks}</p>`;
  }
}
customElements.define('click-panel', ClickPanel);
Firelight
open Fable.Core
open Firelight
open Firelight.Signals
open type LitSignals

let clicks = signal 0

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

    override _.render() =
        let add _ = clicks.set (clicks.get () + 1)

        html $"""
        <button @click={add}>+1</button>
        <p>Clicks: {clicks}</p>"""

defineElement<ClickPanel> "click-panel"

With Lit's own html, watch does the same for one hole: {watch clicks}, as ${watch(clicks)}.

Effects

updateEffect runs a function after the element's next update, and again whenever a signal it read changes. It's a method of a SignalWatcher element in TypeScript; in F# it takes the element as its first argument.

Lit
import { LitElement, html } from 'lit';
import { SignalWatcher, signal } from '@lit-labs/signals';

const unread = signal(3);

export class TitleBadge extends SignalWatcher(LitElement) {
  connectedCallback() {
    super.connectedCallback();
    this.updateEffect(() => {
      document.title = `(${unread.get()}) Inbox`;
    });
  }

  render() {
    const read = () => unread.set(0);
    return html`<button @click=${read}>Mark read</button>`;
  }
}
customElements.define('title-badge', TitleBadge);
Firelight
open Fable.Core
open Browser
open Firelight
open Firelight.Signals
open type Firelight.Lit

let unread = LitSignals.signal 3

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

    override this.connectedCallback() =
        base.connectedCallback ()

        LitSignals.updateEffect (this, fun () ->
            document.title <- $"({unread.get ()}) Inbox")
        |> ignore

    override _.render() =
        let read _ = unread.set 0
        html $"<button @click={read}>Mark read</button>"

LitSignals.defineElement<TitleBadge> "title-badge"

updateEffect returns a function that disposes the effect. Lit disposes it when the element disconnects, so the F# side ignores it, as the TypeScript does.

No direct equivalent

The Firelight.Signals page has a live example. @lit-labs/signals is a Lit Labs package and still experimental.