Bundle / foldkit-bundle
Composing a parent
Bundle.compose states a parent once: its own fields, as Schema.Struct
takes them, and then, through pipe, its own Messages and the bundles it
places.
const App = Bundle.compose({ greeting: Schema.String }).pipe(
Bundle.withMessages({ ClickedReset: {} }),
Bundle.withChild('hello', HelloForm, {
onOut: out => model => ({ model: { ...model, greeting: `Hello, ${out.name}!` } }),
}),
)| Step | What it adds |
|---|---|
Bundle.compose(fields) | the parent's own Model fields |
Bundle.withMessages(cases) | the parent's own Message cases, as defineMessageUnion takes them |
Bundle.withChild(field, bundle, config?) | the bundle under field, and its wrapper Got<Field>Message |
Bundle.withEach(field, bundle, config?) | the bundle once per key of a record under field |
Bundle.withWiring(...wirings) | an integration's wiring (Remote, Mirror, Sync, Agent) |
Bundle.withServices<S>() | the services the parent's own update may require |
Bundle.configure(field, config) | the config of a child added without one |
The result has App.Model and App.Message, the Schemas to give
Surface.application or the runtime; App.children.hello, the placement, with
its view and helpers; and App.placements, the assembly, with initial,
update, subscriptions and complete. The config of withChild and
withEach is what Page.at and Page.each take below: args when the bundle
has them, onOut when it has an OutMessage, and key or when.
Each step is typed by the parent as it is at that step. An onOut sees the
parent's own fields and every child placed so far, so a mistake is reported
where it is written, and a child can read an earlier one. Write its result from
the Model it is given, spreading it or through modifyFields, so the fields of
children placed later are kept. A field or a Message case given twice is
refused: in the types for a field, and when the composition is built for both.
A wiring is usually made from the parent's own Model, through a Surface
application and a Remote domain built from App.Model, so it joins in a
second pipe once those exist:
const Wired = App.pipe(Bundle.withWiring(Data.wiring({ board: BoardSurface })))A child's config can be made from the parent too: a Crud editor's onOut comes
from Editor.at({ data, model: App.model.editor }), which needs the Surface
application built from this very Model. Add such a child without its config,
build what needs the Model, and give the config with Bundle.configure:
const Base = Bundle.compose({ greeting: Schema.String }).pipe(Bundle.withChild('hello', HelloForm))
const greet =
(out: typeof Greeted.Type) =>
(model: typeof Base.Model.Type) => ({ model: { ...model, greeting: `Hello, ${out.name}!` } })
const Page = Base.pipe(Bundle.configure('hello', { onOut: greet }))Base.Model and Base.Message exist at once, for Surface.application.
children and placements do not until every child that needs args or an
onOut has them: reading either earlier is a type error naming the children
still waiting. configure is typed by the child it configures, and refuses a
field that is not a child or one given its config already.
What it builds, and when to build it by hand
compose is sugar over the primitives below, and produces the same values.
The counter above, by hand:
const Clicks = Bundle.declare(Count, 'clicks')
const Model = Schema.Struct({ greeting: Schema.String, ...Clicks.fields })
const Message = defineMessageUnion({ ...Clicks.cases })
const Page = Bundle.parent({ Model, Message })
const placements = Page.assemble(Page.at(Clicks))Use the primitives when the parent's Model and Message already exist
elsewhere, when a placement needs a custom Link (a nested path, an Option
field, a gate on the Link), or when a declaration is shared between modules.
The rest of this README uses them, because each step there is one idea.