Bundle / foldkit-bundle
A Link without a Bundle: Update.foldChild
A page with a dozen @foldkit/ui components rarely wants a Bundle for each:
each is one field, one Got*Message variant, and one line in update. Foldkit's
Update.foldChild already folds such a child, given where it lives: read,
write and toParentMessage. A Link is exactly those three, so spread one in
and give the component's update:
import * as Tabs from '@foldkit/ui/tabs'
const Section = Schema.Literals(['general', 'billing'])
type Section = typeof Section.Type
const SectionTabs = Tabs.create<Section>()
const Model = Schema.Struct({ tabs: Tabs.Model, section: Section })
type Model = typeof Model.Type
const Message = defineMessageUnion({ GotTabsMessage: { message: Tabs.Message }, Saved: {} })
type Message = typeof Message.Type
const tabs = Link.field<Model>()('tabs', Link.wrapper(Message.GotTabsMessage))
const foldTabs = Update.foldChild({
...tabs,
update: SectionTabs.update,
foldOutMessage: Tabs.OutMessage.match<Update.Step<Model, Message>, Tabs.OutMessage<Section>>({
Selected:
({ value }) =>
model => ({ model: { ...model, section: value } }),
}),
})
const update = (model: Model, message: Message) =>
Message.match<Update.Return<Model, Message>>(message, {
GotTabsMessage: ({ message }) => foldTabs(model, message),
Saved: () => ({ model }),
})Link.wrapper(Message.GotTabsMessage)builds the wrapper from a variant the parent's union already declares, so the tag is written once. A variant with fields besidemessageis refused, since a wrapped Message would lack them.- The same Link serves every entry point:
Update.foldChildStep({ ...link, update: Dialog.open, foldOutMessage })for one that takes no Message, andlink.toParentMessagefor the view'sh.submodelandSubscription.lift. Its init folds throughLink.foldInit, since init has no parent yet to read:
const init = (): Update.Return<Model, Message> =>
Update.foldChildInit(
{ model: Tabs.init({ id: 'sections' }) },
Link.foldInit(tabs, { section: 'general' }),
)- An ignored Message leaves the parent as it was.
foldChildwrites the child back even when its update returned the child it was given; the writes ofLink.fieldandLink.optionalthen return the parent itself, so the page does not redraw. Awritegiven toLink.makecopies as written; placements guard it,foldChilddoes not.
Nothing here is placed: there is no placements.update routing, and the
component's Subscriptions are lifted by hand. When a component is placed in
several parents, or its Subscriptions are the part that gets forgotten, make it
a Bundle with Bundle.fromParts above.
The two halves at once: Link.child
Hand-rolling foldChild plus an h.submodel restates the field, the wrapper,
and the slot. Link.child(link, update, view, slotId) states them once and
yields the fold for update and the drawing for view, so one placement drives
both; an absent child (Link.optional) draws nothing.
const posts = Link.child(postsLink, Posts.update, (page, draw) => PostsShell(page, draw), 'posts')
const update = (model: Model, message: Message) =>
Message.match<Update.Return<Model, Message>>(message, {
GotPostsMessage: ({ message }) => posts.update(model, message),
Saved: () => ({ model }),
})
const view = (model: Model, h: HtmlBuilder<Message>) => h.main([], [posts.view(model, h)])A child with view inputs, or with OutMessages of its own, stays hand-rolled:
Link.child takes a plain update and view and lifts neither.
test/foldChild.test.ts runs this example, and
examples/foldkit/ui-showcase folds its
thirty-eight components this way.
An ask that waits for its owner: Bundle.follow
An address can ask something of a child that has no Model yet — a block to
select once the page loads, a preview to show once the entry arrives.
Bundle.follow wraps an update result and, while a pending ask waits and its
owner is ready, sends the ask through the owner's own Messages and lets it go:
const follow = Bundle.follow(EditorPage.placed, {
pending: model => model.linked,
release: model => ({ ...model, linked: Option.none() }),
ready: editor => editor.status !== 'Loading',
toMessages: ask => [EditorMessage.Selected({ id: ask.block })],
})
const update = (model, message) => follow(baseUpdate(model, message))Nothing happens without a pending ask, without the child, or before it is ready — each returns the result untouched, pending kept. An ask that translates to no Messages is still let go. Apply the step outside whatever installs a loaded value, so the owner looks ready when it is read; and keep the pending ask where the address can rewrite it while it waits, or a reload during the load loses it.
ready and toMessages also see the Model the ask was held in, for what
only it knows. send replaces the dispatch when the ask must travel further
than the placement — through the full update with its hooks, rather than the
placement's own fold.