From Lit

Components

A component is still a class that extends LitElement. In F#, registration becomes a function call and the property decorators become one static member, as in Lit without decorators.

Defining a component

defineElement takes the place of @customElement, and [<AttachMembers>] makes Fable compile the members onto the JavaScript class, where Lit finds them.

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

@customElement('hello-badge')
export class HelloBadge extends LitElement {
  render() {
    return html`<span>Hello</span>`;
  }
}
Firelight
open Fable.Core
open Firelight
open type Firelight.Lit

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

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

defineElement<HelloBadge> "hello-badge"

defineElement registers the element when the module loads, like the decorator. Lit without decorators does the same with customElements.define('hello-badge', HelloBadge).

Rendering

render works as in Lit. A helper that only builds a template doesn't need to be a method: make it a function outside the class.

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

@customElement('order-total')
export class OrderTotal extends LitElement {
  @property({ attribute: false }) prices: number[] = [];

  private line(price: number) {
    return html`<li>${price.toFixed(2)}</li>`;
  }

  render() {
    const total = this.prices.reduce((a, b) => a + b, 0);
    return html`
      <ul>${this.prices.map((p) => this.line(p))}</ul>
      <p>Total: ${total.toFixed(2)}</p>`;
  }
}
Firelight
open Fable.Core
open Fable.Core.JsInterop
open Firelight
open type Firelight.Lit

let line (price: float) =
    html $"""<li>{price.ToString "F2"}</li>"""

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

    static member properties =
        PropertyDeclarations.create [
            "prices",
            PropertyDeclaration<float[]>(
                attribute = false)
        ]

    member val prices: float[] = [||] with get, set

    override this.render() =
        let total = Array.sum this.prices

        html $"""
        <ul>{this.prices |> Array.map line}</ul>
        <p>Total: {total.ToString "F2"}</p>"""

defineElement<OrderTotal> "order-total"

ToString "F2" compiles to the same fixed-point formatting as toFixed(2). A string inside a hole needs the template in triple quotes, $""".

A property that JavaScript code sets should be an F# array, which is a JavaScript array. An F# list is a Fable class, so code outside F# can't pass one. Between F# components, a list works well: a hole renders it as it renders an array.

Templates covers expressions, conditionals and lists.

Reactive properties

Each @property() becomes an entry in static member properties, named by a string, and a member val with the property's default value.

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

@customElement('stock-meter')
export class StockMeter extends LitElement {
  @property() label = 'Stock';
  @property({ type: Number }) level = 0;
  @property({ type: Boolean, reflect: true })
  low = false;

  render() {
    return html`${this.label}:
      <meter max="100" value=${this.level}></meter>`;
  }
}
Firelight
open Fable.Core
open Fable.Core.JsInterop
open Firelight
open type Firelight.Lit

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

    static member properties =
        PropertyDeclarations.create [
            "label", PropertyDeclaration<string>()
            "level", PropertyDeclaration<float>()
            "low",
            PropertyDeclaration<bool>(
                reflect = true)
        ]

    member val label = "Stock" with get, set
    member val level = 0.0 with get, set
    member val low = false with get, set

    override this.render() =
        html $"""{this.label}:
          <meter max="100" value={this.level}></meter>"""

defineElement<StockMeter> "stock-meter"

Lit reads type to convert an attribute, as in TypeScript. PropertyDeclaration<'T> sets it from 'T: Number for float, int and the other numeric types, Boolean for bool. So <stock-meter low> sets low to true, and level="0.5" sets the number 0.5. Other types get no type; pass ``type`` (type is an F# keyword) to choose one yourself.

A JavaScript number is a float. An int member's setter truncates what it's given, so level="40.5" would become 40.

Attribute names

attribute takes a string or false, as in TypeScript.

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

@customElement('tag-list')
export class TagList extends LitElement {
  @property({ attribute: 'empty-text' })
  emptyText = 'No tags';
  @property({ attribute: false }) tags: string[] = [];

  render() {
    if (this.tags.length === 0) {
      return html`<i>${this.emptyText}</i>`;
    }
    const tag = (t: string) => html`<b>${t}</b> `;
    return html`${this.tags.map(tag)}`;
  }
}
Firelight
open Fable.Core
open Fable.Core.JsInterop
open Firelight
open type Firelight.Lit

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

    static member properties =
        PropertyDeclarations.create [
            "emptyText",
            PropertyDeclaration<string>(
                attribute = "empty-text")
            "tags",
            PropertyDeclaration<string[]>(
                attribute = false)
        ]

    member val emptyText = "No tags" with get, set
    member val tags: string[] = [||] with get, set

    override this.render() =
        if Array.isEmpty this.tags then
            html $"<i>{this.emptyText}</i>"
        else
            let tag t = html $"<b>{t}</b> "
            html $"{this.tags |> Array.map tag}"

defineElement<TagList> "tag-list"

Internal state

@state() is state = true. A private member val keeps its name in JavaScript, so Lit still finds it. Lit calls a listener method with this set to the element; in F#, bind a lambda that calls the method.

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

@customElement('more-text')
export class MoreText extends LitElement {
  @state() private expanded = false;

  private toggle() {
    this.expanded = !this.expanded;
  }

  render() {
    return html`
      <button @click=${this.toggle}>
        ${this.expanded ? 'Less' : 'More'}
      </button>
      ${this.expanded ? html`<slot></slot>` : nothing}`;
  }
}
Firelight
open Fable.Core
open Firelight
open type Firelight.Lit

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

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

    member val private expanded = false with get, set

    member private this.Toggle() =
        this.expanded <- not this.expanded

    override this.render() =
        let body =
            if this.expanded then html $"<slot></slot>"
            else nothing

        html $"""
        <button @click={fun _ -> this.Toggle()}>
            {if this.expanded then "Less" else "More"}
        </button>
        {body}"""

defineElement<MoreText> "more-text"

Every option keeps its name, as an optional argument of PropertyDeclaration:

Lit Firelight
{ attribute: 'empty-text' } attribute = "empty-text"
{ type: Number } Nothing, for a numeric 'T; or type= jsConstructor<Globals.Number>
{ reflect: true } reflect = true
{ state: true } state = true
{ hasChanged: (v, old) => ... } hasChanged = PropertyDeclaration.HasChanged(fun v old -> ...)
{ noAccessor: true } noAccessor = true
{ useDefault: true } useDefault = true
{ converter: { fromAttribute, toAttribute } } converter = AttributeConverter(fromAttribute = ..., toAttribute = ...)

No direct equivalent

Mistakes that compile

You wrote What happens Write instead
No [<AttachMembers>] Renders once, then never updates: Lit can't see the properties [<AttachMembers>] on every component
"nmae", PropertyDeclaration<string>() Setting name doesn't re-render The member val's exact name
PropertyDeclaration<int64>() or <decimal> for an attribute The property holds the attribute's text: Lit's default converter has no 64-bit or decimal type A converter that parses it
let mutable count changed in a handler Nothing re-renders A declared property, or this.requestUpdate()

The Properties and attributes guide covers the Firelight side in more depth.