@@ -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.
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user