Bundle / foldkit-bundle

Sixty seconds: one counter

ts
import { Schema } from 'effect'
import type { HtmlBuilder } from 'foldkit/html'
import { defineMessageUnion } from 'foldkit/message'
import { Bundle } from 'foldkit-bundle'

const CountModel = Schema.Struct({ count: Schema.Number })
const CountMessage = defineMessageUnion({ Incremented: {} })
const Count = Bundle.make('Count', {
  Model: CountModel,
  Message: CountMessage,
  init: () => ({ model: { count: 0 } }),
  update: model => ({ model: { count: model.count + 1 } }),
})

const Page = Bundle.compose({ greeting: Schema.String }).pipe(Bundle.withChild('clicks', Count))
type Model = typeof Page.Model.Type
type Message = typeof Page.Message.Type
const { placements } = Page

const config = placements.complete({
  init: () => placements.initial({ greeting: 'Hello' }),
  update: placements.update(model => ({ model })),
  subscriptions: placements.subscriptions(),
  view: (model: Model, h: HtmlBuilder<Message>) =>
    h.button(
      [h.OnClick(Page.Message.GotClicksMessage({ message: CountMessage.Incremented() }))],
      [`${model.greeting}: ${model.clicks.count}`],
    ),
})

Count describes a child machine. Page is the parent: its own field, greeting, and Count placed under clicks. From that one declaration it derives the parent's Model, { greeting, clicks }, its Message union, with the wrapper GotClicksMessage, and the assembly that routes that wrapper to the child and writes its next value back into model.clicks. A click changes 0 to 1 through the parent's update; no additional store is created.

config supplies init, update, and view to your Foldkit runtime. These declarations do not mount anything. For a runnable assembly, see the settings example.