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.

ts
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 behaviors option, 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:

ts
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 + Behavior

Accessibility contracts

ts
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) and Slot.describe(slot) return serializable metadata.
  • Mixin.compose combines mixins; Mixin.make, Mixin.dynamic and Mixin.empty build them.
  • diagnostics have stable codes and structured details, so the same information can feed tests, DevTools, documentation or agent tooling.