Mixins / foldkit-mixins
Quick start
The component author publishes the extension points. Style and Behavior can then be authored elsewhere and attached by the caller.
import { Schema } from 'effect'
import { defineMessageUnion } from 'foldkit/message'
import { Attr, Behavior, Capability, Event, Slot, Slots, SlotView, Style } from 'foldkit-mixins'
const Message = defineMessageUnion({ ChangedValue: { value: Schema.String } })
type Message = typeof Message.Type
type FieldInput = { readonly value: string; readonly invalid: boolean }
// 1. The component author publishes the extension points it supports.
const FieldSlots = Slots.define({
root: Slot.make({ capability: Capability.Container }),
input: Slot.make({
capability: Capability.TextInput,
events: [Event.Input],
attributes: [Attr.AriaInvalid],
}),
})
// 2. Appearance can live in a design system or another module.
const FieldStyle = Style.forSlots(FieldSlots)({
root: Style.compose(Style.class('field'), Style.inline({ display: 'grid' })),
input: Style.class('field-input'),
})
// 3. Element-level behavior can be authored independently.
const Validation = Behavior.forSlots(FieldSlots)<FieldInput, Message>({
input: Behavior.slot({
requires: { capability: Capability.TextInput, attributes: [Attr.AriaInvalid] },
attributes: ({ input, h }) => [h.AriaInvalid(input.invalid)],
}),
})
// 4. The pure view renders through the slots. Attachments return a new view;
// the original view is unchanged.
const Field = SlotView.forMessages<Message>()
.define(FieldSlots, (input: FieldInput, slots, h) =>
h.label(slots.root.attrs(), [
h.input(
slots.input.attrs([
h.Value(input.value),
h.OnInput(value => Message.ChangedValue({ value })),
]),
),
]),
)
.pipe(Style.attach(FieldStyle), Behavior.attach(Validation))slots.input.attrs(base) returns the view's base attributes plus every resolved contribution.
Base attributes, event Messages and any ChildAttribute survive. The caller did not copy the
markup or add a validationBehavior prop to the component.
Render it and interpret the result
Field is a view function. From a parent view with a compatible builder, call
Field({ value: model.value, invalid: model.invalid }, h). The input's value
comes from the parent; typing emits ChangedValue; the parent update must
store that value before the next render. Mixins owns none of that state.
In this example, FieldStyle supplies classes and a grid layout, while
Validation supplies aria-invalid. The input's OnInput remains owned by
the base view. Adding another OnInput attachment is a conflict, not a way to
chain a second application update. Compose the action in the existing Message
handler instead.
A Style's compiled rules (Style.pseudo, Style.media, a layer) arrive with
the Slot that draws them. In a browser, the first time a Slot draws a class,
its CSS is appended to one <style data-foldkit-styles> element, in the
microtask after the draw and before the browser paints. The element is made
only when there is something to add, and declares the standard layer order
first unless a sheet on the page already declares one, which it leaves alone.
Classes a stylesheet already on the page carries are left out, so
installing Style.stylesheet(...) yourself (for the first paint, or on the
server) never duplicates them. Outside a browser nothing is injected: a server
puts Style.usedIn(html), the CSS of every compiled class the page's markup
uses, in the page's head (with foldkit-ssr, through its head option).