Bundle / foldkit-bundle

Gates and custom Links

when in a placement config is the parent's own gate: while it returns false, the placement's Subscriptions and resources stop. An absent child also stops them, renders nothing, and ignores its Messages.

ts
Page.place(MediaQuery, 'narrow', {
  args: { query: '(max-width: 40rem)' },
  when: model => !model.helpOpen,
})

A child that is not a top-level field is placed through a Link, with bundle.at(link, config) and bundle.each(link, config). The scope builds Links without restating the parent, and Links are pipeable:

ts
// SidebarPage's Model is { sidebar, open }; GotSidebarMessage is Link.wrapper('GotSidebarMessage', MediaQueryMessage).
const Sidebar = MediaQuery.at(
  SidebarPage.link
    .field('sidebar', GotSidebarMessage)
    .pipe(Link.when((model: typeof SidebarPage.Model.Type) => model.open)),
  { args: { query: '(min-width: 60rem)' } },
)
LinkThe child is
Page.link.field(key, wrapper) or Link.field<Parent>()(…)a struct field, always present
Page.link.optional(key, wrapper) or Link.optional<Parent>()(…)an Option field; absent while None
Page.link.collection(key, keyedWrapper) or Link.collection<Parent>()(…)a record of items by key
Page.link.collectionById(key, keyedWrapper, { id }) or Link.collectionById<Parent>()(…)an array of items, in order
outer.pipe(Link.andThen(inner))inside another placed child
link.pipe(Link.when(predicate))gated by the parent
Link.make({ read, write, wrapper, path })anywhere a lens can reach

Link.wrapper(tag, ChildMessage) and Link.keyedWrapper(tag, ChildMessage, key?) build the parent variants a Link carries; Link.wrapper(Message.GotXMessage) takes one the parent's union already declares. When rest gives a placement's top-level field, placements.initial keeps that value and skips its init, so an optional child can start as None. A nested placement is always initialised, inside whatever rest gave, and outer placements initialise before nested ones. A Link.make whose path is one literal segment, path: ['sidebar'], names that top-level field to initial, as field and optional do.

Services

When the parent's own update needs services, name them on the scope:

ts
// clockUpdate: (model: Model, message: Message) => Update.Return<Model, Message, Clock>
Page.withServices<Clock>()
  .assemble(Page.at(Dark, { args: { query: '(prefers-color-scheme: dark)' } }))
  .update(clockUpdate)