@thomasfosterau/effect-forms-svelte 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
1
+ import { Command } from "@thomasfosterau/effect-forms";
2
+ import * as Schema from "effect/Schema";
3
+ //#region src/EntityForm.ts
4
+ const builders = {
5
+ text: (key, options) => ({
6
+ _tag: "scalar",
7
+ key,
8
+ multiline: false,
9
+ options
10
+ }),
11
+ textarea: (key, options) => ({
12
+ _tag: "scalar",
13
+ key,
14
+ multiline: true,
15
+ options
16
+ }),
17
+ select: (key, options) => ({
18
+ _tag: "select",
19
+ key,
20
+ options
21
+ }),
22
+ custom: (key, component, props, options = {}) => ({
23
+ _tag: "widget",
24
+ key,
25
+ bridge: "value",
26
+ component,
27
+ props,
28
+ options
29
+ }),
30
+ rows: (key, component, props, options = {}) => ({
31
+ _tag: "widget",
32
+ key,
33
+ bridge: "rows",
34
+ component,
35
+ props,
36
+ options
37
+ })
38
+ };
39
+ /**
40
+ * Validate an `f.select` entry against its plan field at definition time: the
41
+ * field must be `enum`-kind (the only kind `Command.plan` derives option
42
+ * values for), and every derived value must have a matching `optionLabels`
43
+ * thunk.
44
+ */
45
+ const validateSelectEntry = (entry, field) => {
46
+ if (field.kind !== "enum") throw new Error(`defineEntityForm: entry "${entry.key}" is f.select but its command field is "${field.kind}"-kind, not enum`);
47
+ const missingLabels = (field.options ?? []).filter((value) => !Object.prototype.hasOwnProperty.call(entry.options.optionLabels, value));
48
+ if (missingLabels.length > 0) throw new Error(`defineEntityForm: entry "${entry.key}" is missing optionLabels for: ${missingLabels.join(", ")}`);
49
+ };
50
+ /**
51
+ * Build an entity-form definition from a command-struct pair and an ordered
52
+ * entry list. Runs once at module load. Throws when the entries don't cover
53
+ * the derivable command fields exactly (exhaustiveness) or name an unknown
54
+ * key — a command-struct change must surface here, at definition time.
55
+ */
56
+ const defineEntityForm = (options, build) => {
57
+ const omitted = new Set(options.omit ?? []);
58
+ const withoutOmitted = (struct) => {
59
+ if (omitted.size === 0) return struct;
60
+ const fields = {};
61
+ for (const key in struct.fields) if (!omitted.has(key)) fields[key] = struct.fields[key];
62
+ return { fields };
63
+ };
64
+ const plan = Command.plan(withoutOmitted(options.create), withoutOmitted(options.update));
65
+ const entries = build(builders);
66
+ const byKey = new Map(plan.fields.map((field) => [field.key, field]));
67
+ const declared = /* @__PURE__ */ new Set();
68
+ for (const entry of entries) {
69
+ const field = byKey.get(entry.key);
70
+ if (field === void 0) throw new Error(`defineEntityForm: entry "${entry.key}" is not a field of either command struct`);
71
+ if (declared.has(entry.key)) throw new Error(`defineEntityForm: entry "${entry.key}" is declared twice`);
72
+ declared.add(entry.key);
73
+ if (entry._tag === "select") validateSelectEntry(entry, field);
74
+ }
75
+ const missing = plan.fields.filter((field) => !declared.has(field.key)).map((field) => field.key);
76
+ if (missing.length > 0) throw new Error(`defineEntityForm: command fields not declared by any entry: ${missing.join(", ")} — declare or explicitly widget them`);
77
+ const entriesFor = (mode) => entries.filter((entry) => {
78
+ const field = byKey.get(entry.key);
79
+ return field !== void 0 && (mode === "create" ? field.inCreate : field.inUpdate);
80
+ });
81
+ return {
82
+ plan,
83
+ entries,
84
+ createInput: Schema.Struct(Command.formInputFields(plan, "create", options.inputOverrides?.create)),
85
+ updateInput: Schema.Struct(Command.formInputFields(plan, "update", options.inputOverrides?.update)),
86
+ createDefaults: Command.blankDefaults(plan, "create"),
87
+ updateDefaults: Command.blankDefaults(plan, "update"),
88
+ adaptCreate: Command.createAdapter(plan, options.adaptOverrides?.create),
89
+ adaptUpdate: Command.updateAdapter(plan, options.adaptOverrides?.update),
90
+ submit: options.submit,
91
+ clientId: plan.clientId,
92
+ entriesFor,
93
+ fieldFor: (key) => {
94
+ const field = byKey.get(key);
95
+ if (field === void 0) throw new Error(`EntityFormDefinition: unknown field "${key}"`);
96
+ return field;
97
+ }
98
+ };
99
+ };
100
+ //#endregion
101
+ export { defineEntityForm };
102
+
103
+ //# sourceMappingURL=EntityForm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EntityForm.js","names":[],"sources":["../src/EntityForm.ts"],"sourcesContent":["/**\n * @copyright Thomas Foster\n *\n * The typed entity-form definition builder — the composition layer of the\n * schema-driven form phase. `defineEntityForm(commands, (f) => [...])` pairs a\n * `Command.plan` derivation (form-input schemas, blank defaults, tri-state\n * adapters — see `effect-forms/Command`) with an ordered list of typed field\n * entries: shared scalar primitives by kind, and arbitrary widget components\n * for the bespoke fields (pickers, contributor lists). One generic renderer\n * component walks `definition.entries`; per-entity code shrinks to one\n * definition module.\n *\n * Field keys are type-constrained to the command structs' keys, and the\n * builder enforces exhaustiveness at definition time (a runtime check, kept\n * deliberately simple over deep conditional types): every derivable command\n * field must be declared exactly once, so adding a field to a command struct\n * breaks the entity's definition module until the form declares it.\n *\n * Labels and other message-bearing props are **thunks** (`() => string`,\n * Paraglide message function references) resolved at render time, never at\n * module scope — module-scope resolution would freeze the import-time locale.\n *\n * @module effect-forms-svelte/EntityForm\n */\n\nimport type { Component } from \"svelte\";\nimport { Command } from \"@thomasfosterau/effect-forms\";\nimport * as Schema from \"effect/Schema\";\n\n/** A render-time message thunk — pass the Paraglide function itself, uncalled. */\nexport type MessageThunk = () => string;\n\n/** Presentation options for a scalar text entry. */\nexport interface TextEntryOptions {\n readonly label: MessageThunk;\n /** Presentation `type` attribute (`url`, `date`). Omitted → text input. */\n readonly type?: \"text\" | \"url\" | \"date\";\n /** Keyboard hint for numeric text fields; defaults from the field's derived kind. */\n readonly inputmode?: \"numeric\";\n /** Force required-ness (defaults to the schema-derived constraint). */\n readonly required?: boolean;\n readonly placeholder?: MessageThunk;\n}\n\n/** A scalar field rendered by the shared text/textarea primitives. */\nexport interface ScalarEntry {\n readonly _tag: \"scalar\";\n readonly key: string;\n readonly multiline: boolean;\n readonly options: TextEntryOptions;\n}\n\n/** Presentation options for a `<select>` entry over a literal-union (`enum` kind) field. */\nexport interface SelectEntryOptions {\n readonly label: MessageThunk;\n readonly required?: boolean;\n /**\n * Per-option render-time label thunks, keyed by the field's derived option\n * *value* (`Command.FieldPlan.options`, read via `fieldFor` — not declared\n * here). `defineEntityForm` throws at definition time if a derived value has\n * no matching thunk.\n */\n readonly optionLabels: Readonly<Record<string, MessageThunk>>;\n}\n\n/**\n * A `<select>` field over a literal-union (`enum` kind) command field. Option\n * *values* derive from the field's plan; this entry supplies only the\n * per-option labels — `defineEntityForm` validates the field is `enum`-kind\n * and that every derived value has a label.\n */\nexport interface SelectEntry {\n readonly _tag: \"select\";\n readonly key: string;\n readonly options: SelectEntryOptions;\n}\n\n/** Options for a widget entry (picker / rows list). */\nexport interface WidgetEntryOptions {\n /** Gate the submit button while this field's value is empty (required pickers). */\n readonly gate?: boolean;\n}\n\n/**\n * A bespoke widget field. The renderer bridges the store to the widget through\n * the stage-1 wrapper contract — `name`, `value`, `onChange`, `errors` — which\n * are **reserved keys**: the bridge is spread last, so a `props()` thunk or a\n * load-supplied field prop can never silently sever the store binding.\n */\nexport interface WidgetEntry {\n readonly _tag: \"widget\";\n readonly key: string;\n /** `\"value\"` bridges a string FK; `\"rows\"` bridges a controlled row list. */\n readonly bridge: \"value\" | \"rows\";\n // reason: the renderer composes arbitrary widgets; per-widget prop checking\n // happens in the definition module via the builder's generic signature.\n readonly component: Component<any>;\n readonly props: () => Record<string, unknown>;\n readonly options: WidgetEntryOptions;\n}\n\n/** One ordered entry in an entity form definition. */\nexport type EntityFormEntry = ScalarEntry | WidgetEntry | SelectEntry;\n\n/** The per-mode submit labels. */\nexport interface SubmitMessages {\n readonly create: MessageThunk;\n readonly update: MessageThunk;\n}\n\n/** The runtime shape `defineEntityForm` accepts for a command struct. */\nexport interface CommandStructLike {\n readonly fields: Record<string, Schema.Top>;\n}\n\n/** The value `defineEntityForm` returns — everything one renderer needs. */\nexport interface EntityFormDefinition {\n readonly plan: Command.CommandPlan;\n readonly entries: readonly EntityFormEntry[];\n readonly createInput: Schema.Top;\n readonly updateInput: Schema.Top;\n readonly createDefaults: Readonly<Record<string, string>>;\n readonly updateDefaults: Readonly<Record<string, string>>;\n readonly adaptCreate: (input: Command.FormInputRecord) => Record<string, unknown>;\n readonly adaptUpdate: (input: Command.FormInputRecord) => Record<string, unknown>;\n readonly submit: SubmitMessages;\n /** The create struct carries a client-generated id (idempotent retries). */\n readonly clientId: boolean;\n /** Entries visible in one mode, in declaration order. */\n readonly entriesFor: (mode: \"create\" | \"update\") => readonly EntityFormEntry[];\n /** The plan field behind an entry key. */\n readonly fieldFor: (key: string) => Command.FieldPlan;\n}\n\n/** The writable command keys a definition may declare (the `id` key is renderer-owned). */\nexport type CommandKeys<C extends CommandStructLike, U extends CommandStructLike> = Exclude<\n Extract<keyof C[\"fields\"] | keyof U[\"fields\"], string>,\n \"id\"\n>;\n\n/** The entry builders handed to the definition callback, key-typed to the command pair. */\nexport interface EntryBuilders<K extends string> {\n /** A scalar field rendered by the shared `TextInputField`. */\n text(key: K, options: TextEntryOptions): EntityFormEntry;\n /** A scalar field rendered by the shared `TextAreaInputField`. */\n textarea(key: K, options: TextEntryOptions): EntityFormEntry;\n /**\n * A `<select>` field over a literal-union (`enum` kind) command field.\n * Option values are derived from the field's plan; `options.optionLabels`\n * supplies each derived value's render-time label thunk.\n */\n select(key: K, options: SelectEntryOptions): EntityFormEntry;\n /**\n * A bespoke string-valued widget (an FK picker). The renderer supplies\n * `name` / `value` / `onChange` / `errors`; `props()` supplies the rest at\n * render time (labels stay thunk-resolved).\n */\n custom<P extends Record<string, unknown>>(\n key: K,\n component: Component<never>,\n props: () => P,\n options?: WidgetEntryOptions,\n ): EntityFormEntry;\n /**\n * A bespoke row-list widget (a contributor list). The renderer owns the row\n * state and supplies `name` / `value` / `onChange` / `errors`; the widget\n * owns the wire serialisation (its hidden input).\n */\n rows<P extends Record<string, unknown>>(\n key: K,\n component: Component<never>,\n props: () => P,\n options?: WidgetEntryOptions,\n ): EntityFormEntry;\n}\n\n/** Everything `defineEntityForm` needs beyond the entries. */\nexport interface DefineEntityFormOptions<C extends CommandStructLike, U extends CommandStructLike> {\n readonly create: C;\n readonly update: U;\n readonly submit: SubmitMessages;\n /** Per-field adapter overrides, per mode — the irregular-coercion escape hatch. */\n readonly adaptOverrides?: {\n readonly create?: Command.AdaptOverrides;\n readonly update?: Command.AdaptOverrides;\n };\n /** Per-field Form-Input schema overrides, per mode (e.g. an optional create title with a server default). */\n readonly inputOverrides?: {\n readonly create?: Readonly<Record<string, Schema.Top>>;\n readonly update?: Readonly<Record<string, Schema.Top>>;\n };\n /**\n * Command fields deliberately absent from the standalone form (a carve-out,\n * e.g. BookSeries' `editors` — managed through the Book form's inline-create\n * path instead). Omitted fields are stripped from the create/update field\n * records before `Command.plan` runs, so they need no entry, are never\n * classified (a kind-less omitted field can't throw at definition time),\n * and never appear in the derived input schema, defaults, or adapters.\n */\n readonly omit?: readonly CommandKeys<C, U>[];\n}\n\nconst builders: EntryBuilders<string> = {\n text: (key, options) => ({ _tag: \"scalar\", key, multiline: false, options }),\n textarea: (key, options) => ({ _tag: \"scalar\", key, multiline: true, options }),\n select: (key, options) => ({ _tag: \"select\", key, options }),\n custom: (key, component, props, options = {}) => ({\n _tag: \"widget\",\n key,\n bridge: \"value\",\n component: component as Component<any>,\n props,\n options,\n }),\n rows: (key, component, props, options = {}) => ({\n _tag: \"widget\",\n key,\n bridge: \"rows\",\n component: component as Component<any>,\n props,\n options,\n }),\n};\n\n/**\n * Validate an `f.select` entry against its plan field at definition time: the\n * field must be `enum`-kind (the only kind `Command.plan` derives option\n * values for), and every derived value must have a matching `optionLabels`\n * thunk.\n */\nconst validateSelectEntry = (entry: SelectEntry, field: Command.FieldPlan): void => {\n if (field.kind !== \"enum\") {\n throw new Error(\n `defineEntityForm: entry \"${entry.key}\" is f.select but its command field is \"${field.kind}\"-kind, not enum`,\n );\n }\n const missingLabels = (field.options ?? []).filter(\n (value) => !Object.prototype.hasOwnProperty.call(entry.options.optionLabels, value),\n );\n if (missingLabels.length > 0) {\n throw new Error(\n `defineEntityForm: entry \"${entry.key}\" is missing optionLabels for: ${missingLabels.join(\", \")}`,\n );\n }\n};\n\n/**\n * Build an entity-form definition from a command-struct pair and an ordered\n * entry list. Runs once at module load. Throws when the entries don't cover\n * the derivable command fields exactly (exhaustiveness) or name an unknown\n * key — a command-struct change must surface here, at definition time.\n */\nexport const defineEntityForm = <C extends CommandStructLike, U extends CommandStructLike>(\n options: DefineEntityFormOptions<C, U>,\n build: (f: EntryBuilders<CommandKeys<C, U>>) => readonly EntityFormEntry[],\n): EntityFormDefinition => {\n const omitted = new Set<string>(options.omit ?? []);\n const withoutOmitted = (struct: CommandStructLike): CommandStructLike => {\n if (omitted.size === 0) return struct;\n const fields: Record<string, Schema.Top> = {};\n for (const key in struct.fields) if (!omitted.has(key)) fields[key] = struct.fields[key];\n return { fields };\n };\n const plan = Command.plan(withoutOmitted(options.create), withoutOmitted(options.update));\n const entries = build(builders as EntryBuilders<CommandKeys<C, U>>);\n\n const byKey = new Map<string, Command.FieldPlan>(plan.fields.map((field) => [field.key, field]));\n const declared = new Set<string>();\n for (const entry of entries) {\n const field = byKey.get(entry.key);\n if (field === undefined) {\n throw new Error(\n `defineEntityForm: entry \"${entry.key}\" is not a field of either command struct`,\n );\n }\n if (declared.has(entry.key)) {\n throw new Error(`defineEntityForm: entry \"${entry.key}\" is declared twice`);\n }\n declared.add(entry.key);\n if (entry._tag === \"select\") validateSelectEntry(entry, field);\n }\n const missing = plan.fields.filter((field) => !declared.has(field.key)).map((field) => field.key);\n if (missing.length > 0) {\n throw new Error(\n `defineEntityForm: command fields not declared by any entry: ${missing.join(\", \")} — declare or explicitly widget them`,\n );\n }\n\n const entriesFor = (mode: \"create\" | \"update\") =>\n entries.filter((entry) => {\n const field = byKey.get(entry.key);\n return field !== undefined && (mode === \"create\" ? field.inCreate : field.inUpdate);\n });\n\n return {\n plan,\n entries,\n createInput: Schema.Struct(\n Command.formInputFields(plan, \"create\", options.inputOverrides?.create) as Record<\n string,\n never\n >,\n ),\n updateInput: Schema.Struct(\n Command.formInputFields(plan, \"update\", options.inputOverrides?.update) as Record<\n string,\n never\n >,\n ),\n createDefaults: Command.blankDefaults(plan, \"create\"),\n updateDefaults: Command.blankDefaults(plan, \"update\"),\n adaptCreate: Command.createAdapter(plan, options.adaptOverrides?.create),\n adaptUpdate: Command.updateAdapter(plan, options.adaptOverrides?.update),\n submit: options.submit,\n clientId: plan.clientId,\n entriesFor,\n fieldFor: (key: string) => {\n const field = byKey.get(key);\n if (field === undefined) throw new Error(`EntityFormDefinition: unknown field \"${key}\"`);\n return field;\n },\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,381 @@
1
+ import { FormUpdate } from "./EnhanceCore.js";
2
+ import { FormListeners } from "./Listeners.js";
3
+ import { TaintedFields } from "./Tainted.js";
4
+ import { Constraints, Errors, Field, FormBuilder, Issues, Mode, Path, Result, State, Validate } from "@thomasfosterau/effect-forms";
5
+ import * as Schema from "effect/Schema";
6
+ import * as Layer from "effect/Layer";
7
+ //#region src/FormStore.svelte.d.ts
8
+ type FormActionResult<A, Message = unknown> = Result.FormActionResult<A, Message>;
9
+ type FormErrors = Errors.FormErrors;
10
+ type FormPath = Issues.FormPath;
11
+ type FormState<A, Message = unknown> = State.FormState<A, Message>;
12
+ type TypedFormErrors<A> = Errors.TypedFormErrors<A>;
13
+ type InputConstraint = Constraints.InputConstraint;
14
+ type InputConstraints = Constraints.InputConstraints;
15
+ type LeafFormatter = Issues.LeafFormatter;
16
+ /** How a form validates (and optionally auto-submits, and revalidates after a post) as the user types. */
17
+ type FormMode = Mode.FormMode;
18
+ /** A field's sync / async validators — see {@link module:effect-forms/Validate}. */
19
+ type FieldValidators<A, R = never> = Validate.FieldValidators<A, R>;
20
+ /** Per-field state exposed on a {@link FieldHandle}, mirroring TanStack Form's field meta. */
21
+ interface FieldMeta {
22
+ /** Changed at some point, or blurred (TanStack: `isTouched = isDirty || isBlurred`). */
23
+ readonly isTouched: boolean;
24
+ /** Has lost focus at least once — distinct from {@link isTouched}. */
25
+ readonly isBlurred: boolean;
26
+ /**
27
+ * Was changed away from its default value at some point, via `field.value =
28
+ * …` (or another {@link FormStore} write method). Persistent: unlike {@link
29
+ * isDefaultValue}, this does not flip back to `false` if the value is later
30
+ * reverted to the default — only {@link FormStore.reset} clears it.
31
+ */
32
+ readonly isDirty: boolean;
33
+ /** Whether the current value is (still, or again) equal to the field's default. */
34
+ readonly isDefaultValue: boolean;
35
+ }
36
+ /**
37
+ * A typed, reactive handle to one field, returned by {@link FormStore.field}.
38
+ * Bundles everything a single input needs — `bind:value={field.value}`,
39
+ * `name={field.name}`, `onblur={field.handleBlur}`, `{...field.constraints}`,
40
+ * `field.visibleErrors`, `field.meta` — instead of wiring each by hand against
41
+ * an untyped {@link FormPath}. See {@link Path.FieldPath} for how the path is
42
+ * type-checked.
43
+ */
44
+ declare class FieldHandle<V> {
45
+ #private;
46
+ /** The path in flat field-name notation — use as the input's `name` attribute. */
47
+ readonly name: string;
48
+ constructor(store: FormStore<Schema.Top, object, unknown>, path: FormPath);
49
+ /** The field's current value. Assignable — `bind:value={field.value}` works. */
50
+ get value(): V;
51
+ set value(next: V);
52
+ /** All messages currently attached to this field. */
53
+ get errors(): readonly string[];
54
+ /** This field's messages, gated by the form's validation mode (see {@link FormStore.shouldShowError}). */
55
+ get visibleErrors(): readonly string[];
56
+ /** Native validation attributes for this field — spread onto the `<input>`. */
57
+ get constraints(): InputConstraint;
58
+ /** This field's {@link FieldMeta}. */
59
+ get meta(): FieldMeta;
60
+ /** Mark this field blurred — see {@link FormStore.handleBlur}. */
61
+ handleBlur: () => void;
62
+ }
63
+ /** Transport: turn a submission into a {@link FormActionResult}. Defaults to `fetch`. */
64
+ type SubmitFn<A, Message> = (body: FormData, action: string, form: HTMLFormElement) => Promise<FormActionResult<A, Message>>;
65
+ /** The submission passed to an {@link FormOptions.onSubmit} handler. */
66
+ interface Submission<A, Message> {
67
+ /** The current (reactive) form data. */
68
+ readonly data: A;
69
+ /** The multipart body that would be posted. */
70
+ readonly formData: FormData;
71
+ /** The form element being submitted. */
72
+ readonly form: HTMLFormElement;
73
+ /**
74
+ * The submit button that triggered this submission (its `name`/`value` is
75
+ * already folded into {@link Submission.formData}, matching native
76
+ * `new FormData(form, submitter)` semantics), or `null` for an auto-submit
77
+ * (no button was clicked).
78
+ */
79
+ readonly submitter: HTMLElement | null;
80
+ /** Abort the submission entirely (no network request, no result applied). */
81
+ readonly cancel: () => void;
82
+ /**
83
+ * Resolve the submission with a result computed in JS (e.g. via an API
84
+ * client), instead of letting the default network POST run. The result is
85
+ * applied to the store exactly as a server response would be.
86
+ */
87
+ readonly resolve: (result: FormActionResult<A, Message>) => void;
88
+ }
89
+ /** Options for {@link createForm}. */
90
+ interface FormOptions<S extends Schema.Top, A, Message, R = never> {
91
+ /** The schema to validate against in the browser (defaults to the bound schema). */
92
+ readonly schema?: S;
93
+ /**
94
+ * When the form validates as the user types (and whether it auto-submits) —
95
+ * see {@link module:effect-forms/Mode}. Defaults to `onSubmit`. `onChange` /
96
+ * `onBlur` auto-validate the form; use {@link FormStore.shouldShowError} /
97
+ * {@link FormStore.visibleErrorsAt} to gate display per field.
98
+ */
99
+ readonly mode?: FormMode;
100
+ /**
101
+ * The builder's field record, set automatically when {@link createForm} is
102
+ * given a {@link FormBuilder}. A fallback for deriving a blank array item
103
+ * when no `schema` is bound — with a schema, {@link FormStore.append} /
104
+ * {@link FormStore.insertItem} resolve the item (at any nesting depth) by
105
+ * walking the schema's AST instead.
106
+ */
107
+ readonly fields?: Field.FieldsRecord;
108
+ /**
109
+ * The builder itself, set automatically when {@link createForm} is given a
110
+ * {@link FormBuilder}. Its `refineEffect` refinements run client-side as a
111
+ * whole-form async validator whenever {@link FormStore.validate} runs.
112
+ */
113
+ readonly builder?: FormBuilder.FormBuilder<Field.FieldsRecord, R>;
114
+ /** The action URL the form posts to (defaults to the current page's action). */
115
+ readonly action?: string;
116
+ /** Reset `data` back to the initial values after a successful submit. */
117
+ readonly resetForm?: boolean;
118
+ /** Warn (via `beforeunload`) when navigating away with unsaved changes. */
119
+ readonly taintedMessage?: string | false;
120
+ /** Milliseconds before an in-flight submission flips `delayed` to `true`. */
121
+ readonly delayMs?: number;
122
+ /** Override the message rendering used by client-side validation. */
123
+ readonly validationFormat?: LeafFormatter;
124
+ /** Override how a submission is sent (default: `fetch` to the form's action). */
125
+ readonly submit?: SubmitFn<A, Message>;
126
+ /**
127
+ * Per-field sync/async validators, keyed by {@link Path.toString} (e.g.
128
+ * `"author.name"`, `"tags[0]"`). See {@link FormStore.setFieldValidators} to
129
+ * register these dynamically instead (e.g. once an array item exists).
130
+ */
131
+ readonly validators?: Record<string, FieldValidators<any, R>>;
132
+ /**
133
+ * Services an async field validator or a builder's `refineEffect`
134
+ * refinement needs on its `R` channel. A `ManagedRuntime` is built from
135
+ * this once per store and reused for every client-side Effect run.
136
+ */
137
+ readonly layer?: Layer.Layer<R>;
138
+ /**
139
+ * Handle the submission entirely in JS: given the current data, return the
140
+ * {@link FormActionResult} to apply (e.g. by calling an API client). When set,
141
+ * this runs instead of the network POST — the "handled using JS on the page"
142
+ * path. `onSubmit`'s `cancel()` / `resolve()` still take precedence.
143
+ */
144
+ readonly handler?: (data: A) => FormActionResult<A, Message> | Promise<FormActionResult<A, Message>>;
145
+ /**
146
+ * Handle the submission in JS on the page. Receives the data + form, can
147
+ * `cancel()` it or `resolve(result)` it directly; if it neither cancels nor
148
+ * resolves, the default network POST runs.
149
+ */
150
+ readonly onSubmit?: (input: Submission<A, Message>) => void | Promise<void>;
151
+ /** Called with the raw {@link FormActionResult} once the response is parsed. */
152
+ readonly onResult?: (input: {
153
+ readonly result: FormActionResult<A, Message>;
154
+ }) => void;
155
+ /** Called with the computed {@link FormUpdate} before it is applied. */
156
+ readonly onUpdate?: (input: {
157
+ readonly update: FormUpdate<A, Message>;
158
+ readonly result: FormActionResult<A, Message>;
159
+ }) => void;
160
+ /** Called after the update has been applied to the store. */
161
+ readonly onUpdated?: (input: {
162
+ readonly form: {
163
+ readonly data: A;
164
+ readonly errors: FormErrors;
165
+ readonly valid: boolean;
166
+ };
167
+ }) => void;
168
+ /** Called on an unexpected (non-user-fixable) error. */
169
+ readonly onError?: (input: {
170
+ readonly error: string;
171
+ }) => void;
172
+ /** Called on a redirect result (default: `window.location.assign`). */
173
+ readonly onRedirect?: (input: {
174
+ readonly location: string;
175
+ }) => void;
176
+ /**
177
+ * Field- and form-level listeners (onMount / onChange / onBlur) — see
178
+ * {@link module:effect-forms-svelte/Listeners}. Fire independent of
179
+ * `mode`'s validation, and tear down with {@link FormStore.enhance}'s
180
+ * `destroy`.
181
+ */
182
+ readonly listeners?: FormListeners;
183
+ }
184
+ /** A reactive form, returned by {@link createForm}. */
185
+ declare class FormStore<S extends Schema.Top, A = S["Type"], Message = unknown, R = never> {
186
+ #private;
187
+ /** The reactive form data — bind inputs to its fields. */
188
+ data: A;
189
+ /** The current status message, if any. */
190
+ message: Message | undefined;
191
+ /** Native input constraints derived server-side. */
192
+ constraints: InputConstraints;
193
+ /** Whether a submission is in flight. */
194
+ submitting: boolean;
195
+ /** Whether the in-flight submission has passed `delayMs`. */
196
+ delayed: boolean;
197
+ /** Whether the current state came from a submission. */
198
+ posted: boolean;
199
+ /** Whether the last validation/submit was valid. */
200
+ valid: boolean;
201
+ /** How many times {@link FormStore.enhance}'s submit handler has actually attempted a submission. */
202
+ submissionAttempts: number;
203
+ readonly id: string;
204
+ /** The current nested error tree (server errors merged with client errors), typed to the form data's shape. */
205
+ readonly errors: TypedFormErrors<A>;
206
+ /** Client-side-sourced errors only (schema, per-field validators, `refineEffect`). */
207
+ readonly clientErrors: TypedFormErrors<A>;
208
+ /** Server-sourced errors only (the last submission's response). */
209
+ readonly serverErrors: TypedFormErrors<A>;
210
+ /** Whether any async field validator or `refineEffect` run is currently in flight. */
211
+ readonly isValidating: boolean;
212
+ /** Whether the form has ever been submitted (attempted). */
213
+ readonly isSubmitted: boolean;
214
+ /** Whether the most recently *completed* submission succeeded. `false` before any submit completes. */
215
+ readonly isSubmitSuccessful: boolean;
216
+ /**
217
+ * Whether the submit button should be enabled: not mid-submit, not
218
+ * mid-validation, and either nothing has been validated yet (a pristine
219
+ * form isn't presumptively invalid — the server gets the first look) or the
220
+ * last validation run said valid.
221
+ */
222
+ readonly canSubmit: boolean;
223
+ /** The tainted-field tree, re-derived as `data` changes. */
224
+ readonly tainted: TaintedFields;
225
+ /** Whether any field has been changed since the form was seeded. */
226
+ readonly isTainted: boolean;
227
+ /** The tree of fields changed since the last submission (empty before any submit). */
228
+ readonly changedSinceSubmit: TaintedFields;
229
+ /** Whether anything has changed since the last submission. */
230
+ readonly hasChangedSinceSubmit: boolean;
231
+ constructor(initial: FormState<A, Message>, options?: FormOptions<S, A, Message, R>);
232
+ /**
233
+ * Spreadable attributes for the `<form>` element: `<form {...form.props}>`.
234
+ * Sets `method="post"` (and `action`, if configured) and attaches the
235
+ * progressive-enhancement submit handler via a Svelte attachment.
236
+ */
237
+ get props(): Record<string | symbol, unknown>;
238
+ /** Whether the field at `path` (or anything beneath it) is tainted. */
239
+ taintedAt(path: FormPath): boolean;
240
+ /** The messages currently attached to `path` (for dynamic / nested paths). */
241
+ errorsAt(path: FormPath): readonly string[];
242
+ /** Whether the async validator(s) at `path` are currently running. */
243
+ isValidatingAt(path: FormPath): boolean;
244
+ /**
245
+ * Register (or replace) `path`'s validators — the option-driven equivalent
246
+ * of attaching `validators` at {@link createForm} time, for fields that
247
+ * don't exist until later (e.g. an appended array item).
248
+ */
249
+ setFieldValidators(path: FormPath, validators: FieldValidators<unknown, R>): void;
250
+ /** Re-validate the whole form against the schema (if one was supplied), plus a builder's `refineEffect` refinements. */
251
+ validate(): boolean;
252
+ /** Validate a single field against the schema, returning that field's messages. */
253
+ validateField(path: FormPath): readonly string[];
254
+ /** Set the value at `path` (path-addressed, immutable). Bind directly to `data` otherwise. */
255
+ setValue(path: FormPath, value: unknown): void;
256
+ /** Read the value at `path`. */
257
+ valueAt(path: FormPath): unknown;
258
+ /** Mark the field at `path` touched (defaults to `true`) — drives `onBlur` display. */
259
+ setTouched(path: FormPath, touched?: boolean): void;
260
+ /** Whether the field at `path` has been touched. */
261
+ isTouchedAt(path: FormPath): boolean;
262
+ /** Whether the field at `path` was ever changed away from its default — see {@link FieldMeta.isDirty}. */
263
+ isDirtyAt(path: FormPath): boolean;
264
+ /** Whether the field at `path` currently holds its default value — the negation of {@link taintedAt}. */
265
+ isDefaultValueAt(path: FormPath): boolean;
266
+ /** The native validation attributes for `path` (a leaf of {@link constraints}). */
267
+ constraintAt(path: FormPath): InputConstraint;
268
+ /**
269
+ * A typed, reactive handle to the field at `path` — see {@link FieldHandle}.
270
+ * `path` is type-checked against the form data's shape (see {@link
271
+ * Path.FieldPath}); a typo'd segment or wrong-shaped path is a compile error.
272
+ */
273
+ field<P extends Path.FieldPath<A>>(path: P): FieldHandle<Path.PathValue<A, P>>;
274
+ /**
275
+ * Handle a field losing focus: mark it touched, run its `onBlur` /
276
+ * `onBlurAsync` validators (or, once posted, the revalidate-on-blur mode),
277
+ * fire that field's `onBlur` listener (if any, debounced per its config),
278
+ * and — in `onBlur` validation mode — validate the whole form
279
+ * (auto-submitting if configured and something changed since the last
280
+ * submit).
281
+ */
282
+ handleBlur(path: FormPath): void;
283
+ /**
284
+ * Whether an error at `path` should be shown yet, per the validation mode:
285
+ * always after a submit; in `onChange` once the field is tainted; in `onBlur`
286
+ * once it is touched; never in `onSubmit` before posting.
287
+ */
288
+ shouldShowError(path: FormPath): boolean;
289
+ /** The messages at `path`, but only when {@link shouldShowError} allows it. */
290
+ visibleErrorsAt(path: FormPath): readonly string[];
291
+ /** Append an item to the array at `path` (a blank item is derived when omitted). */
292
+ append(path: FormPath, value?: unknown): void;
293
+ /**
294
+ * Insert an item into the array at `path` at `index` (clamped to the
295
+ * array's bounds; a non-finite `index` inserts at the end), shifting later
296
+ * items — and their per-index touched flags / errors — up by one. A blank
297
+ * item is derived when `value` is omitted.
298
+ */
299
+ insertItem(path: FormPath, index: number, value?: unknown): void;
300
+ /**
301
+ * Replace the item at `index` in the array at `path` in place. Drops that
302
+ * index's own touched flag / errors (they described the old value); other
303
+ * indices are untouched.
304
+ */
305
+ replaceItem(path: FormPath, index: number, value: unknown): void;
306
+ /** Remove every item from the array at `path`, dropping all of its per-index state. */
307
+ clearItems(path: FormPath): void;
308
+ /**
309
+ * Remove the item at `index` from the array at `path`, shifting later
310
+ * items' per-index touched flags / errors down by one so they keep
311
+ * following their row.
312
+ */
313
+ removeItem(path: FormPath, index: number): void;
314
+ /** Swap two items in the array at `path`, swapping their per-index touched flags / errors too. */
315
+ swap(path: FormPath, indexA: number, indexB: number): void;
316
+ /** Move the item at `from` to `to` within the array at `path`, carrying its per-index state along. */
317
+ move(path: FormPath, from: number, to: number): void;
318
+ /** Revert `data` to the values sent by the last submission (no-op before any submit). */
319
+ revertToLastSubmit(): void;
320
+ /**
321
+ * Reset `data`, `errors`, and `message` back to the tainted baseline. Pass
322
+ * `values` to re-seed that baseline first (e.g. adopting a different
323
+ * entity's values) instead of returning to the original seed.
324
+ */
325
+ reset(values?: A): void;
326
+ /** A serialisable snapshot for SvelteKit's `snapshot` capture/restore. */
327
+ capture(): FormState<A, Message>;
328
+ /** Restore from a {@link capture} snapshot (its merged tree becomes the server tree; the client tree clears). */
329
+ restore(snapshot: FormState<A, Message>): void;
330
+ /**
331
+ * Apply server-sourced `data` + `errors` directly, bypassing a submit
332
+ * round-trip — for glue code that repopulates the store from a derived
333
+ * result it received some other way (e.g.
334
+ * {@link module:effect-forms-svelte/SvelteKit}'s `applyActionResult`, from
335
+ * a SvelteKit form action's failure). Sets `posted` so
336
+ * {@link shouldShowError} shows the new errors immediately; `valid` is
337
+ * always `false` (the caller only reaches for this on a failure result).
338
+ */
339
+ setServerState(data: A, errors: FormErrors): void;
340
+ /**
341
+ * Progressive-enhancement form action. Use as `<form use:form.enhance>` (or
342
+ * spread {@link FormStore.props}). Intercepts submit, runs the `onSubmit`
343
+ * handler, then sends the body and applies the result. Without JavaScript the
344
+ * form posts normally.
345
+ */
346
+ enhance: (node: HTMLFormElement) => {
347
+ destroy: () => void;
348
+ };
349
+ /** Build a {@link FormData} body from the current data (SPA / manual submit). */
350
+ toFormData(): FormData;
351
+ }
352
+ /** A schema plus its blank-form defaults — what {@link createForm} wraps `makeForm` over. */
353
+ interface FormSeed<S extends Schema.Top> {
354
+ readonly schema: S;
355
+ readonly defaults: S["Type"];
356
+ readonly id?: string;
357
+ }
358
+ /** The built-schema type a {@link FormBuilder} of `TFields` / `R` collapses to. */
359
+ type BuiltSchema<TFields extends Field.FieldsRecord, R> = Schema.Codec<Field.DecodedFromFields<TFields>, Field.EncodedFromFields<TFields>, R>;
360
+ /** Options for the {@link createForm} {@link FormBuilder} overload. */
361
+ type FormBuilderOptions<TFields extends Field.FieldsRecord, R, Message> = FormOptions<BuiltSchema<TFields, R>, Field.EncodedFromFields<TFields>, Message, R> & {
362
+ /** Blank-form values. Defaults to a schema-derived encoded blank per field. */
363
+ readonly defaults?: Field.EncodedFromFields<TFields>;
364
+ /** Distinguishes multiple forms on a page. Defaults to `"default"`. */
365
+ readonly id?: string;
366
+ };
367
+ /**
368
+ * Create a reactive {@link FormStore}. Three sources are accepted:
369
+ *
370
+ * - a {@link FormBuilder} — the schema, blank defaults, and array-item defaults
371
+ * are all derived from the builder (client validation, including
372
+ * `refineEffect` refinements, is wired automatically);
373
+ * - `{ schema, defaults }` — seed a blank form (wrapping the core's `makeForm`);
374
+ * - a server-produced {@link FormState} — hydrate from it (schema in `options`).
375
+ */
376
+ declare function createForm<TFields extends Field.FieldsRecord, R, Message = unknown>(builder: FormBuilder.FormBuilder<TFields, R>, options?: FormBuilderOptions<TFields, R, Message>): FormStore<BuiltSchema<TFields, R>, Field.EncodedFromFields<TFields>, Message, R>;
377
+ declare function createForm<S extends Schema.Top, Message = unknown, R = never>(seed: FormSeed<S>, options?: FormOptions<S, S["Type"], Message, R>): FormStore<S, S["Type"], Message, R>;
378
+ declare function createForm<S extends Schema.Top, A = S["Type"], Message = unknown, R = never>(initial: FormState<A, Message>, options: FormOptions<S, A, Message, R>): FormStore<S, A, Message, R>;
379
+ //#endregion
380
+ export { FieldHandle, FieldMeta, FieldValidators, FormBuilderOptions, FormMode, FormOptions, FormSeed, FormStore, Submission, SubmitFn, createForm };
381
+ //# sourceMappingURL=FormStore.svelte.d.ts.map