Mixins / foldkit-mixins
SlotView
SlotView.define(slots, render) is a pure view that publishes slots. Attach with
SlotView.attach, Style.attach or Behavior.attach; each returns a new view.
Nothing but the render callback's builder names the view's Message universe, so plain define
infers it from an explicit h: HtmlBuilder<Message> annotation. Without one, Message becomes
unknown and the eventual variance error is hard to read.
SlotView.forMessages<Message>().define(slots, render) fixes the universe up front instead;
Input still comes from the callback's own input annotation.
Behavior.attach uses the behavior's Input as the attached view's Input, so declare a
behavior over the view's input type, not merely the subset of fields it happens to read.
For per-item groups such as tabs, options or calendar cells, use
SlotView.buildersFor(slots, mixins, { input, h }) and map the items yourself. One contribution
then applies to every item while each item's base attributes retain their own event ownership.
Parts: redraw only what changed
A SlotView runs whole on every change to its input. A larger view can be cut into parts, each declaring the input keys it reads and given only those. A part runs again only when one of those values changed by identity; otherwise Foldkit reuses its last drawing without diffing it.
type Input = { readonly title: string; readonly count: number }
const Parts = SlotView.parts(PanelSlots)<Input, Message>()
const Title = Parts.part('Title', { reads: ['title'] }, (input, slots, h) =>
h.h2(slots.title.attrs(), [input.title]),
)
const Count = Parts.part('Count', { reads: ['count'] }, (input, slots, h) =>
h.p(slots.count.attrs(), [String(input.count)]),
)
const Panel = Parts.assemble((input, slots, h, draw) =>
h.section(slots.root.attrs(), [draw(Title), draw(Count)]),
)input.count inside Title does not compile, so a part cannot read a value it
is not redrawn for. Panel is an ordinary SlotView: Styles and Behaviors attach
to it and reach every part's Slots.
- A part's own Behaviors go in its
behaviorsoption, declared over the part's selection (Pick<Input, 'count'>), and are resolved against it. - A Mixin attached to the whole view that reads the input (a Behavior's
attributes,Style.whenInput) is resolved against the whole input, so the parts it reaches are drawn again on every change: correct, not cached. Static Styles cost nothing. - Memoizing needs a running application. Drawn inert (a test, a server's first pass), every part is drawn afresh.
A Slot drawn once per item can memoize each item, in a part or any SlotView:
const drawRow = (
slots: SlotView.SlotBuilders<typeof ListSlots, Message>,
h: HtmlBuilder<Message>,
{ index, text }: { readonly index: number; readonly text: string },
) => h.li(slots.row.attrs([h.Key(text)], { index, id: text }), [text])
const Rows = ListParts.part('Rows', { reads: ['rows'] }, (input, slots, h) =>
h.ul(
slots.root.attrs(),
input.rows.map((text, index) => slots.row.lazy({ index, id: text }, drawRow, { index, text })),
),
)An item is drawn again only when one of its arguments changed by identity, or
when a Mixin gave the Slots it used something different for it (a static Style
by identity, so two views apart only by a Style draw their rows apart). A Behavior
that works out a roving tab stop from the whole list redraws the row the stop
left and the row it reached, and no other. drawRow reads only its arguments,
so define it once, not per render. A Mixin that gives an item a handler made per
render, and an item drawing that holds another, are drawn every time: correct,
not cached.
Items are remembered by key for the life of the page, as Foldkit's keyed memo
is, so key by something bounded (a node id), not by something that keeps
changing (a search's results). The memo is one per drawing function: two views
drawing the same key with one drawRow share it, and each misses whenever
what it gives the row differs from the other's. Drawn inert, nothing is kept.
With foldkit-surface
Mixins do not require Surface, but the two fit naturally: Surface says what a feature may observe and emit; slots say where that feature may be customized.
foldkit-mixins-surface renders a Surface projection through a SlotView, so a Behavior reads the
projected input rather than the root application Model, and its builder carries the Surface's
Message subset.
Application Model / Message
│
Surface
what may be seen/emitted
│
SlotView
where it may be customized
│
Style + BehaviorAccessibility contracts
const DialogPattern = A11y.pattern({
trigger: { capability: Capability.Interactive, events: [Event.Click] },
content: { capability: Capability.Container },
})
A11y.validate(DialogPattern, DialogSlots)A11y.validate is pure and DOM-independent. It checks the declared contract for missing or hidden
slots, capability mismatches and unpublished events or attributes. An optional requirement
tolerates an absent slot but still checks a present one. It does not certify WCAG compliance.
Introspection
The contracts are data:
Slots.describe(contract)andSlot.describe(slot)return serializable metadata.Mixin.composecombines mixins;Mixin.make,Mixin.dynamicandMixin.emptybuild them.- diagnostics have stable codes and structured
details, so the same information can feed tests, DevTools, documentation or agent tooling.