Files
effect-view/packages/effect-view/ai-docs/MutationForm.md
T
ThilawynandJulien Valverdé 0e2b4c1951
Publish / publish (push) Successful in 2m6s
Lint / lint (push) Successful in 1m3s
Improve effect-view docs, mutation keys, and Fast Refresh (#76)
## Summary

- Add comprehensive AI-oriented documentation for effect-view components, state, async rendering, queries, mutations, forms, streams, and runtime setup.
- Fix `Mutation.mutate` so each invocation consistently uses its own key instead of reusing the previous mutation key.
- Improve Vite Fast Refresh instrumentation for:
  - User-defined component wrappers.
  - Data-first and pipeline-based `withContext`/`withRuntime` entrypoints.
  - Nested function handling.
  - Stable hook signatures that preserve state for non-hook-related edits.
- Add regression tests for mutation keys and refresh behavior.
- Bump `effect-view` to `0.1.5` and `@effect-view/vite-plugin` to `0.0.2`.

## Testing

- `bun run --cwd packages/effect-view test -- src/Mutation.test.ts`
  - 4 tests passed
- `bun run --cwd packages/vite-plugin test -- src/plugin.test.ts`
  - 11 tests passed

The full test suite was not run.

---------

Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: #76
2026-08-26 01:06:03 +02:00

52 lines
3.2 KiB
Markdown

# MutationForm
A root form (implements `Form.Form`, see `Form.md`) that owns a local encoded draft and passes the valid decoded value to a `Mutation` when `submit` runs. Use for registration, checkout, search — any workflow with an explicit submit action.
```tsx
import { Effect } from "effect"
import { Component, MutationForm, View } from "effect-view"
const form = yield* Component.useOnMount(() =>
MutationForm.make({
schema: ProfileSchema,
initialEncodedValue: { displayName: "", age: "", contact: { email: "" } },
f: ([profile, form]) => Effect.log(`Creating ${profile.displayName}, age ${profile.age}`),
}).pipe(MutationForm.thenRun),
)
const [canCommit, isCommitting] = yield* View.useAll([form.canCommit, form.isCommitting])
const runPromise = yield* Component.useRunPromise()
<button disabled={!canCommit || isCommitting} onClick={() => void runPromise(form.submit)}>
{isCommitting ? "Creating..." : "Create profile"}
</button>
```
- `MutationForm.make({ schema, initialEncodedValue, f })` constructs the form; the underlying `Mutation`'s input key is the tuple `[decodedValue, form]`, not just the decoded value. Most `f` implementations only destructure the first element (`profile.age` is a number even though the input edited a string); `form` is included so `f` can, if needed, read other form state (e.g. `form.issues`, `form.encodedValue`) while handling the submission. `MutationForm.thenRun` starts initial validation in the current scope.
- Create once and keep stable — the usual home is `Component.useOnMount`.
- `form.submit` runs the mutation only when the form can currently commit; schema issues block submission before the mutation ever runs.
- Focus into subforms/fields with `Form.focusObjectOn`/`focusArrayAt`/etc. (see `Form.md`) and bind them to inputs with `Form.useInput`.
- If `f` fails with a `Schema.SchemaError`, `form.submit` formats that error into `form.issues` automatically — the same formatting path used for client-side decoding errors — instead of only surfacing it as a mutation failure. Any other failure from `f` is left as the mutation's `Failure` and does not touch `form.issues`.
## Example: schema-owned date conversion
```tsx
class DateTimeUtcFromZoned extends Schema.transformOrFail(Schema.DateTimeZonedFromSelf, Schema.DateTimeUtcFromSelf, {
strict: true,
decode: input => ParseResult.succeed(DateTime.toUtc(input)),
encode: DateTime.setZoneCurrent,
}) {}
export class DateTimeUtcFromZonedInput extends Schema.transformOrFail(Schema.String, DateTimeUtcFromZoned, {
strict: true,
decode: (input, _options, ast) => Effect.flatMap(DateTime.CurrentTimeZone, timeZone =>
Option.match(DateTime.makeZoned(input, { timeZone, adjustForTimeZone: true }), {
onSome: ParseResult.succeed,
onNone: () => ParseResult.fail(new ParseResult.Type(ast, input, "Enter a valid date and time")),
})),
encode: value => ParseResult.succeed(DateTime.formatIsoZoned(value).slice(0, 16)),
}) {}
```
An `<input type="datetime-local">` edits `"2026-07-22T14:30"`; the schema decodes it to a UTC `DateTime.Utc` (`decode(...).pipe requires DateTime.CurrentTimeZone` — provide `DateTime.layerCurrentZoneLocal` in the runtime) and the mutation receives the UTC instant directly. No manual date parsing in the component.