Skip to main content
On this pageFunctions

Submodel

Functions

defineView

functionsource
/**
 * Defines the view function of a Submodel, a child component embedded
 *  via `h.submodel`.
 * 
 *  Use this ONLY for views that will be embedded via `h.submodel`. Plain
 *  view functions (page-level render functions, helper render functions
 *  that compose Html, etc.) don't need to be defined this way. Write
 *  them as ordinary `(model) => Html` functions.
 * 
 *  Explicit type arguments are required because Message has no
 *  inferable source on the function signature itself.
 */
<Model, Message, ViewInputs = void>(fn: ViewInputs extends void
  ? (model: Model) => VNode | null
  : (model: Model, viewInputs: ViewInputs) => VNode | null): SubmodelView<Model, Message, ViewInputs>

Types

Config

typesource
/**
 * Configuration for embedding a child Submodel into a parent's view.
 * 
 *  - `slotId`: unique identifier for this Submodel instance under the
 *    current boundary. Name the slot semantically (e.g.
 *    `'sidebar-group'`). For lists, use a stable per-item id (typically
 *    `entry.id`), not the array index. If the same model is rendered in
 *    two DOM positions (desktop + mobile, master + detail), each slot
 *    needs its own id (e.g. `'desktop-sidebar-group'`,
 *    `'mobile-sidebar-group'`). Two `h.submodel` calls inside the same
 *    parent boundary with the same `slotId` throw at view-build time,
 *    including across `createLazy`/`createKeyedLazy` cache hits.
 *  - `view`: the child's `SubmodelView`. Must be branded via
 *    defineView so `h.submodel` can infer the child's Message
 *    type. Unbranded plain functions fail to type-check here.
 *  - `model`: the child's model, inferred from `view`'s first parameter.
 *    Compared by `===` when the boundary is wrapped in a memoizing
 *    helper such as `createKeyedLazy`.
 *  - `viewInputs`: optional second-argument data passed to `view`,
 *    inferred from `view`'s second parameter. Function values AT THE TOP
 *    LEVEL of `viewInputs` (slot callbacks like `toView`) are
 *    auto-wrapped to execute in the parent's boundary so handlers the
 *    consumer builds inside them dispatch through the parent's wrapping
 *    chain. Function values nested below the top level (e.g.
 *    `viewInputs: { config: { onSubmit } }`) throw at view-build time
 *    with a path-based error like `viewInputs.config.onSubmit`. The
 *    check is runtime-only (TypeScript cannot distinguish a
 *    user-declared nested callback from a data value whose prototype
 *    carries methods), so a misuse compiles cleanly and surfaces the
 *    first time the boundary renders. Keep slot callbacks at the top
 *    level of `viewInputs`.
 *  - `toParentMessage`: function that lifts a child message into the
 *    current boundary's Message type. The argument is typed as the
 *    child's Message via the view's brand, so destructuring is correctly
 *    typed without annotation. For per-instance identifiers, capture
 *    them in a closure
 *    (`(message) => GotEntryMessage({ entryId: entry.id, message })`).
 * 
 *  High-level events the parent handles declaratively flow through
 *  each Submodel's `OutMessage`. The parent's `GotChildMessage`
 *  handler unpacks the third tuple element of the child's `update`
 *  return and pattern-matches on `Option<OutMessage>`. See `Ui.Menu`,
 *  `Ui.Listbox`, etc., for examples.
 */
type Config = Readonly<{
  model: ViewModelOf<View>
  slotId: string
  toParentMessage: (message: ViewMessageOf<View>) => unknown
  view: View
  viewInputs: ViewInputsOf<View>
}>

Reflect

typesource
/**
 * Data-first / data-last signature for a `reflect*` setter built with
 *  `Function.dual`.
 * 
 *  A `reflect*` helper conforms a Submodel to a value that originated
 *  outside it (a URL, a server push, restored storage, a sibling field),
 *  without emitting an OutMessage. It is the inbound complement to
 *  OutMessage's outbound direction: the world is the source of truth, so
 *  the Submodel mirrors it silently and never announces the change back.
 * 
 *  Being dual, it reads two ways. Data-first sets the field and returns the
 *  model; data-last returns `(model) => model`, which slots point-free into
 *  an `evo` callback:
 * 
 *  ```ts
 *  // data-first
 *  const next = ColorListbox.reflectSelectedItem(model.colors, fromUrl)
 *  // data-last, point-free in evo
 *  evo(model, { colors: ColorListbox.reflectSelectedItem(fromUrl) })
 *  ```
 */
type Reflect = (model: Model, value: Value) => Model

Reflect2

typesource
/**
 * Two-argument variant of Reflect, for setters that resolve a
 *  value against a companion argument (e.g. `Tabs.reflectSelectedTab(value,
 *  options)`, which finds the value's index in `options`).
 */
type Reflect2 = (model: Model, a: A, b: B) => Model

View

typesource
/**
 * A view function branded with the Message type it dispatches. Build
 *  one with defineView:
 * 
 *  ```ts
 *  export const view = defineView<Counter.Model, Counter.Message>(
 *    (model) => h.button([h.OnClick(Increment())], ['+']),
 *  )
 *  ```
 * 
 *  When `ViewInputs` is provided, the view takes a second `viewInputs`
 *  argument:
 * 
 *  ```ts
 *  export const view = defineView<
 *    Checkbox.Model,
 *    Checkbox.Message,
 *    ViewInputs
 *  >((model, viewInputs) => viewInputs.toView({ checkbox: [...] }))
 *  ```
 * 
 *  Required at the `h.submodel` call site so unbranded plain functions
 *  fail to type-check there.
 */
type View = ViewInputs extends void
  ? (model: Model) => VNode | null
  : (model: Model, viewInputs: ViewInputs) => VNode | null & {
  __submodelMessage: Message
}

Stay in the update loop.

New releases, patterns, and the occasional deep dive.


Built with Foldkit.

© 2026 Devin Jameson