Form / foldkit-form

Example

ts
import { Schema } from 'effect'
import { Bundle } from 'foldkit-bundle'
import { Entity } from 'foldkit-entity'
import { Form } from 'foldkit-form'

const Post = Entity.define(
  'Post',
  Schema.Struct({
    id: Schema.String,
    title: Schema.String.check(Schema.isMinLength(1)).annotate({ title: 'Title' }),
  }),
)

// The operation's input, declared once. Give the same value to the operation.
const RenameInput = Schema.Struct({ id: Schema.String, title: Post.fields.title.schema })

const Rename = Form.make('Rename', Entity.input(Post, RenameInput))

// Place it: a Model field and a Message variant of the page.
const Page = Bundle.compose({ saved: Schema.Array(RenameInput) }).pipe(
  Bundle.withChild('rename', Rename.bundle, {
    // `submitted.value` is a decoded RenameInput. What it means is the page's.
    onOut: submitted => model => ({
      model: { ...model, saved: [...model.saved, submitted.value] },
    }),
  }),
)
const RenameForm = Page.children.rename
  • Form.make takes the name and an Entity.input. It resolves a control per key and throws if it cannot. A form that writes no Entity of its own (a sign-up, a search, a login) takes Form.input(schema), an input over an Entity defined from that schema: Form.make('Search', Form.input(SearchInput)). A view must also supply a renderer for each control kind, including custom kinds.
  • Rename.bundle is an ordinary Bundle. onOut is required, so a submit is never dropped by omission.
  • Rename.Message builds the form's Messages for your view to dispatch: Changed({ key, value }), Blurred({ key }), Submitted(), Reset(), and ValidatedAll() (see Validation).
  • Rename.controls lists the keys in the input's order, each with its control, label, description, required, and the Entity member.
  • Rename.field(model.rename, key) reads one key's state as Field<Draft>, for a view that walks controls; model.rename.fields.title is the same value typed to its key.

Run one edit through the parent

Continuing the example above, an assembly initializes the slice and routes its wrapped Messages. This reducer-level example needs no renderer:

ts
const { placements } = Page
const update = placements.update(model => ({ model }))
let model = placements.initial({ saved: [] }).model
model = update(model, Page.Message.GotRenameMessage({
  message: Rename.Message.Changed({ key: 'title', value: 'Hello' }),
})).model
model = update(model, Page.Message.GotRenameMessage({
  message: Rename.Message.Submitted(),
})).model
// model.saved contains the decoded input, with title "Hello" and id "".

Here a plain Schema.String allows the empty id. For editing a real row, fill the form with that row's id first; add an input-schema constraint if an empty id is invalid for your operation. onOut stores the value locally in this example. It does not save to a server. Forms with asynchronous checks also need the runtime to execute their returned Commands.