Packages

Firelight.Elmish

Run the Model-View-Update loop inside a component: one immutable model, one pure update function.

Why use it

As a component's state grows, mutable fields changed from different event handlers get hard to follow. Elmish keeps all of the state in one immutable model, and every change goes through a single update function: given a message and the current model, it returns the next model. That function is plain F#, with no DOM or Lit in it, so it's easy to read and to test.

ElmishController connects the loop to a Lit component. It's a reactive controller: init runs when the component is created, and each new model renders the component. Use ElmishController.withCmds for side effects such as HTTP requests, or pass a full Elmish Program for subscriptions and tracing.

Example

Because every model is an immutable value, undo is just a list of the earlier ones. Nothing in the component has to know how to reverse a change.

module Snippets.UndoCounter

open Fable.Core
open Firelight
open Firelight.Elmish
open type Firelight.Lit

// The whole state is one immutable value, so undo is just keeping the earlier values.
type Model = { Count: int; History: int list }

type Msg =
    | Increment
    | Decrement
    | Undo

let init () = { Count = 0; History = [] }

let update msg model =
    match msg with
    | Increment ->
        {
            Count = model.Count + 1
            History = model.Count :: model.History
        }
    | Decrement ->
        {
            Count = model.Count - 1
            History = model.Count :: model.History
        }
    | Undo ->
        match model.History with
        | previous :: older -> { Count = previous; History = older }
        | [] -> model

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

    let elmish = ElmishController.simple this init update

    static member styles =
        css
            $$"""
        :host { display: inline-flex; align-items: center; gap: 0.75rem; }
        button { font: inherit; padding: 0.4rem 0.9rem; cursor: pointer; }
        output { min-width: 2ch; text-align: center; font-weight: 600; }
        """

    override _.render() =
        let model = elmish.model

        html
            $"""
        <button @click={fun _ -> elmish.dispatch Decrement}>−</button>
        <output>{model.Count}</output>
        <button @click={fun _ -> elmish.dispatch Increment}>+</button>
        <button ?disabled={model.History.IsEmpty} @click={fun _ -> elmish.dispatch Undo}>Undo</button>"""

defineElement<UndoCounter> "my-undo-counter"

For a larger example, the Todo demo runs one Elmish loop and shares its state with each item through context.

Install

dotnet add package Firelight.Elmish