Mixins / foldkit-mixins
Behavior: reusable element-level interaction
A Behavior is a reusable bundle of element-level interaction that does not own application
state. Its attributes are built while resolving from the view's input and h, so it can only
emit Messages from the view's own Message universe. Like an agent capability,
a Behavior points one way — toward a Message — and never gets a Model setter.
const Focus = Behavior.forSlots(FieldSlots)<FieldInput, Message>({
input: Behavior.slot({
requires: { capability: Capability.TextInput },
attributes: ({ input, h }) => (input.invalid ? [h.AriaInvalid(true)] : []),
mount: input => MeasureField(input.value), // your own Mount.define* action
}),
})requires is checked against the slot contract at definition time. A behavior that requires
Event.Click cannot attach to a slot that did not publish click. Multiple mount contributions
compose into one OnMount per element.
A slot rendered once per item (rows, tabs, options) tells the Behavior which repetition it is
resolving through the second argument of attrs:
h.ul(slots.list.attrs(), items.map((item, index) =>
h.li(slots.row.attrs([h.Key(item.id)], { index, id: item.id, count: items.length }), [item.label]),
))The Behavior receives it as item ({ index, id?, count? }) in attributes and as the second
argument of mount, so one Behavior can write tabindex="0" on the current row and -1 on the
rest, or aria-posinset and aria-setsize on each. Without an item argument, item is
undefined and the Behavior decorates the slot as a single element.
Ready-made Behaviors
Behaviors holds Behaviors that own no state. The first is Collection: describe the parent's
array once, and a view and the Behaviors on it agree about identity, order, and disabled items.
import { Behaviors } from 'foldkit-mixins'
const items = Behaviors.Collection.of(input.tools, {
id: tool => tool.id,
disabled: tool => tool.disabled,
}) // ids, size, at, indexOf, isDisabled, enabled, slotItem
const Ids = Behaviors.Collection.behavior(ToolbarSlots)<ToolbarInput, Message>({
item: 'tool',
items: input => Behaviors.Collection.of(input.tools, { id: tool => tool.id }),
posInSet: true,
})
// in the view: slots.tool.attrs([h.Key(tool.id)], items.slotItem(index))of is pure and refuses two items with one id (mixins:duplicate-item-id), since an id is what
aria-activedescendant and aria-controls rely on. The Behavior writes id, aria-disabled,
and with posInSet aria-posinset and aria-setsize, reading the item context the view passes;
it contributes nothing to a slot resolved without one. Behaviors that need state, such as a
roving tab stop or a press, live in foldkit-primitives as a Bundle plus its Behavior.
Four more stateless Behaviors say a parent's state in ARIA, so a custom view gets it right without hand-writing it. Each takes slot names and functions of the view's input; none owns anything.
Behaviors.Disclosure.behavior(Slots)<Input, Message>({ trigger, content, open, id })writesaria-expandedandaria-controlson the trigger,idandhiddenon the content. The floor is<details>.Behaviors.ToggleState.behavior(Slots)({ control, state, as: 'checked' | 'pressed' })writesaria-checked(checkbox, switch, radio, option) oraria-pressed(a toggle button) from aboolean | 'mixed'.Behaviors.FieldAssociation.behavior(Slots)({ control, label, description?, error?, id, invalid?, required? })derives<id>,<id>-label,<id>-description,<id>-errorfrom one base id (FieldAssociation.ids(id)for a view that renders them itself) and writesid,for,aria-labelledby,aria-describedby(the error joins whileinvalid),aria-invalid,aria-required. Derived, not minted, so a resumed page and a test agree.Behaviors.SpinValue.behavior(Slots)({ control, value, onChange, min?, max?, step?, page?, text? })writes thespinbuttonrole andaria-value*, and steps the parent's number with ArrowUp and ArrowDown bystep, PageUp and PageDown bypage(default ten steps), Home and End to the bounds, clamped and default-prevented;SpinValue.spin(value, key, modifiers, bounds)is the pure step. A press-and-hold repeat and the wheel are not handled: Foldkit's wheel attribute carries no delta, and a repeat is a timer the Model would own.