Docs
Lint / lint (push) Successful in 14s

This commit is contained in:
Julien Valverdé
2026-06-12 01:51:30 +02:00
parent 241d489b0c
commit 257d505fdf
2 changed files with 154 additions and 1 deletions
+153
View File
@@ -0,0 +1,153 @@
---
sidebar_position: 3
title: Forms
---
# Forms
The `Form` module is the low-level field model used by Effect-FC forms. A form
field is built on the same state model as the rest of Effect-FC:
- `encodedValue`: a `Lens` containing the raw value used by the input.
- `value`: a `Subscribable` containing the decoded value as an `Option`.
- `issues`: a `Subscribable` containing schema validation issues for that field.
- `canCommit`, `isValidating`, and `isCommitting`: `Subscribable` flags for UI
state.
Most apps create a root form with `SubmittableForm` or `SynchronizedForm`, then
focus it into fields with `Form.focusObjectOn`, `Form.focusArrayAt`, or the
other focusing helpers.
## 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.
```tsx
import { Component, Form, Subscribable } from "effect-fc"
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* Subscribable.useAll([props.form.issues])
return (
<label>
{props.label}
<input
value={input.value}
onChange={(event) => input.setValue(event.currentTarget.value)}
/>
{issues.map((issue) => (
<small key={issue.path.join(".")}>
{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.
## Submittable Forms
Use `SubmittableForm` when the user edits local encoded form state, validates it
with a schema, and then submits a decoded value.
```tsx
import { Effect, Schema } from "effect"
import { Component, Form, SubmittableForm, Subscribable } from "effect-fc"
import { TextInputView } from "./TextInputView"
const RegisterSchema = Schema.Struct({
email: Schema.String,
password: Schema.String,
})
class RegisterFormState extends Effect.Service<RegisterFormState>()(
"RegisterFormState",
{
scoped: Effect.gen(function* () {
const form = yield* SubmittableForm.service({
schema: RegisterSchema,
initialEncodedValue: {
email: "",
password: "",
},
f: ([value]) =>
Effect.log(`Registering ${value.email}`),
})
return {
form,
email: Form.focusObjectOn(form, "email"),
password: Form.focusObjectOn(form, "password"),
} as const
}),
},
) {}
const RegisterFormView = Component.make("RegisterForm")(
function* () {
const state = yield* RegisterFormState
const [canCommit, isCommitting] = yield* Subscribable.useAll([
state.form.canCommit,
state.form.isCommitting,
])
const runPromise = yield* Component.useRunPromise()
const TextInput = yield* TextInputView.use
return (
<form
onSubmit={(event) => {
event.preventDefault()
void runPromise(state.form.submit)
}}
>
<TextInput label="Email" form={state.email} />
<TextInput label="Password" form={state.password} />
<button disabled={!canCommit}>
{isCommitting ? "Submitting..." : "Submit"}
</button>
</form>
)
},
)
```
`SubmittableForm.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/Subscribables.
## Synchronized Forms
Use `SynchronizedForm` 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 `SubmittableForm` for "submit this draft" flows, and
`SynchronizedForm` for "keep this target state synchronized when valid" flows.
## Focusing Fields
Forms can be focused just like Lenses:
```tsx
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.
+1 -1
View File
@@ -3,7 +3,7 @@ import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
const sidebars: SidebarsConfig = {
docsSidebar: ["getting-started", "state-management"],
docsSidebar: ["getting-started", "state-management", "forms"],
}
export default sidebars