From Lit

Composition

A reactive controller is an F# class that implements ReactiveController and adds itself to its host, as in Lit. Mixins don't carry over: F# can't build a class from a function, so shared behaviour goes in a base class or a controller.

Composing components, with child elements in a template, properties passed down and events sent up, works as in Lit: see Components, Events and the Component communication guide.

Reactive controllers

The F# controller implements all four callbacks, where TypeScript leaves out the ones it doesn't need. as this lets the constructor add the controller to its host.

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

export class WidthController implements ReactiveController {
  width = 0;

  constructor(private host: ReactiveControllerHost) {
    host.addController(this);
  }

  private onResize = () => {
    this.width = window.innerWidth;
    this.host.requestUpdate();
  };

  hostConnected() {
    this.width = window.innerWidth;
    this.host.requestUpdate();
    window.addEventListener('resize', this.onResize);
  }

  hostDisconnected() {
    window.removeEventListener('resize', this.onResize);
  }
}

@customElement('width-label')
export class WidthLabel extends LitElement {
  private size = new WidthController(this);

  render() {
    const wide = this.size.width >= 600;
    const label = wide ? 'Wide' : 'Narrow';
    return html`${label} window`;
  }
}
Firelight
open Fable.Core
open Browser
open Browser.Types
open Firelight
open type Firelight.Lit

type WidthController(host: ReactiveControllerHost) as this =
    inherit ReactiveControllerBase()

    let mutable width = 0.0
    let mutable stop = ignore

    do host.addController this

    member _.Width = width

    member private _.Listen() =
        let onResize (_: Event) =
            width <- window.innerWidth
            host.requestUpdate ()

        width <- window.innerWidth
        host.requestUpdate ()
        stop <- Ev.listen window "resize" onResize

    override this.hostConnected() = this.Listen()
    override _.hostDisconnected() = stop ()

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

    let size = WidthController(this)

    override _.render() =
        let wide = size.Width >= 600.0
        let label = if wide then "Wide" else "Narrow"
        html $"{label} window"

defineElement<WidthLabel> "width-label"

ReactiveControllerBase stands for implements ReactiveController: override the callbacks you need, and the others do nothing. The controller doesn't need [<AttachMembers>]: Lit calls only the four callbacks, which the base class implements under their own names. The listener is added with Ev.listen when the host connects, and its remover kept, as on Lifecycle.

Mixins

A mixin adds the same members to classes with different bases. F# has one base class per class, so a behaviour you'd write as one mixin becomes a base class. Several, combined freely, become controllers.

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

type Constructor<T = {}> = new (...args: any[]) => T;

const Highlightable = <T extends Constructor<LitElement>>(
  Base: T,
) => {
  class Highlighted extends Base {
    static styles = css`
      :host([highlighted]) { background: #fff3bf; }
    `;

    @property({ type: Boolean, reflect: true })
    highlighted = false;
  }
  return Highlighted;
};

@customElement('tip-text')
export class TipText extends Highlightable(LitElement) {
  render() {
    return html`<slot></slot>`;
  }
}
Firelight
open Fable.Core
open Fable.Core.JsInterop
open Firelight
open type Firelight.Lit

[<AbstractClass; AttachMembers>]
type Highlightable() =
    inherit LitElement()

    static member styles =
        css $$"""
        :host([highlighted]) { background: #fff3bf; }
        """

    static member properties =
        PropertyDeclarations.create [
            "highlighted",
            PropertyDeclaration<bool>(
                reflect = true)
        ]

    member val highlighted = false with get, set

[<AttachMembers>]
type TipText() =
    inherit Highlightable()

    override _.render() = html $"<slot></slot>"

defineElement<TipText> "tip-text"

A subclass can declare static member properties of its own; Lit merges them with the base class's, as it does in TypeScript.

No direct equivalent

Firelight packages bind several of Lit's controllers: ContextProvider and ContextConsumer (Context), Task as LitTask (Tasks), the resize, intersection, mutation and performance controllers in Firelight.Observers, and the animation controllers in Firelight.Motion. The Controllers guide covers writing your own.