5.0 KiB
sidebar_position, title
| sidebar_position | title |
|---|---|
| 3 | Forms |
Forms
The form modules provide an Effect-first model for editable, schema-validated state. They are not a visual component library. Instead, they give you Lenses/Views for form values, validation issues, and commit state, so you can build the UI with whatever components your app already uses.
The base Form type represents a field. A field tracks both the raw encoded
value used by inputs and the decoded value produced by an Effect Schema:
encodedValue: aLenscontaining the raw input value.value: aViewcontaining the decoded value as anOption.issues: aViewcontaining schema validation issues for that field.canCommit,isValidating, andisCommitting:Viewflags for UI state.
There are two root form flavors:
MutationForm: owns local encoded form state, validates it, and submits a decoded value through a mutation.LensForm: wraps an existing targetLensand writes valid changes back to that target.
Most apps create one of those root forms, then focus it into fields with
Form.focusObjectOn, Form.focusArrayAt, or the other focusing helpers.
The core form model is Effect code and can technically be used outside
Effect View. Effect View adds the React-facing hooks, such as Form.useInput, that
make those forms convenient to bind to components.
Field Inputs
Use Form.useInput to connect a Form field to a controlled input. It returns
a React-style { value, setValue } pair and writes changes back to the field's
encodedValue Lens.
import { Component, Form, View } from "effect-view"
const TextInputView = Component.make("TextInput")(
function* (props: {
readonly form: Form.Form<readonly PropertyKey[], string, string>
readonly label: string
}) {
const input = yield* Form.useInput(props.form, {
debounce: "250 millis",
})
const [issues] = yield* View.useAll([props.form.issues])
return (
<label>
{props.label}
<input
value={input.value}
onChange={(event) => input.setValue(event.currentTarget.value)}
/>
{issues.map((issue, index) => (
<small key={index}>
{issue.message}
</small>
))}
</label>
)
},
)
Use Form.useOptionalInput for Option fields. It returns the same value/setter
pair plus enabled and setEnabled, which is useful for optional inputs that
can be toggled on and off.
Mutation Forms
Use MutationForm when the user edits local encoded form state, validates it
with a schema, and then submits a decoded value.
import { Effect, Schema } from "effect"
import { Component, Form, MutationForm, View } from "effect-view"
import { TextInputView } from "./TextInputView"
const RegisterSchema = Schema.Struct({
email: Schema.String,
password: Schema.String,
})
const RegisterFormView = Component.make("RegisterForm")(
function* () {
const [form, email, password] = yield* Component.useOnMount(() =>
Effect.gen(function* () {
const form = yield* MutationForm.service({
schema: RegisterSchema,
initialEncodedValue: {
email: "",
password: "",
},
f: ([value]) => Effect.log(`Registering ${value.email}`),
})
return [
form,
Form.focusObjectOn(form, "email"),
Form.focusObjectOn(form, "password"),
] as const
}),
)
const [canCommit, isCommitting] = yield* View.useAll([
form.canCommit,
form.isCommitting,
])
const runPromise = yield* Component.useRunPromise()
const TextInput = yield* TextInputView.use
return (
<form
onSubmit={(event) => {
event.preventDefault()
void runPromise(form.submit)
}}
>
<TextInput label="Email" form={email} />
<TextInput label="Password" form={password} />
<button disabled={!canCommit || isCommitting}>
{isCommitting ? "Submitting..." : "Submit"}
</button>
</form>
)
},
)
MutationForm.service starts validation in the form's scope. The form keeps
its encoded input state, decoded value, validation issues, submit mutation, and
commit flags available as Lenses/Views.
Lens Forms
Use LensForm when a form should edit an existing target Lens.
Instead of submitting a final value, valid encoded changes are synchronized back
to the target Lens.
This is useful for edit screens where the form is a validated view over existing
state. Use MutationForm for "submit this draft" flows, and LensForm for
"keep this target state synchronized when valid" flows.
Focusing Fields
Forms can be focused just like Lenses:
const emailField = Form.focusObjectOn(form, "email")
const firstItemField = Form.focusArrayAt(form, 0)
Focused fields keep the parent form's validation and commit state, but narrow
encodedValue, value, and issues to the field path. This lets each input
component receive only the field it needs.