Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Elm-Style State

nano-ui-emit is the reducer-based alternative to Var: the view emits typed messages, and a pure reducer folds them into a model.

data Model = Model {counter :: Int, name :: Text}
  deriving Eq

data Msg = Increment | Decrement | SetName Text

view :: Model -> NanoUIE Msg ()
view m = column $ do
  heading "Profile"
  row $ do
    emitWhen (button "-") Decrement
    label (T.pack (show m.counter))
    emitWhen (button "+") Increment
  emitChanged textInput m.name SetName

update :: Msg -> Model -> Model
update Increment m = m {counter = m.counter + 1}
update Decrement m = m {counter = m.counter - 1}
update (SetName n) m = m {name = n}

main :: IO ()
main = runSdlAppReduce defaultSdlOptions update (Model 0 "Ada") view

Widgets, layout, focus, and keys are unchanged. Only the state model differs.

Emitting

NanoUIE msg a is a UI action that can emit only msg. There is no Typeable constraint and no runtime type test: emitting the wrong type is a compile error.

A NanoUIE view is an ordinary NanoUI action with the view’s message sink in scope (an implicit parameter, ?emit). Every core widget runs in it unchanged, so NanoUI.Emit re-exports NanoUI and adds the functions below. An emit inside a container, an overlay body, a tab or a menu callback goes to the sink of the view around it.

FunctionEmits when
emit msgImmediately
emitWhen widget msgThe widget returns True (button click, submitted field)
emitChanged widget old toMsgA Var a -> NanoUI r widget’s final value differs from old
emitEdited widget old toMsgThe widget’s value changed and respChanged is true
settingsForm :: Float -> NanoUIE Msg ()
settingsForm volume =
  column $ do
    label "Settings"
    withKey ("form" :: Text) $ do
      emitChanged (slider 0 100) volume SetVolume
      emitWhen (button "Save") Save

emitChanged runs the widget against a temporary Var seeded with the model value and emits the new value only if it differs. emitEdited is its stricter form for text fields, where programmatic writes should not look like edits.

Give each view a signature in its own message type, view :: Model -> NanoUIE Msg (). The runners take a view of that shape, and a local helper defined outside a view needs the signature too, or GHC finds no sink for its emit.

Caution

Emit only inside an event branch. emit Save at the top level of a view emits on every pass, and the reducer runs every message. The adapters above already guard on the widget’s event.

Messages are collected across every pass of a frame, including local-Var rebuilds, and reduced at frame end in emission order.

Components

mapMessages embeds a child view whose message type is smaller than its parent’s:

data ChildMsg = ChildIncrement | ChildReset

data ParentMsg = Child ChildMsg | SaveAll

childView :: Int -> NanoUIE ChildMsg ()
childView n = row $ do
  emitWhen (button "+") ChildIncrement
  label (T.pack (show n))
  emitWhen (button "Reset") ChildReset

parentView :: Int -> NanoUIE ParentMsg ()
parentView n = column $ do
  mapMessages Child (childView n)
  emitWhen (button "Save All") SaveAll

Messages go straight to the parent’s sink in traversal order, with no intermediate queue. mapMessages changes only the message type and leaves widget ids unchanged, so a list still needs withKey around each component, exactly as a Var-based row does.

For local UI state, keep using Var inside the component; the durable model stays in NanoUIE.

Running and testing

runSdlAppReduce and runRgfwAppReduce take an update function, an initial model, and a view:

main :: IO ()
main = runSdlAppReduce defaultSdlOptions update initialModel view

The view runs with the current model, messages are reduced at frame end, and the drawing shown for that frame is from before the reduction. If the model changed, another frame is requested to draw it.

For tests and embedded uses:

FunctionReturns
runNanoUIE viewNanoUI (result, [msg])
runFrameE ctx input viewIO (result, [msg], DrawData, Bool)
runFrameReduce update ctx input model viewIO (result, model, [msg], DrawData, Bool)
runClickReduce update ctx input model view posIO (model, [msg], Bool)

runFrameReduce requests a follow-up frame when the reduced model differs. reduceMessages and reduceUpdates fold message lists.

When to use it

Var-firstElm-style
State shapeDistributed through the view and app recordOne model value
TransitionsDirect mutationExplicit Msg and one update
TestingWidget-level with NanoUI.TestingPure update tests plus frame tests
Local widget stateNaturalStill use Var for drafts and open flags
Best forTools and editors with external stateState machines, wizards, undo/history, small apps with a clear model

The styles mix. An emitting view can keep Vars for transient UI state; a Var-based view can run a small component with runNanoUIE and act on the returned messages.

Keep update pure: file and network I/O belong in the view’s event branch, or in an effect queue run outside the reducer. Do not mirror model state in Vars to read it a frame earlier; keep durable state in the model only.