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