Mixins / foldkit-mixins-ui

toView and resolve

Every component adapter (all but Anchor, which is a Behavior) has both:

Adapter.toView(mixins, { h, input? }, draw)  ->  the component's toView
Adapter.resolve(attributes, mixins, { h, input? })  ->  the resolved bundles

toView is resolve placed where the component calls back, and is the normal path. It fits a stateless component's view and a Submodel's viewInputs alike. The Message type comes from h, and a Submodel's value type (a RadioGroup's or Tabs' Value) from where the toView goes, so neither needs writing out.

A button drawn as a button needs no draw at all. Button.view takes the label, a style, and @foldkit/ui's own config:

ts
import { Button } from 'foldkit-mixins-ui'

const saveButton = (disabled: boolean, h: HtmlBuilder<Message>) =>
  Button.view(
    {
      label: 'Save',
      style: SaveStyle,
      type: 'submit',
      disabled,
      onClick: Message.Saved(),
    },
    h,
  )

A toggle button, shadcn's Toggle, is a Button with pressed: it writes aria-pressed, and each variant has its on look.

An input or a textarea places its own bundles, so its view takes a draw. Input.view and Textarea.view take the value and Messages, a style, and @foldkit/ui's own config (type and placeholder, rows for a textarea):

ts
import { Input } from 'foldkit-mixins-ui'

const emailField = (value: string, h: HtmlBuilder<Message>) =>
  Input.view(
    {
      id: 'email',
      value,
      onInput: value => Message.TypedEmail({ value }),
      type: 'email',
      placeholder: 'you@example.com',
      style: FieldStyle,
      draw: ({ input, label }, h) => h.div([], [h.label(label, ['Email']), h.input(input)]),
    },
    h,
  )

Input.field and Textarea.field draw a whole field from a FieldValidation.Field: the value, its first error or checking mark as the description, and aria-invalid where the field is invalid. changed maps the typed value to a Message; label is the text over the control; and draw places the resolved label, control, and description bundles (the default stacks them):

ts
import { Input } from 'foldkit-mixins-ui'

const titleField = (field: FieldValidation.Field<string>, h: HtmlBuilder<Message>) =>
  Input.field(
    { id: 'title', label: 'Title', field, changed: value => Message.TypedTitle({ value }) },
    h,
  )

resolve is the seam underneath, for bundles already in hand: a view that receives a Tabs render as its input, or a Calendar whose Mixins read the mode it is showing, which is only known once the attributes arrive:

ts
import type { CalendarAttributes } from '@foldkit/ui/calendar'
import { Calendar, CalendarSlots } from 'foldkit-mixins-ui'

const CalendarStyle = Style.forSlots(CalendarSlots)({
  root: Style.whenInput<CalendarAttributes['_tag']>(mode => mode === 'Years', Style.class('years')),
})

const resolveCalendar = (attributes: CalendarAttributes, h: HtmlBuilder<Message>) =>
  Calendar.resolve(attributes, [CalendarStyle.mixin], { input: attributes._tag, h })