@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.
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/ClientValidate.d.ts +18 -0
- package/dist/ClientValidate.js +37 -0
- package/dist/ClientValidate.js.map +1 -0
- package/dist/EnhanceCore.d.ts +61 -0
- package/dist/EnhanceCore.js +123 -0
- package/dist/EnhanceCore.js.map +1 -0
- package/dist/EntityForm.d.ts +158 -0
- package/dist/EntityForm.js +103 -0
- package/dist/EntityForm.js.map +1 -0
- package/dist/FormStore.svelte.d.ts +381 -0
- package/dist/FormStore.svelte.js +986 -0
- package/dist/FormStore.svelte.js.map +1 -0
- package/dist/Listeners.d.ts +57 -0
- package/dist/Listeners.js +38 -0
- package/dist/Listeners.js.map +1 -0
- package/dist/SvelteKit.d.ts +22 -0
- package/dist/SvelteKit.js +18 -0
- package/dist/SvelteKit.js.map +1 -0
- package/dist/Tainted.d.ts +32 -0
- package/dist/Tainted.js +81 -0
- package/dist/Tainted.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/package.json +59 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FormStore.svelte.js","names":["#store","#path","#options","#mode","#attachmentKey","#runtime","#listenerTimers","#sourced","#validating","#lastSubmitOutcome","#hasValidated","#initial","#lastSubmitted","#previousData","#fieldValidators","#enhanceAttachment","#recomputeValid","#runRefineEffect","#markDirty","#detachedData","#onFieldChanged","#touched","#dirtied","#onFieldBlurred","#scheduleListener","#hasUnsubmittedChanges","#autoSubmit","#arrayAt","#defaultItem","#remapArrayIndices","#sourcedSnapshot","#autoSubmitNode","#runSubmit","#validateTimer","#fibers","#interruptFiber","#validatingGeneration","#runFieldTrigger","#applySyncFieldValidator","#shouldFallBackToRevalidate","#applyRevalidateFallback","#runAsyncFieldValidator","#setClientMessagesAt","#revalidatesOn","#runValidatorFiber","#lastRefinementPaths","#applyRedirect","#applyError","#applySubmittedData","#applyUpdate","#buildDispatchListeners","#scheduleValidation"],"sources":["../src/FormStore.svelte.ts"],"sourcesContent":["/**\n * @copyright Thomas Foster\n *\n * The reactive Svelte 5 form store.\n *\n * {@link createForm} binds a schema to a reactive, rune-backed {@link FormStore}.\n * Give it either `{ schema, defaults }` (it wraps the core's `makeForm` to seed\n * an empty form) or a server-produced {@link FormState} to hydrate from. The\n * store exposes mutable `data` (bind inputs to it), typed `errors`\n * (`form.errors.name`), `submitting` / `delayed` / `message` / `valid`, tainted\n * tracking, and a spreadable {@link FormStore.props} for `<form {...form.props}>`\n * that wires progressive-enhancement submission (a Svelte attachment).\n *\n * Submission is handled one of two ways: an `onSubmit` handler the page\n * provides (full client-side / JS handling, with `cancel()` and an optional\n * returned result), or — by default — a POST to the form's action (e.g. a\n * derived SvelteKit action) whose result is applied back to the store.\n *\n * ## Validation engine v2\n *\n * `errors` merges two parallel trees (see {@link Errors.SourcedErrors}):\n * `server` (the last submission's response) and `client` (browser-side\n * validation — the schema, per-field validators, a builder's `refineEffect`\n * refinements). A field's value changing clears *only that field's* server\n * error — a `State.withError` uniqueness failure the user hasn't touched\n * survives a client revalidation of some other field. `clientErrors` /\n * `serverErrors` expose the per-source trees for a UI that wants to style\n * them differently.\n *\n * `validators` (keyed by {@link Path.toString}) attach per-field sync/async\n * validators (the `FieldValidators` shape — TanStack Form's `onChange(Async)`\n * / `onBlur(Async)` vocabulary). Async validators run as Effect fibers: a new\n * value interrupts the field's in-flight fiber before forking the next run —\n * debounce + \"latest wins\" cancellation fall out of that alone (see\n * {@link Validate.runAsyncFieldValidator}). `isValidating` /\n * {@link FormStore.isValidatingAt} reflect any fiber still running; supply\n * `layer` when a validator (or a builder's `refineEffect`) needs services.\n *\n * `Mode.RevalidateMode` (`options.mode.revalidate`, default `\"onChange\"`)\n * governs what happens *after* a failed post in `onSubmit` mode: fixing a\n * field re-validates it (clearing its error) without a resubmit.\n *\n * @module effect-forms-svelte/FormStore\n */\n\nimport {\n Constraints,\n Errors,\n Field,\n Form,\n FormBuilder,\n Issues,\n Mode,\n Path,\n Result,\n State,\n Validate,\n} from \"@thomasfosterau/effect-forms\";\nimport * as Duration from \"effect/Duration\";\nimport * as Effect from \"effect/Effect\";\nimport * as Fiber from \"effect/Fiber\";\nimport * as Layer from \"effect/Layer\";\nimport * as ManagedRuntime from \"effect/ManagedRuntime\";\nimport type * as Schema from \"effect/Schema\";\nimport { createAttachmentKey } from \"svelte/attachments\";\nimport { validateData, validateField } from \"./ClientValidate.ts\";\nimport {\n computeDataApplication,\n type FormUpdate,\n objectToFormData,\n parseActionResult,\n resultToUpdate,\n} from \"./EnhanceCore.ts\";\nimport { dispatchChange, type FieldListener, type FormListeners } from \"./Listeners.ts\";\nimport {\n computeTainted,\n isAnyTainted,\n isTaintedAt,\n taintedLeafPaths,\n type TaintedFields,\n} from \"./Tainted.ts\";\n\n// Terse local aliases onto the namespaced core exports (the public API is\n// `import { Form, State, … } from \"@thomasfosterau/effect-forms\"`).\ntype FormActionResult<A, Message = unknown> = Result.FormActionResult<A, Message>;\ntype FormErrors = Errors.FormErrors;\ntype FormPath = Issues.FormPath;\ntype FormIssue = Issues.FormIssue;\ntype FormState<A, Message = unknown> = State.FormState<A, Message>;\ntype TypedFormErrors<A> = Errors.TypedFormErrors<A>;\ntype InputConstraint = Constraints.InputConstraint;\ntype InputConstraints = Constraints.InputConstraints;\ntype LeafFormatter = Issues.LeafFormatter;\n/** How a form validates (and optionally auto-submits, and revalidates after a post) as the user types. */\nexport type FormMode = Mode.FormMode;\n/** A field's sync / async validators — see {@link module:effect-forms/Validate}. */\nexport type FieldValidators<A, R = never> = Validate.FieldValidators<A, R>;\nconst { clearSourceAt, emptySourced, hasErrors, mergeSourced, noErrors, replaceAt, setSourceAt } =\n Errors;\nconst { getAt, setAt } = Path;\nconst makeForm = Form.make;\n\n/** Per-field state exposed on a {@link FieldHandle}, mirroring TanStack Form's field meta. */\nexport interface FieldMeta {\n /** Changed at some point, or blurred (TanStack: `isTouched = isDirty || isBlurred`). */\n readonly isTouched: boolean;\n /** Has lost focus at least once — distinct from {@link isTouched}. */\n readonly isBlurred: boolean;\n /**\n * Was changed away from its default value at some point, via `field.value =\n * …` (or another {@link FormStore} write method). Persistent: unlike {@link\n * isDefaultValue}, this does not flip back to `false` if the value is later\n * reverted to the default — only {@link FormStore.reset} clears it.\n */\n readonly isDirty: boolean;\n /** Whether the current value is (still, or again) equal to the field's default. */\n readonly isDefaultValue: boolean;\n}\n\n/**\n * A typed, reactive handle to one field, returned by {@link FormStore.field}.\n * Bundles everything a single input needs — `bind:value={field.value}`,\n * `name={field.name}`, `onblur={field.handleBlur}`, `{...field.constraints}`,\n * `field.visibleErrors`, `field.meta` — instead of wiring each by hand against\n * an untyped {@link FormPath}. See {@link Path.FieldPath} for how the path is\n * type-checked.\n */\nexport class FieldHandle<V> {\n readonly #store: FormStore<Schema.Top, object, unknown>;\n readonly #path: FormPath;\n /** The path in flat field-name notation — use as the input's `name` attribute. */\n readonly name: string;\n\n constructor(store: FormStore<Schema.Top, object, unknown>, path: FormPath) {\n this.#store = store;\n this.#path = path;\n this.name = Path.toString(path);\n }\n\n /** The field's current value. Assignable — `bind:value={field.value}` works. */\n // fallow-ignore-next-line unused-class-member -- consumed through the EffectFieldLike structural mirror in packages/components/src/fields/field-types.ts; the components→effect-forms boundary forbids a direct type edge (see docs/decisions/effect-native-form-toolkit.md), so fallow can't trace the interface-typed call site.\n get value(): V {\n return this.#store.valueAt(this.#path) as V;\n }\n\n // fallow-ignore-next-line unused-class-member -- same structural-mirror consumer as the getter above.\n set value(next: V) {\n this.#store.setValue(this.#path, next);\n }\n\n /** All messages currently attached to this field. */\n get errors(): readonly string[] {\n return this.#store.errorsAt(this.#path);\n }\n\n /** This field's messages, gated by the form's validation mode (see {@link FormStore.shouldShowError}). */\n // fallow-ignore-next-line unused-class-member -- consumed through the EffectFieldLike structural mirror in packages/components/src/fields/field-types.ts; see docs/decisions/effect-native-form-toolkit.md.\n get visibleErrors(): readonly string[] {\n return this.#store.visibleErrorsAt(this.#path);\n }\n\n /** Native validation attributes for this field — spread onto the `<input>`. */\n get constraints(): InputConstraint {\n return this.#store.constraintAt(this.#path);\n }\n\n /** This field's {@link FieldMeta}. */\n // fallow-ignore-next-line unused-class-member -- documented per-field API (see the FieldHandle class doc); no in-repo caller yet, kept per docs/decisions/effect-native-form-toolkit.md.\n get meta(): FieldMeta {\n const isBlurred = this.#store.isTouchedAt(this.#path);\n const isDirty = this.#store.isDirtyAt(this.#path);\n return {\n isTouched: isBlurred || isDirty,\n isBlurred,\n isDirty,\n isDefaultValue: this.#store.isDefaultValueAt(this.#path),\n };\n }\n\n /** Mark this field blurred — see {@link FormStore.handleBlur}. */\n // fallow-ignore-next-line unused-class-member -- consumed through the EffectFieldLike structural mirror in packages/components/src/fields/field-types.ts; see docs/decisions/effect-native-form-toolkit.md.\n handleBlur = (): void => {\n this.#store.handleBlur(this.#path);\n };\n}\n\n/** Transport: turn a submission into a {@link FormActionResult}. Defaults to `fetch`. */\nexport type SubmitFn<A, Message> = (\n body: FormData,\n action: string,\n form: HTMLFormElement,\n) => Promise<FormActionResult<A, Message>>;\n\n/** The submission passed to an {@link FormOptions.onSubmit} handler. */\nexport interface Submission<A, Message> {\n /** The current (reactive) form data. */\n readonly data: A;\n /** The multipart body that would be posted. */\n readonly formData: FormData;\n /** The form element being submitted. */\n readonly form: HTMLFormElement;\n /**\n * The submit button that triggered this submission (its `name`/`value` is\n * already folded into {@link Submission.formData}, matching native\n * `new FormData(form, submitter)` semantics), or `null` for an auto-submit\n * (no button was clicked).\n */\n readonly submitter: HTMLElement | null;\n /** Abort the submission entirely (no network request, no result applied). */\n readonly cancel: () => void;\n /**\n * Resolve the submission with a result computed in JS (e.g. via an API\n * client), instead of letting the default network POST run. The result is\n * applied to the store exactly as a server response would be.\n */\n readonly resolve: (result: FormActionResult<A, Message>) => void;\n}\n\n/** Options for {@link createForm}. */\nexport interface FormOptions<S extends Schema.Top, A, Message, R = never> {\n /** The schema to validate against in the browser (defaults to the bound schema). */\n readonly schema?: S;\n /**\n * When the form validates as the user types (and whether it auto-submits) —\n * see {@link module:effect-forms/Mode}. Defaults to `onSubmit`. `onChange` /\n * `onBlur` auto-validate the form; use {@link FormStore.shouldShowError} /\n * {@link FormStore.visibleErrorsAt} to gate display per field.\n */\n readonly mode?: FormMode;\n /**\n * The builder's field record, set automatically when {@link createForm} is\n * given a {@link FormBuilder}. A fallback for deriving a blank array item\n * when no `schema` is bound — with a schema, {@link FormStore.append} /\n * {@link FormStore.insertItem} resolve the item (at any nesting depth) by\n * walking the schema's AST instead.\n */\n readonly fields?: Field.FieldsRecord;\n /**\n * The builder itself, set automatically when {@link createForm} is given a\n * {@link FormBuilder}. Its `refineEffect` refinements run client-side as a\n * whole-form async validator whenever {@link FormStore.validate} runs.\n */\n readonly builder?: FormBuilder.FormBuilder<Field.FieldsRecord, R>;\n /** The action URL the form posts to (defaults to the current page's action). */\n readonly action?: string;\n /** Reset `data` back to the initial values after a successful submit. */\n readonly resetForm?: boolean;\n /** Warn (via `beforeunload`) when navigating away with unsaved changes. */\n readonly taintedMessage?: string | false;\n /** Milliseconds before an in-flight submission flips `delayed` to `true`. */\n readonly delayMs?: number;\n /** Override the message rendering used by client-side validation. */\n readonly validationFormat?: LeafFormatter;\n /** Override how a submission is sent (default: `fetch` to the form's action). */\n readonly submit?: SubmitFn<A, Message>;\n /**\n * Per-field sync/async validators, keyed by {@link Path.toString} (e.g.\n * `\"author.name\"`, `\"tags[0]\"`). See {@link FormStore.setFieldValidators} to\n * register these dynamically instead (e.g. once an array item exists).\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly validators?: Record<string, FieldValidators<any, R>>;\n /**\n * Services an async field validator or a builder's `refineEffect`\n * refinement needs on its `R` channel. A `ManagedRuntime` is built from\n * this once per store and reused for every client-side Effect run.\n */\n readonly layer?: Layer.Layer<R>;\n /**\n * Handle the submission entirely in JS: given the current data, return the\n * {@link FormActionResult} to apply (e.g. by calling an API client). When set,\n * this runs instead of the network POST — the \"handled using JS on the page\"\n * path. `onSubmit`'s `cancel()` / `resolve()` still take precedence.\n */\n readonly handler?: (\n data: A,\n ) => FormActionResult<A, Message> | Promise<FormActionResult<A, Message>>;\n /**\n * Handle the submission in JS on the page. Receives the data + form, can\n * `cancel()` it or `resolve(result)` it directly; if it neither cancels nor\n * resolves, the default network POST runs.\n */\n readonly onSubmit?: (input: Submission<A, Message>) => void | Promise<void>;\n /** Called with the raw {@link FormActionResult} once the response is parsed. */\n readonly onResult?: (input: { readonly result: FormActionResult<A, Message> }) => void;\n /** Called with the computed {@link FormUpdate} before it is applied. */\n readonly onUpdate?: (input: {\n readonly update: FormUpdate<A, Message>;\n readonly result: FormActionResult<A, Message>;\n }) => void;\n /** Called after the update has been applied to the store. */\n readonly onUpdated?: (input: {\n readonly form: { readonly data: A; readonly errors: FormErrors; readonly valid: boolean };\n }) => void;\n /** Called on an unexpected (non-user-fixable) error. */\n readonly onError?: (input: { readonly error: string }) => void;\n /** Called on a redirect result (default: `window.location.assign`). */\n readonly onRedirect?: (input: { readonly location: string }) => void;\n /**\n * Field- and form-level listeners (onMount / onChange / onBlur) — see\n * {@link module:effect-forms-svelte/Listeners}. Fire independent of\n * `mode`'s validation, and tear down with {@link FormStore.enhance}'s\n * `destroy`.\n */\n readonly listeners?: FormListeners;\n}\n\nconst defaultSubmit = async <A, Message>(\n body: FormData,\n action: string,\n): Promise<FormActionResult<A, Message>> => {\n const response = await fetch(action, {\n method: \"POST\",\n body,\n headers: { accept: \"application/json\" },\n });\n let parsed: unknown;\n try {\n parsed = await response.json();\n } catch {\n return {\n _tag: \"Error\",\n status: response.status,\n message: response.statusText || \"Request failed\",\n };\n }\n return parseActionResult<A, Message>(parsed, response.status);\n};\n\n/** Whether `index` is a valid position in an array of `length`. */\nconst inRange = (index: number, length: number): boolean => index >= 0 && index < length;\n\n/** A field trigger a validator or the revalidate mode reacts to. */\ntype FieldTrigger = \"change\" | \"blur\";\n\n/** A reactive form, returned by {@link createForm}. */\nexport class FormStore<S extends Schema.Top, A = S[\"Type\"], Message = unknown, R = never> {\n /** The reactive form data — bind inputs to its fields. */\n data: A = $state()!;\n /** The current status message, if any. */\n message: Message | undefined = $state();\n /** Native input constraints derived server-side. */\n constraints: InputConstraints = $state({});\n /** Whether a submission is in flight. */\n submitting = $state(false);\n /** Whether the in-flight submission has passed `delayMs`. */\n delayed = $state(false);\n /** Whether the current state came from a submission. */\n posted = $state(false);\n /** Whether the last validation/submit was valid. */\n valid = $state(false);\n /** How many times {@link FormStore.enhance}'s submit handler has actually attempted a submission. */\n submissionAttempts = $state(0);\n\n /** The values sent by the last submission (for revert / changed-since-submit). */\n #lastSubmitted: A | undefined = $state();\n /**\n * Per-field blurred flags, keyed by {@link Path.toString}. Drives `onBlur`\n * display (via {@link shouldShowError}) and {@link FieldMeta.isBlurred}.\n */\n #touched: Record<string, boolean> = $state({});\n /**\n * Per-field persistent dirty flags, keyed by {@link Path.toString}. Set once\n * a field's value is changed away from its default via {@link setValue} or\n * an array mutator (`append` / `removeItem` / `swap` / `move`), and never\n * cleared except by {@link reset} — unlike {@link taintedAt}, a revert to the\n * default value does not clear it. Backs {@link FieldMeta.isDirty}. Only\n * tracks writes made through those methods — mutating `data` directly\n * (`bind:value={form.data.x}`, bypassing {@link FormStore.field}) never marks\n * a field dirty.\n */\n #dirtied: Record<string, boolean> = $state({});\n /** Parallel error trees — see {@link Errors.SourcedErrors}. `errors` is their merged view. */\n #sourced: Errors.SourcedErrors = $state(emptySourced);\n /** Whether *any* validation (whole-form or per-field) has run yet — see {@link canSubmit}. */\n #hasValidated = $state(false);\n /** Per-field / whole-form (\"\") async-validation-in-flight flags, keyed by `\"<path>:<trigger>\"` or `\"\"`. */\n #validating: Record<string, boolean> = $state({});\n /** Per-key run counter guarding `#validating` writes — see {@link FormStore.#runValidatorFiber}. */\n #validatingGeneration: Record<string, number> = {};\n /** The outcome of the last completed submission, for {@link isSubmitSuccessful}. */\n #lastSubmitOutcome: \"success\" | \"failure\" | undefined = $state(undefined);\n\n readonly id: string;\n /** The tainted baseline `tainted`/`isTainted` compare against — rebased on every successful submit. */\n #initial: A;\n readonly #options: FormOptions<S, A, Message, R>;\n readonly #mode: Mode.ParsedMode;\n readonly #attachmentKey = createAttachmentKey();\n readonly #runtime: ManagedRuntime.ManagedRuntime<R, never>;\n #autoSubmitNode: HTMLFormElement | undefined;\n #validateTimer: ReturnType<typeof setTimeout> | undefined;\n /** Per-field-listener debounce timers, keyed by `\"change:<path>\"` / `\"blur:<path>\"`. */\n readonly #listenerTimers = new Map<string, ReturnType<typeof setTimeout>>();\n /** Per-field validators, keyed by {@link Path.toString}; seeded from `options.validators`. */\n #fieldValidators: Record<string, FieldValidators<unknown, R>>;\n /** In-flight async-validator fibers, keyed by `\"<path>:<trigger>\"` (fields) or `\"\"` (whole-form `refineEffect`). */\n #fibers: Record<string, Fiber.Fiber<unknown, never>> = {};\n /** The error paths the last `refineEffect` run set, so the next run can clear exactly those before merging fresh ones. */\n #lastRefinementPaths: FormPath[] = [];\n /** The data snapshot the field-tracking watcher last diffed against. */\n #previousData: A;\n\n /** The current nested error tree (server errors merged with client errors), typed to the form data's shape. */\n readonly errors: TypedFormErrors<A> = $derived.by(() => mergeSourced(this.#sourced) as never);\n /** Client-side-sourced errors only (schema, per-field validators, `refineEffect`). */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n readonly clientErrors: TypedFormErrors<A> = $derived.by(() => this.#sourced.client as never);\n /** Server-sourced errors only (the last submission's response). */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n readonly serverErrors: TypedFormErrors<A> = $derived.by(() => this.#sourced.server as never);\n /** Whether any async field validator or `refineEffect` run is currently in flight. */\n readonly isValidating: boolean = $derived(Object.values(this.#validating).some(Boolean));\n /** Whether the form has ever been submitted (attempted). */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n readonly isSubmitted: boolean = $derived(this.submissionAttempts > 0);\n /** Whether the most recently *completed* submission succeeded. `false` before any submit completes. */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n readonly isSubmitSuccessful: boolean = $derived(this.#lastSubmitOutcome === \"success\");\n /**\n * Whether the submit button should be enabled: not mid-submit, not\n * mid-validation, and either nothing has been validated yet (a pristine\n * form isn't presumptively invalid — the server gets the first look) or the\n * last validation run said valid.\n */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n readonly canSubmit: boolean = $derived(\n !this.submitting && !this.isValidating && (!this.#hasValidated || this.valid),\n );\n\n /** The tainted-field tree, re-derived as `data` changes. */\n readonly tainted: TaintedFields = $derived.by(() => computeTainted(this.#initial, this.data));\n /** Whether any field has been changed since the form was seeded. */\n readonly isTainted: boolean = $derived(isAnyTainted(this.tainted));\n /** The tree of fields changed since the last submission (empty before any submit). */\n readonly changedSinceSubmit: TaintedFields = $derived.by(() =>\n this.#lastSubmitted === undefined ? {} : computeTainted(this.#lastSubmitted, this.data),\n );\n /** Whether anything has changed since the last submission. */\n readonly hasChangedSinceSubmit: boolean = $derived(isAnyTainted(this.changedSinceSubmit));\n\n constructor(initial: FormState<A, Message>, options: FormOptions<S, A, Message, R> = {}) {\n this.id = initial.id;\n this.#initial = structuredClone(initial.data);\n this.#previousData = structuredClone(initial.data);\n this.#options = options;\n this.#mode = Mode.parse(options.mode);\n // `Layer.empty` is `Layer<never>`; when `options.layer` is omitted the\n // caller is asserting `R` is `never` (no validator / refineEffect\n // refinement on this store demands a service) — the same \"caller\n // asserts\" contract `validateValueSync` documents for a sync-only schema.\n this.#runtime = ManagedRuntime.make(options.layer ?? (Layer.empty as Layer.Layer<R>));\n this.#fieldValidators = {\n ...(options.validators as Record<string, FieldValidators<unknown, R>> | undefined),\n };\n this.data = structuredClone(initial.data);\n this.#sourced = { client: noErrors, server: (initial.errors ?? noErrors) as FormErrors };\n this.message = initial.message;\n // The `{ schema, defaults }` overload always arrives with `constraints`\n // already derived (via the core's `Form.make`); the `FormState`-hydration\n // overload's caller may not have supplied any, so derive from\n // `options.schema` here too — the two overloads must be symmetric.\n this.constraints =\n initial.constraints ??\n (options.schema ? Constraints.constraintsFromSchema(options.schema) : {});\n this.posted = initial.posted;\n this.valid = initial.valid;\n // `canSubmit` treats \"never validated\" as presumptively valid (a pristine\n // form isn't guilty until proven so); hydrating from a `posted` state\n // means something already *has* judged it, so `valid` must be trusted\n // from the start — otherwise a server-rejected `FormState` briefly (or\n // permanently, if nothing re-triggers validation) reports `canSubmit: true`.\n this.#hasValidated = initial.posted;\n }\n\n /**\n * Spreadable attributes for the `<form>` element: `<form {...form.props}>`.\n * Sets `method=\"post\"` (and `action`, if configured) and attaches the\n * progressive-enhancement submit handler via a Svelte attachment.\n */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n get props(): Record<string | symbol, unknown> {\n return {\n method: \"post\",\n ...(this.#options.action !== undefined ? { action: this.#options.action } : {}),\n [this.#attachmentKey]: this.#enhanceAttachment,\n };\n }\n\n /** Whether the field at `path` (or anything beneath it) is tainted. */\n taintedAt(path: FormPath): boolean {\n return isTaintedAt(this.tainted, path);\n }\n\n /** The messages currently attached to `path` (for dynamic / nested paths). */\n errorsAt(path: FormPath): readonly string[] {\n return Errors.getError(this.errors as FormErrors, path) ?? [];\n }\n\n /** Whether the async validator(s) at `path` are currently running. */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n isValidatingAt(path: FormPath): boolean {\n const key = Path.toString(path);\n return this.#validating[`${key}:change`] === true || this.#validating[`${key}:blur`] === true;\n }\n\n /**\n * Register (or replace) `path`'s validators — the option-driven equivalent\n * of attaching `validators` at {@link createForm} time, for fields that\n * don't exist until later (e.g. an appended array item).\n */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n setFieldValidators(path: FormPath, validators: FieldValidators<unknown, R>): void {\n this.#fieldValidators = { ...this.#fieldValidators, [Path.toString(path)]: validators };\n }\n\n /** Re-validate the whole form against the schema (if one was supplied), plus a builder's `refineEffect` refinements. */\n validate(): boolean {\n if (this.#options.schema) {\n const clientErrors = validateData(\n this.#options.schema,\n this.data,\n this.#options.validationFormat,\n );\n this.#sourced = { ...this.#sourced, client: clientErrors };\n this.#hasValidated = true;\n this.#recomputeValid();\n }\n this.#runRefineEffect();\n return this.valid;\n }\n\n /** Validate a single field against the schema, returning that field's messages. */\n validateField(path: FormPath): readonly string[] {\n if (!this.#options.schema) return this.errorsAt(path);\n return validateField(this.#options.schema, this.data, path, this.#options.validationFormat);\n }\n\n /** Set the value at `path` (path-addressed, immutable). Bind directly to `data` otherwise. */\n setValue(path: FormPath, value: unknown): void {\n this.data = setAt(this.data, path, value);\n this.#markDirty(path);\n this.#previousData = this.#detachedData();\n this.#onFieldChanged(path);\n }\n\n /** Read the value at `path`. */\n valueAt(path: FormPath): unknown {\n return getAt(this.data, path);\n }\n\n /** Mark the field at `path` touched (defaults to `true`) — drives `onBlur` display. */\n setTouched(path: FormPath, touched = true): void {\n this.#touched = { ...this.#touched, [Path.toString(path)]: touched };\n }\n\n /** Whether the field at `path` has been touched. */\n isTouchedAt(path: FormPath): boolean {\n return this.#touched[Path.toString(path)] === true;\n }\n\n /**\n * Once a change away from the default at `path` is seen (per the current\n * `data`, against `taintedAt`), sticks until {@link reset}. Called after\n * `data` has already been updated, by every method that writes it\n * (`setValue` and the array mutators below) — writing `data` directly\n * (`bind:value={form.data.x}`) bypasses this, same as it bypasses `tainted`\n * recomputing off anything but `data` itself would.\n */\n #markDirty(path: FormPath): void {\n const key = Path.toString(path);\n if (!this.#dirtied[key] && this.taintedAt(path)) {\n this.#dirtied = { ...this.#dirtied, [key]: true };\n }\n }\n\n /** Whether the field at `path` was ever changed away from its default — see {@link FieldMeta.isDirty}. */\n isDirtyAt(path: FormPath): boolean {\n return this.#dirtied[Path.toString(path)] === true;\n }\n\n /** Whether the field at `path` currently holds its default value — the negation of {@link taintedAt}. */\n isDefaultValueAt(path: FormPath): boolean {\n return !this.taintedAt(path);\n }\n\n /** The native validation attributes for `path` (a leaf of {@link constraints}). */\n constraintAt(path: FormPath): InputConstraint {\n return Constraints.constraintAt(this.constraints, path);\n }\n\n /**\n * A typed, reactive handle to the field at `path` — see {@link FieldHandle}.\n * `path` is type-checked against the form data's shape (see {@link\n * Path.FieldPath}); a typo'd segment or wrong-shaped path is a compile error.\n */\n field<P extends Path.FieldPath<A>>(path: P): FieldHandle<Path.PathValue<A, P>> {\n return new FieldHandle(\n this as unknown as FormStore<Schema.Top, object, unknown>,\n Path.fromString(path),\n );\n }\n\n /**\n * Handle a field losing focus: mark it touched, run its `onBlur` /\n * `onBlurAsync` validators (or, once posted, the revalidate-on-blur mode),\n * fire that field's `onBlur` listener (if any, debounced per its config),\n * and — in `onBlur` validation mode — validate the whole form\n * (auto-submitting if configured and something changed since the last\n * submit).\n */\n handleBlur(path: FormPath): void {\n this.setTouched(path, true);\n this.#onFieldBlurred(path);\n const key = Path.toString(path);\n const field = this.#options.listeners?.fields?.[key];\n if (field?.onBlur) {\n // Capture the value at blur time, not at timer-fire time — otherwise a\n // debounced listener would report whatever the field holds *now*, not\n // what it held when it was blurred.\n const value = this.valueAt(path);\n this.#scheduleListener(`blur:${key}`, field.debounce, () => field.onBlur?.({ path, value }));\n }\n if (this.#mode.validation === \"onBlur\") {\n this.validate();\n if (this.#mode.autoSubmit && this.valid && this.#hasUnsubmittedChanges()) this.#autoSubmit();\n }\n }\n\n /** Run `run` now, or after `debounce` (per-key timer, replacing any pending run for that key). */\n #scheduleListener(key: string, debounce: Duration.Input | undefined, run: () => void): void {\n if (debounce === undefined) {\n run();\n return;\n }\n clearTimeout(this.#listenerTimers.get(key));\n this.#listenerTimers.set(\n key,\n setTimeout(() => {\n this.#listenerTimers.delete(key);\n run();\n }, Duration.toMillis(debounce)),\n );\n }\n\n /**\n * The listeners table to dispatch change events against, with each field's\n * `onChange` wrapped in this store's own debounce timer — or `undefined` if\n * nothing is configured to consume a change (an `onMount`-only config, say),\n * so the deep watcher can skip diffing the data tree entirely. Built once\n * per {@link enhance} call rather than per change, since the listener table\n * itself doesn't change across the mount's lifetime.\n */\n #buildDispatchListeners(): FormListeners | undefined {\n const listeners = this.#options.listeners;\n if (!listeners || (listeners.onChange === undefined && !listeners.fields)) return undefined;\n if (!listeners.fields) return listeners;\n const fields: Record<string, FieldListener> = {};\n for (const [key, field] of Object.entries(listeners.fields)) {\n fields[key] = field.onChange\n ? {\n ...field,\n onChange: (input) =>\n this.#scheduleListener(`change:${key}`, field.debounce, () =>\n field.onChange?.(input),\n ),\n }\n : field;\n }\n return { ...listeners, fields };\n }\n\n /**\n * Whether there are edits worth auto-submitting: before any submit, any\n * change from the seeded values; afterwards, any change since the last\n * submission. Stops `autoSubmit` from re-posting server-echoed data in a loop.\n */\n #hasUnsubmittedChanges(): boolean {\n return this.#lastSubmitted === undefined ? this.isTainted : this.hasChangedSinceSubmit;\n }\n\n /**\n * Whether an error at `path` should be shown yet, per the validation mode:\n * always after a submit; in `onChange` once the field is tainted; in `onBlur`\n * once it is touched; never in `onSubmit` before posting.\n */\n shouldShowError(path: FormPath): boolean {\n if (this.posted) return true;\n if (this.#mode.validation === \"onChange\") return this.taintedAt(path);\n if (this.#mode.validation === \"onBlur\") return this.isTouchedAt(path);\n return false;\n }\n\n /** The messages at `path`, but only when {@link shouldShowError} allows it. */\n visibleErrorsAt(path: FormPath): readonly string[] {\n return this.shouldShowError(path) ? this.errorsAt(path) : [];\n }\n\n /** Read the array at `path` (empty when absent). */\n #arrayAt(path: FormPath): readonly unknown[] {\n return (getAt(this.data, path) ?? []) as readonly unknown[];\n }\n\n /** Append an item to the array at `path` (a blank item is derived when omitted). */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n append(path: FormPath, value?: unknown): void {\n const current = this.#arrayAt(path);\n const item = value !== undefined ? value : this.#defaultItem(path);\n this.data = setAt(this.data, path, [...current, item]);\n this.#markDirty(path);\n this.#previousData = this.#detachedData();\n this.#onFieldChanged(path);\n }\n\n /**\n * Insert an item into the array at `path` at `index` (clamped to the\n * array's bounds; a non-finite `index` inserts at the end), shifting later\n * items — and their per-index touched flags / errors — up by one. A blank\n * item is derived when `value` is omitted.\n */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n insertItem(path: FormPath, index: number, value?: unknown): void {\n const current = this.#arrayAt(path);\n const at = Math.max(\n 0,\n Math.min(Number.isFinite(index) ? index : current.length, current.length),\n );\n const item = value !== undefined ? value : this.#defaultItem(path);\n this.data = setAt(this.data, path, [...current.slice(0, at), item, ...current.slice(at)]);\n this.#markDirty(path);\n this.#remapArrayIndices(path, Path.insertAt(at));\n }\n\n /**\n * Replace the item at `index` in the array at `path` in place. Drops that\n * index's own touched flag / errors (they described the old value); other\n * indices are untouched.\n */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n replaceItem(path: FormPath, index: number, value: unknown): void {\n const current = this.#arrayAt(path);\n if (!inRange(index, current.length)) return;\n const next = [...current];\n next[index] = value;\n this.data = setAt(this.data, path, next);\n this.#markDirty(path);\n this.#remapArrayIndices(path, Path.replaceAt(index));\n }\n\n /** Remove every item from the array at `path`, dropping all of its per-index state. */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n clearItems(path: FormPath): void {\n this.data = setAt(this.data, path, []);\n this.#markDirty(path);\n this.#remapArrayIndices(path, Path.clearAll);\n }\n\n /**\n * Remove the item at `index` from the array at `path`, shifting later\n * items' per-index touched flags / errors down by one so they keep\n * following their row.\n */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n removeItem(path: FormPath, index: number): void {\n const current = this.#arrayAt(path);\n if (!inRange(index, current.length)) return;\n this.data = setAt(\n this.data,\n path,\n current.filter((_, i) => i !== index),\n );\n this.#markDirty(path);\n this.#remapArrayIndices(path, Path.removeAt(index));\n this.#previousData = this.#detachedData();\n this.#onFieldChanged(path);\n }\n\n /** Swap two items in the array at `path`, swapping their per-index touched flags / errors too. */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n swap(path: FormPath, indexA: number, indexB: number): void {\n const current = this.#arrayAt(path);\n if (!inRange(indexA, current.length) || !inRange(indexB, current.length) || indexA === indexB) {\n return;\n }\n const next = [...current];\n [next[indexA], next[indexB]] = [next[indexB], next[indexA]];\n this.data = setAt(this.data, path, next);\n this.#markDirty(path);\n this.#remapArrayIndices(path, Path.swapAt(indexA, indexB));\n this.#previousData = this.#detachedData();\n this.#onFieldChanged(path);\n }\n\n /** Move the item at `from` to `to` within the array at `path`, carrying its per-index state along. */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n move(path: FormPath, from: number, to: number): void {\n const current = this.#arrayAt(path);\n if (!inRange(from, current.length) || to < 0 || to >= current.length || from === to) return;\n const next = [...current];\n const [item] = next.splice(from, 1);\n next.splice(to, 0, item);\n this.data = setAt(this.data, path, next);\n this.#markDirty(path);\n this.#remapArrayIndices(path, Path.moveAt(from, to));\n this.#previousData = this.#detachedData();\n this.#onFieldChanged(path);\n }\n\n /**\n * Remap the per-index touched flags and errors under the array at `path`\n * through `map`, so state recorded against a row follows it through an\n * index-shifting mutation. See {@link Path.IndexMap}.\n */\n #remapArrayIndices(path: FormPath, map: Path.IndexMap): void {\n const sourced = this.#sourcedSnapshot();\n this.#sourced = {\n client: Errors.remapIndices(sourced.client, path, map),\n server: Errors.remapIndices(sourced.server, path, map),\n };\n this.#touched = Path.remapKeyedRecord(this.#touched, path, map);\n }\n\n /** Revert `data` to the values sent by the last submission (no-op before any submit). */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n revertToLastSubmit(): void {\n if (this.#lastSubmitted === undefined) return;\n // `#lastSubmitted` is `$state`; reading it yields a proxy `structuredClone`\n // rejects — snapshot first (same rule as `#detachedData`).\n this.data = structuredClone($state.snapshot(this.#lastSubmitted)) as A;\n this.#previousData = this.#detachedData();\n this.#sourced = emptySourced;\n }\n\n /**\n * Derive a blank item for the array field at `path`, at any nesting depth\n * (e.g. `authors[0].emails`) — by walking the bound schema's AST there when\n * one is available, falling back to a top-level {@link FormOptions.fields}\n * lookup, else `\"\"`.\n */\n #defaultItem(path: FormPath): unknown {\n if (this.#options.schema) return Field.defaultItemAt(this.#options.schema, path);\n const fields = this.#options.fields;\n if (fields && path.length === 1 && typeof path[0] === \"string\") {\n const def = fields[path[0]];\n if (def && Field.isArrayFieldDef(def)) return Field.defaultValue(def.itemSchema);\n }\n return \"\";\n }\n\n /** Submit via the enhanced node, if one is mounted and not already in flight. */\n #autoSubmit(): void {\n if (this.submitting || this.#autoSubmitNode === undefined) return;\n void this.#runSubmit(this.#autoSubmitNode);\n }\n\n /** Debounced (in `onChange`) whole-form validation, auto-submitting when valid. */\n #scheduleValidation(): void {\n const run = (): void => {\n this.validate();\n if (this.#mode.autoSubmit && this.valid && this.#hasUnsubmittedChanges()) this.#autoSubmit();\n };\n const ms = this.#mode.debounce;\n if (ms !== null && ms > 0) {\n clearTimeout(this.#validateTimer);\n this.#validateTimer = setTimeout(run, ms);\n } else {\n run();\n }\n }\n\n #recomputeValid(): void {\n this.valid = !hasErrors(mergeSourced(this.#sourced));\n }\n\n /**\n * A plain (non-`$state`-proxy) snapshot of `#sourced`. `clearSourceAt` /\n * `setSourceAt` / `replaceAt` in the framework-neutral core `structuredClone`\n * their `errors` argument — which throws `DataCloneError` on a live Svelte\n * `$state` proxy — so anywhere `#sourced` (or one of its two branches) feeds\n * one of those calls, snapshot it first.\n */\n #sourcedSnapshot(): Errors.SourcedErrors {\n return $state.snapshot(this.#sourced) as Errors.SourcedErrors;\n }\n\n /** Whether {@link Mode.ParsedMode.revalidate} names this field trigger. */\n #revalidatesOn(trigger: FieldTrigger): boolean {\n if (this.#mode.revalidate === \"never\") return false;\n return this.#mode.revalidate === \"onChange\" ? trigger === \"change\" : trigger === \"blur\";\n }\n\n /** Interrupt (fire-and-forget) any fiber running under `key`, if one is in flight. */\n #interruptFiber(key: string): void {\n const previous = this.#fibers[key];\n if (previous) Effect.runFork(Fiber.interrupt(previous));\n delete this.#fibers[key];\n }\n\n /**\n * Fork `effect` under `key` as an interrupt-on-supersede fiber, toggling\n * `#validating[key]` for its duration: any fiber already running under\n * `key` is interrupted first (this — not a timer — is what gives \"latest\n * wins\"; see {@link Validate.runAsyncFieldValidator}). The old and new\n * fiber's `#validating` writes are guarded by a per-key generation counter\n * — `Effect.runFork(Fiber.interrupt(previous))` is fire-and-forget, so the\n * *old* fiber's `Effect.ensuring` finalizer can run after the *new* one has\n * already flipped `#validating[key]` back to `true`; without the guard that\n * finalizer would clear it while the new run is still genuinely in flight.\n */\n #runValidatorFiber(key: string, effect: Effect.Effect<unknown, never, R>): void {\n this.#interruptFiber(key);\n const generation = (this.#validatingGeneration[key] ?? 0) + 1;\n this.#validatingGeneration[key] = generation;\n this.#validating = { ...this.#validating, [key]: true };\n\n const program = effect.pipe(\n Effect.ensuring(\n Effect.sync(() => {\n if (this.#validatingGeneration[key] === generation) {\n this.#validating = { ...this.#validating, [key]: false };\n }\n }),\n ),\n );\n\n this.#fibers[key] = this.#runtime.runFork(program);\n }\n\n /**\n * Handle a field's value actually changing: the field's server-sourced\n * error (if any) is superseded — clear it — then run whichever validators\n * apply for the `\"change\"` trigger.\n */\n #onFieldChanged(path: FormPath): void {\n this.#sourced = clearSourceAt(this.#sourcedSnapshot(), \"server\", path);\n this.#runFieldTrigger(path, \"change\");\n this.#recomputeValid();\n }\n\n /** Handle a field losing focus: run whichever validators apply for the `\"blur\"` trigger. */\n #onFieldBlurred(path: FormPath): void {\n this.#runFieldTrigger(path, \"blur\");\n this.#recomputeValid();\n }\n\n /**\n * Run `path`'s sync + async validators for `trigger` (resolved by {@link\n * Validate.resolveFieldTrigger}). When neither is registered, falls back to\n * a whole-schema {@link validateField} re-run once the form has been\n * posted and {@link Mode.ParsedMode.revalidate} names this trigger — the\n * \"fixing a field clears its error without a resubmit\" behaviour.\n */\n #runFieldTrigger(path: FormPath, trigger: FieldTrigger): void {\n const validators = this.#fieldValidators[Path.toString(path)];\n const value = getAt(this.data, path);\n const resolved = Validate.resolveFieldTrigger(validators, trigger);\n\n if (resolved.sync) {\n this.#applySyncFieldValidator(path, resolved.sync, value);\n } else if (this.#shouldFallBackToRevalidate(trigger)) {\n this.#applyRevalidateFallback(path);\n }\n\n if (resolved.async) {\n this.#hasValidated = true;\n this.#runAsyncFieldValidator(path, trigger, resolved.async, value, resolved.asyncDebounceMs);\n }\n }\n\n /** Run a field's registered sync validator and set its client messages. */\n #applySyncFieldValidator(\n path: FormPath,\n validator: Validate.SyncFieldValidator<unknown>,\n value: unknown,\n ): void {\n this.#setClientMessagesAt(path, Validate.runSyncFieldValidator(validator, value));\n this.#hasValidated = true;\n }\n\n /** Whether, absent a registered validator, a whole-schema fallback revalidation applies for `trigger`. */\n #shouldFallBackToRevalidate(trigger: FieldTrigger): boolean {\n return this.posted && this.#revalidatesOn(trigger) && this.#options.schema !== undefined;\n }\n\n /** Re-run the whole-schema client validation and adopt just `path`'s messages. */\n #applyRevalidateFallback(path: FormPath): void {\n this.#sourced = setSourceAt(this.#sourcedSnapshot(), \"client\", path, this.validateField(path));\n this.#hasValidated = true;\n }\n\n /** Replace `path`'s client messages with `issues`' messages (folding {@link FormIssue}s at the field's own path). */\n #setClientMessagesAt(path: FormPath, issues: ReadonlyArray<FormIssue>): void {\n this.#sourced = setSourceAt(\n this.#sourcedSnapshot(),\n \"client\",\n path,\n issues.map((issue) => issue.message),\n );\n }\n\n /**\n * Run `path`'s `trigger` async validator as an interrupt-on-supersede\n * fiber (via {@link FormStore.#runValidatorFiber}) — the result (unless\n * the fiber is itself later interrupted) replaces the field's client\n * messages.\n */\n #runAsyncFieldValidator(\n path: FormPath,\n trigger: FieldTrigger,\n validator: Validate.AsyncFieldValidator<unknown, R>,\n value: unknown,\n debounceMs: number,\n ): void {\n const fiberKey = `${Path.toString(path)}:${trigger}`;\n this.#runValidatorFiber(\n fiberKey,\n Validate.runAsyncFieldValidator(validator, value, debounceMs).pipe(\n Effect.tap((issues) =>\n Effect.sync(() => {\n this.#setClientMessagesAt(path, issues);\n this.#recomputeValid();\n }),\n ),\n ),\n );\n }\n\n /**\n * Run the builder's `refineEffect` refinements (client-side, when the form\n * was seeded from a {@link FormBuilder}) as a whole-form async validator —\n * interrupt-on-supersede, same as a field's async validator (see {@link\n * FormStore.#runValidatorFiber}). The previous run's issue *paths* are\n * cleared before the fresh ones are merged in, so a superseded run can't\n * leave stale messages behind without disturbing schema-derived client\n * errors set by {@link validate} in between.\n */\n #runRefineEffect(): void {\n const builder = this.#options.builder;\n if (!builder || FormBuilder.asyncRefinements(builder).length === 0) return;\n\n this.#runValidatorFiber(\n \"\",\n FormBuilder.runAsyncRefinements(builder, this.data as never).pipe(\n Effect.tap((issues) =>\n Effect.sync(() => {\n let client = this.#sourcedSnapshot().client;\n for (const path of this.#lastRefinementPaths) client = replaceAt(client, path, []);\n client = Errors.mergeErrors(client, Errors.issuesToErrors(issues));\n this.#lastRefinementPaths = issues.map((issue) => issue.path);\n this.#sourced = { ...this.#sourced, client };\n this.#recomputeValid();\n }),\n ),\n ),\n );\n }\n\n /**\n * Reset `data`, `errors`, and `message` back to the tainted baseline. Pass\n * `values` to re-seed that baseline first (e.g. adopting a different\n * entity's values) instead of returning to the original seed.\n */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n reset(values?: A): void {\n if (values !== undefined) this.#initial = structuredClone(values);\n for (const key of Object.keys(this.#fibers)) this.#interruptFiber(key);\n this.data = structuredClone(this.#initial);\n this.#previousData = this.#detachedData();\n this.#sourced = emptySourced;\n this.message = undefined;\n this.valid = false;\n this.posted = false;\n this.#touched = {};\n this.#dirtied = {};\n this.#lastSubmitted = undefined;\n this.#hasValidated = false;\n this.#validating = {};\n this.#lastRefinementPaths = [];\n this.submissionAttempts = 0;\n this.#lastSubmitOutcome = undefined;\n }\n\n /** A serialisable snapshot for SvelteKit's `snapshot` capture/restore. */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n capture(): FormState<A, Message> {\n return {\n id: this.id,\n valid: this.valid,\n posted: this.posted,\n data: this.#detachedData(),\n errors: this.errors as FormErrors,\n message: this.message,\n constraints: this.constraints,\n };\n }\n\n /**\n * An independent deep copy of `data`, detached from the live `$state`\n * proxy. `structuredClone` can't clone the proxy directly (Node's\n * structured-clone algorithm rejects it); take a plain snapshot first, per\n * Svelte's own guidance for this exact case.\n */\n #detachedData(): A {\n return structuredClone($state.snapshot(this.data)) as A;\n }\n\n /** Restore from a {@link capture} snapshot (its merged tree becomes the server tree; the client tree clears). */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n restore(snapshot: FormState<A, Message>): void {\n this.data = structuredClone(snapshot.data);\n this.#previousData = this.#detachedData();\n this.#sourced = { client: noErrors, server: snapshot.errors };\n this.message = snapshot.message;\n this.valid = snapshot.valid;\n this.posted = snapshot.posted;\n this.constraints = snapshot.constraints ?? {};\n this.#hasValidated = snapshot.posted;\n }\n\n /**\n * Apply server-sourced `data` + `errors` directly, bypassing a submit\n * round-trip — for glue code that repopulates the store from a derived\n * result it received some other way (e.g.\n * {@link module:effect-forms-svelte/SvelteKit}'s `applyActionResult`, from\n * a SvelteKit form action's failure). Sets `posted` so\n * {@link shouldShowError} shows the new errors immediately; `valid` is\n * always `false` (the caller only reaches for this on a failure result).\n */\n setServerState(data: A, errors: FormErrors): void {\n this.data = data;\n this.#previousData = structuredClone(data);\n this.#sourced = { client: noErrors, server: errors };\n this.valid = false;\n this.posted = true;\n this.#hasValidated = true;\n }\n\n #applyUpdate(update: FormUpdate<A, Message>, result: FormActionResult<A, Message>): void {\n this.#options.onUpdate?.({ update, result });\n if (update.redirect !== undefined) return this.#applyRedirect(update.redirect);\n if (update.error !== undefined) return this.#applyError(update.error);\n if (update.data !== undefined) this.#applySubmittedData(update.data, result);\n\n // A submission result is authoritative: it superseded whatever the client\n // last computed, so the client tree clears and the response becomes the\n // new server tree (server errors are then cleared field-by-field as the\n // user edits / revalidates — see #onFieldChanged / #runFieldTrigger).\n this.#sourced = { client: noErrors, server: (update.errors ?? noErrors) as FormErrors };\n this.#hasValidated = true;\n this.message = update.message;\n if (update.valid !== undefined) this.valid = update.valid;\n this.#lastSubmitOutcome = this.valid ? \"success\" : \"failure\";\n this.posted = true;\n this.#options.onUpdated?.({\n form: { data: this.data, errors: this.errors as FormErrors, valid: this.valid },\n });\n }\n\n #applyRedirect(location: string): void {\n this.#lastSubmitOutcome = \"success\";\n if (this.#options.onRedirect) this.#options.onRedirect({ location });\n else if (typeof window !== \"undefined\") window.location.assign(location);\n }\n\n #applyError(error: string): void {\n this.#lastSubmitOutcome = \"failure\";\n this.#options.onError?.({ error });\n }\n\n /**\n * Adopt (or discard) the submission's echoed data via {@link\n * computeDataApplication} and rebase the tainted baseline. An\n * auto-submitting (live-save) form keeps its local data as the source of\n * truth: adopting the server's echo would discard keystrokes made during\n * the round-trip and, when the echo isn't byte-identical (a trimmed or\n * server-stamped field), retrigger the change watcher — an extra submit,\n * or an endless loop for a non-idempotent echo. On a successful submit the\n * tainted baseline still rebases to whatever `data` ends up holding, so\n * `isTainted` (and the `beforeunload` guard it drives) reflects what was\n * actually just saved, not the original seed.\n */\n #applySubmittedData(data: A, result: FormActionResult<A, Message>): void {\n const applied = computeDataApplication({\n isSuccess: result._tag === \"Success\",\n appliedData: data,\n initial: this.#initial,\n // The snapshot actually sent (see `#runSubmit`), not the live `data` —\n // further edits may have landed while this request was in flight.\n // Detached (`$state.snapshot`) so the `structuredClone`s below never\n // receive a `$state` proxy via `applied.data` / `applied.rebaseTo`.\n submittedData: $state.snapshot(this.#lastSubmitted ?? this.data) as A,\n resetForm: this.#options.resetForm ?? false,\n autoSubmit: this.#mode.autoSubmit,\n });\n if (applied.data !== undefined) this.data = structuredClone(applied.data);\n if (applied.rebaseTo !== undefined) {\n this.#initial = structuredClone(applied.rebaseTo);\n this.#touched = {};\n }\n // Resync the field-tracking watcher's baseline to whatever `data` ends up\n // holding post-submit, so it doesn't re-diff (and re-run field validators\n // / re-clear server errors for) the submission's own echo.\n this.#previousData = this.#detachedData();\n }\n\n async #runSubmit(node: HTMLFormElement, submitter: HTMLElement | null = null): Promise<void> {\n // `new FormData(node, submitter)` folds the clicked button's name/value in,\n // matching the native no-JS POST; the auto-submit path calls this with no\n // submitter by definition, so it keeps its current (button-less) body.\n const formData = new FormData(node, submitter);\n let cancelled = false;\n let resolved: FormActionResult<A, Message> | undefined;\n await this.#options.onSubmit?.({\n data: this.data,\n formData,\n form: node,\n submitter,\n cancel: () => {\n cancelled = true;\n },\n resolve: (result) => {\n resolved = result;\n },\n });\n if (cancelled) return;\n\n // Snapshot what we're sending so `revertToLastSubmit` / `changedSinceSubmit`\n // measure against the submitted values.\n this.#lastSubmitted = this.#detachedData();\n this.submissionAttempts++;\n\n this.submitting = true;\n const delayMs = this.#options.delayMs ?? 500;\n const delayTimer = setTimeout(() => {\n this.delayed = true;\n }, delayMs);\n\n try {\n const submit = this.#options.submit ?? defaultSubmit;\n const action = node.action || (typeof location !== \"undefined\" ? location.href : \"\");\n let result: FormActionResult<A, Message>;\n if (resolved !== undefined) {\n result = resolved;\n } else if (this.#options.handler) {\n result = await this.#options.handler(this.data);\n } else {\n result = await submit(formData, action, node);\n }\n this.#options.onResult?.({ result });\n this.#applyUpdate(resultToUpdate(result), result);\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : \"Submission failed\";\n this.#lastSubmitOutcome = \"failure\";\n this.#options.onError?.({ error: message });\n } finally {\n clearTimeout(delayTimer);\n this.submitting = false;\n this.delayed = false;\n }\n }\n\n /**\n * Progressive-enhancement form action. Use as `<form use:form.enhance>` (or\n * spread {@link FormStore.props}). Intercepts submit, runs the `onSubmit`\n * handler, then sends the body and applies the result. Without JavaScript the\n * form posts normally.\n */\n enhance = (node: HTMLFormElement): { destroy: () => void } => {\n const handleSubmit = (event: SubmitEvent): void => {\n event.preventDefault();\n void this.#runSubmit(node, event.submitter);\n };\n node.addEventListener(\"submit\", handleSubmit);\n\n const beforeUnload = (event: BeforeUnloadEvent): void => {\n if (this.#options.taintedMessage && this.isTainted) event.preventDefault();\n };\n if (this.#options.taintedMessage && typeof window !== \"undefined\") {\n window.addEventListener(\"beforeunload\", beforeUnload);\n }\n\n // Remember the mounted node so `onChange` / `onBlur` auto-submit can post.\n this.#autoSubmitNode = node;\n\n this.#options.listeners?.onMount?.({ data: $state.snapshot(this.data) });\n\n // Two independent watchers over `data`, both rooted so they get explicit\n // teardown alongside the DOM listeners:\n // - the `onChange`-mode one re-validates (and maybe auto-submits) the\n // whole form, debounced by `mode.debounce`, and dispatches any\n // configured change listeners (independent of mode, so listeners fire\n // whichever mode the form validates in);\n // - the field-tracking one diffs against the last-seen snapshot and runs\n // per-field validators / clears superseded server errors for exactly\n // the paths that changed — independent of `mode.validation`, since a\n // field's own validators and the post-submit revalidate mode both\n // apply regardless of the whole-form trigger. Always on (not gated on\n // whether any validator is registered yet): `setFieldValidators` can\n // register one after mount (its documented use case — a field that\n // doesn't exist until an array item is appended), and a gate evaluated\n // only at mount time would silently never watch for it.\n const dispatchListeners = this.#buildDispatchListeners();\n let stopWatching: (() => void) | undefined;\n if (this.#mode.validation === \"onChange\" || dispatchListeners !== undefined) {\n let previous = $state.snapshot(this.data);\n stopWatching = $effect.root(() => {\n $effect(() => {\n // Establish a deep dependency on the data tree.\n const snapshot = $state.snapshot(this.data);\n if (this.#mode.validation === \"onChange\" && (this.isTainted || this.posted)) {\n this.#scheduleValidation();\n }\n if (dispatchListeners !== undefined)\n dispatchChange(previous, snapshot, dispatchListeners);\n previous = snapshot;\n });\n });\n }\n\n const stopFieldWatching = $effect.root(() => {\n $effect(() => {\n const snapshot = $state.snapshot(this.data) as A;\n const changed = taintedLeafPaths(computeTainted(this.#previousData, snapshot));\n this.#previousData = snapshot;\n for (const path of changed) this.#onFieldChanged(path);\n });\n });\n\n return {\n destroy: () => {\n node.removeEventListener(\"submit\", handleSubmit);\n if (typeof window !== \"undefined\") window.removeEventListener(\"beforeunload\", beforeUnload);\n stopWatching?.();\n stopFieldWatching?.();\n clearTimeout(this.#validateTimer);\n for (const timer of this.#listenerTimers.values()) clearTimeout(timer);\n this.#listenerTimers.clear();\n for (const key of Object.keys(this.#fibers)) this.#interruptFiber(key);\n if (this.#autoSubmitNode === node) this.#autoSubmitNode = undefined;\n },\n };\n };\n\n /** Attachment form of {@link FormStore.enhance} for {@link FormStore.props}. */\n readonly #enhanceAttachment = (node: Element): (() => void) =>\n this.enhance(node as HTMLFormElement).destroy;\n\n /** Build a {@link FormData} body from the current data (SPA / manual submit). */\n // fallow-ignore-next-line unused-class-member -- public library API, no in-repo caller yet; kept per docs/decisions/effect-native-form-toolkit.md.\n toFormData(): FormData {\n return objectToFormData(this.data);\n }\n}\n\n/** A schema plus its blank-form defaults — what {@link createForm} wraps `makeForm` over. */\nexport interface FormSeed<S extends Schema.Top> {\n readonly schema: S;\n readonly defaults: S[\"Type\"];\n readonly id?: string;\n}\n\n/** The built-schema type a {@link FormBuilder} of `TFields` / `R` collapses to. */\ntype BuiltSchema<TFields extends Field.FieldsRecord, R> = Schema.Codec<\n Field.DecodedFromFields<TFields>,\n Field.EncodedFromFields<TFields>,\n R\n>;\n\n/** Options for the {@link createForm} {@link FormBuilder} overload. */\nexport type FormBuilderOptions<TFields extends Field.FieldsRecord, R, Message> = FormOptions<\n BuiltSchema<TFields, R>,\n Field.EncodedFromFields<TFields>,\n Message,\n R\n> & {\n /** Blank-form values. Defaults to a schema-derived encoded blank per field. */\n readonly defaults?: Field.EncodedFromFields<TFields>;\n /** Distinguishes multiple forms on a page. Defaults to `\"default\"`. */\n readonly id?: string;\n};\n\n/**\n * Create a reactive {@link FormStore}. Three sources are accepted:\n *\n * - a {@link FormBuilder} — the schema, blank defaults, and array-item defaults\n * are all derived from the builder (client validation, including\n * `refineEffect` refinements, is wired automatically);\n * - `{ schema, defaults }` — seed a blank form (wrapping the core's `makeForm`);\n * - a server-produced {@link FormState} — hydrate from it (schema in `options`).\n */\nexport function createForm<TFields extends Field.FieldsRecord, R, Message = unknown>(\n builder: FormBuilder.FormBuilder<TFields, R>,\n options?: FormBuilderOptions<TFields, R, Message>,\n): FormStore<BuiltSchema<TFields, R>, Field.EncodedFromFields<TFields>, Message, R>;\nexport function createForm<S extends Schema.Top, Message = unknown, R = never>(\n seed: FormSeed<S>,\n options?: FormOptions<S, S[\"Type\"], Message, R>,\n): FormStore<S, S[\"Type\"], Message, R>;\nexport function createForm<S extends Schema.Top, A = S[\"Type\"], Message = unknown, R = never>(\n initial: FormState<A, Message>,\n options: FormOptions<S, A, Message, R>,\n): FormStore<S, A, Message, R>;\nexport function createForm(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n source: FormBuilder.FormBuilder<any, any> | FormSeed<Schema.Top> | FormState<unknown, unknown>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n options: FormOptions<Schema.Top, any, any, any> & {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n defaults?: any;\n id?: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } = {},\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n): FormStore<Schema.Top, any, any, any> {\n if (FormBuilder.isFormBuilder(source)) {\n const schema = FormBuilder.buildSchema(source) as Schema.Top;\n const form = Form.fromBuilder(source, {\n ...(options.defaults !== undefined ? { defaults: options.defaults as never } : {}),\n ...(options.id !== undefined ? { id: options.id } : {}),\n });\n return new FormStore(form.empty(), {\n ...options,\n schema,\n fields: source.fields,\n builder: source,\n });\n }\n const isSeed = \"defaults\" in source && \"schema\" in source;\n if (isSeed) {\n const seed = source as FormSeed<Schema.Top>;\n const state = makeForm(seed.schema, {\n defaults: seed.defaults,\n ...(seed.id !== undefined ? { id: seed.id } : {}),\n }).empty();\n return new FormStore(state, { ...options, schema: seed.schema });\n }\n return new FormStore(source as FormState<unknown, unknown>, options);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Issues } from "@thomasfosterau/effect-forms";
|
|
2
|
+
import * as Duration from "effect/Duration";
|
|
3
|
+
//#region src/Listeners.d.ts
|
|
4
|
+
type FormPath = Issues.FormPath;
|
|
5
|
+
/** One leaf that changed between two data snapshots. */
|
|
6
|
+
interface ListenerEvent {
|
|
7
|
+
/** The path to the changed leaf. */
|
|
8
|
+
readonly path: FormPath;
|
|
9
|
+
/** The leaf's value in the *later* snapshot. */
|
|
10
|
+
readonly value: unknown;
|
|
11
|
+
}
|
|
12
|
+
/** A field-level listener pair, keyed by {@link Path.toString} in {@link FormListeners.fields}. */
|
|
13
|
+
interface FieldListener {
|
|
14
|
+
/** Called when this field's value changes (after `debounce`, if set). */
|
|
15
|
+
readonly onChange?: (input: {
|
|
16
|
+
readonly path: FormPath;
|
|
17
|
+
readonly value: unknown;
|
|
18
|
+
}) => void;
|
|
19
|
+
/** Called when this field is blurred (after `debounce`, if set). */
|
|
20
|
+
readonly onBlur?: (input: {
|
|
21
|
+
readonly path: FormPath;
|
|
22
|
+
readonly value: unknown;
|
|
23
|
+
}) => void;
|
|
24
|
+
/** Debounce applied to both callbacks above. No debounce when omitted. */
|
|
25
|
+
readonly debounce?: Duration.Input;
|
|
26
|
+
}
|
|
27
|
+
/** Form-wide listener hooks, plus the field-keyed table. */
|
|
28
|
+
interface FormListeners {
|
|
29
|
+
/** Called once when the form mounts (i.e. when `enhance` attaches). */
|
|
30
|
+
readonly onMount?: (input: {
|
|
31
|
+
readonly data: unknown;
|
|
32
|
+
}) => void;
|
|
33
|
+
/** Called for every changed leaf, naming which path triggered it. */
|
|
34
|
+
readonly onChange?: (input: {
|
|
35
|
+
readonly path: FormPath;
|
|
36
|
+
readonly data: unknown;
|
|
37
|
+
}) => void;
|
|
38
|
+
/** Field-level listeners, keyed by {@link Path.toString} (e.g. `"author.name"`, `"tags[0]"`). */
|
|
39
|
+
readonly fields?: Record<string, FieldListener>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The leaf paths that changed from `before` to `after`, each with its new
|
|
43
|
+
* value. Built on {@link computeTainted}, so the same rules apply: array
|
|
44
|
+
* elements are diffed by index, and a container-vs-scalar kind change taints
|
|
45
|
+
* the whole field rather than recursing.
|
|
46
|
+
*/
|
|
47
|
+
declare function changeEvents(before: unknown, after: unknown): ListenerEvent[];
|
|
48
|
+
/**
|
|
49
|
+
* Fire the configured {@link FormListeners} for the transition from `before`
|
|
50
|
+
* to `after`: the form-level `onChange` for every changed leaf, then that
|
|
51
|
+
* leaf's field-level `onChange` if one is registered. Synchronous — debounce
|
|
52
|
+
* is the caller's concern (see the module doc).
|
|
53
|
+
*/
|
|
54
|
+
declare function dispatchChange(before: unknown, after: unknown, listeners: FormListeners | undefined): void;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { FieldListener, FormListeners, ListenerEvent, changeEvents, dispatchChange };
|
|
57
|
+
//# sourceMappingURL=Listeners.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { computeTainted, taintedLeafPaths } from "./Tainted.js";
|
|
2
|
+
import { Path } from "@thomasfosterau/effect-forms";
|
|
3
|
+
//#region src/Listeners.ts
|
|
4
|
+
/**
|
|
5
|
+
* The leaf paths that changed from `before` to `after`, each with its new
|
|
6
|
+
* value. Built on {@link computeTainted}, so the same rules apply: array
|
|
7
|
+
* elements are diffed by index, and a container-vs-scalar kind change taints
|
|
8
|
+
* the whole field rather than recursing.
|
|
9
|
+
*/
|
|
10
|
+
function changeEvents(before, after) {
|
|
11
|
+
return taintedLeafPaths(computeTainted(before, after)).map((path) => ({
|
|
12
|
+
path,
|
|
13
|
+
value: Path.getAt(after, path)
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Fire the configured {@link FormListeners} for the transition from `before`
|
|
18
|
+
* to `after`: the form-level `onChange` for every changed leaf, then that
|
|
19
|
+
* leaf's field-level `onChange` if one is registered. Synchronous — debounce
|
|
20
|
+
* is the caller's concern (see the module doc).
|
|
21
|
+
*/
|
|
22
|
+
function dispatchChange(before, after, listeners) {
|
|
23
|
+
if (!listeners) return;
|
|
24
|
+
for (const event of changeEvents(before, after)) {
|
|
25
|
+
listeners.onChange?.({
|
|
26
|
+
path: event.path,
|
|
27
|
+
data: after
|
|
28
|
+
});
|
|
29
|
+
listeners.fields?.[Path.toString(event.path)]?.onChange?.({
|
|
30
|
+
path: event.path,
|
|
31
|
+
value: event.value
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { changeEvents, dispatchChange };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=Listeners.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Listeners.js","names":[],"sources":["../src/Listeners.ts"],"sourcesContent":["/**\n * @copyright Thomas Foster\n *\n * Field- and form-level listeners — side-effecting hooks that fire as the\n * form's data changes, independent of validation. The model is TanStack\n * Form's listeners guide: a field can react to its own `onChange` / `onBlur`\n * (optionally debounced), and the form as a whole can react to `onMount` and\n * to any change, told which path triggered it. The classic use is linked\n * fields — a country field's `onChange` resetting a dependent province field.\n *\n * This module is the pure half: {@link changeEvents} diffs two data\n * snapshots (reusing {@link module:effect-forms-svelte/Tainted}'s tree) into\n * the flat list of changed leaf paths + their new values, and\n * {@link dispatchChange} fires the configured listeners for that diff. Both\n * are synchronous and side-effect-free beyond invoking the caller's own\n * callbacks — no timers, no Svelte. The reactive store (`FormStore.svelte.ts`)\n * is the impure half: it re-derives the diff off its deep `$effect` watcher,\n * wraps each field listener in its own debounce timer, and owns `onBlur` /\n * `onMount` timing.\n *\n * Because {@link dispatchChange} only ever compares the *immediately prior*\n * snapshot against the current one (never the form's original values), a\n * listener that mutates data — like the linked-fields recipe — cannot\n * re-trigger itself: the watcher's next pass diffs against the mutation it\n * just produced, so only the fields that actually moved in *that* transition\n * fire again. See `Listeners.test.ts` for the two-pass trace.\n *\n * @module effect-forms-svelte/Listeners\n */\n\nimport type { Issues } from \"@thomasfosterau/effect-forms\";\nimport { Path } from \"@thomasfosterau/effect-forms\";\nimport type * as Duration from \"effect/Duration\";\nimport { computeTainted, taintedLeafPaths } from \"./Tainted.ts\";\n\ntype FormPath = Issues.FormPath;\n\n/** One leaf that changed between two data snapshots. */\nexport interface ListenerEvent {\n /** The path to the changed leaf. */\n readonly path: FormPath;\n /** The leaf's value in the *later* snapshot. */\n readonly value: unknown;\n}\n\n/** A field-level listener pair, keyed by {@link Path.toString} in {@link FormListeners.fields}. */\nexport interface FieldListener {\n /** Called when this field's value changes (after `debounce`, if set). */\n readonly onChange?: (input: { readonly path: FormPath; readonly value: unknown }) => void;\n /** Called when this field is blurred (after `debounce`, if set). */\n readonly onBlur?: (input: { readonly path: FormPath; readonly value: unknown }) => void;\n /** Debounce applied to both callbacks above. No debounce when omitted. */\n readonly debounce?: Duration.Input;\n}\n\n/** Form-wide listener hooks, plus the field-keyed table. */\nexport interface FormListeners {\n /** Called once when the form mounts (i.e. when `enhance` attaches). */\n readonly onMount?: (input: { readonly data: unknown }) => void;\n /** Called for every changed leaf, naming which path triggered it. */\n readonly onChange?: (input: { readonly path: FormPath; readonly data: unknown }) => void;\n /** Field-level listeners, keyed by {@link Path.toString} (e.g. `\"author.name\"`, `\"tags[0]\"`). */\n readonly fields?: Record<string, FieldListener>;\n}\n\n/**\n * The leaf paths that changed from `before` to `after`, each with its new\n * value. Built on {@link computeTainted}, so the same rules apply: array\n * elements are diffed by index, and a container-vs-scalar kind change taints\n * the whole field rather than recursing.\n */\nexport function changeEvents(before: unknown, after: unknown): ListenerEvent[] {\n return taintedLeafPaths(computeTainted(before, after)).map((path) => ({\n path,\n value: Path.getAt(after, path),\n }));\n}\n\n/**\n * Fire the configured {@link FormListeners} for the transition from `before`\n * to `after`: the form-level `onChange` for every changed leaf, then that\n * leaf's field-level `onChange` if one is registered. Synchronous — debounce\n * is the caller's concern (see the module doc).\n */\nexport function dispatchChange(\n before: unknown,\n after: unknown,\n listeners: FormListeners | undefined,\n): void {\n if (!listeners) return;\n for (const event of changeEvents(before, after)) {\n listeners.onChange?.({ path: event.path, data: after });\n listeners.fields?.[Path.toString(event.path)]?.onChange?.({\n path: event.path,\n value: event.value,\n });\n }\n}\n"],"mappings":";;;;;;;;;AAuEA,SAAgB,aAAa,QAAiB,OAAiC;CAC7E,OAAO,iBAAiB,eAAe,QAAQ,KAAK,CAAC,CAAC,CAAC,KAAK,UAAU;EACpE;EACA,OAAO,KAAK,MAAM,OAAO,IAAI;CAC/B,EAAE;AACJ;;;;;;;AAQA,SAAgB,eACd,QACA,OACA,WACM;CACN,IAAI,CAAC,WAAW;CAChB,KAAK,MAAM,SAAS,aAAa,QAAQ,KAAK,GAAG;EAC/C,UAAU,WAAW;GAAE,MAAM,MAAM;GAAM,MAAM;EAAM,CAAC;EACtD,UAAU,SAAS,KAAK,SAAS,MAAM,IAAI,EAAE,EAAE,WAAW;GACxD,MAAM,MAAM;GACZ,OAAO,MAAM;EACf,CAAC;CACH;AACF"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FormStore } from "./FormStore.svelte.js";
|
|
2
|
+
import { Result } from "@thomasfosterau/effect-forms";
|
|
3
|
+
//#region src/SvelteKit.d.ts
|
|
4
|
+
type FormActionData<A = unknown, Message = unknown> = Result.FormActionData<A, Message>;
|
|
5
|
+
/**
|
|
6
|
+
* The minimal view of SvelteKit's `ActionResult` that {@link applyActionResult}
|
|
7
|
+
* inspects — a failure carries the derived action's {@link FormActionData}.
|
|
8
|
+
*/
|
|
9
|
+
interface ServerActionResult {
|
|
10
|
+
readonly type: "success" | "failure" | "redirect" | "error";
|
|
11
|
+
readonly data?: FormActionData | Record<string, unknown> | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Apply a derived form action's result to `form`. On a validation `failure`
|
|
15
|
+
* carrying `{ form }`, repopulate the store's errors + posted values and return
|
|
16
|
+
* `true` (consumed). Otherwise return `false` — the caller should defer to the
|
|
17
|
+
* framework (`applyAction`) for success / redirect / error.
|
|
18
|
+
*/
|
|
19
|
+
declare function applyActionResult(form: FormStore<any, any, any>, result: ServerActionResult): boolean;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { ServerActionResult, applyActionResult };
|
|
22
|
+
//# sourceMappingURL=SvelteKit.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/SvelteKit.ts
|
|
2
|
+
const isFormActionData = (data) => typeof data === "object" && data !== null && "form" in data && Boolean(data.form);
|
|
3
|
+
/**
|
|
4
|
+
* Apply a derived form action's result to `form`. On a validation `failure`
|
|
5
|
+
* carrying `{ form }`, repopulate the store's errors + posted values and return
|
|
6
|
+
* `true` (consumed). Otherwise return `false` — the caller should defer to the
|
|
7
|
+
* framework (`applyAction`) for success / redirect / error.
|
|
8
|
+
*/
|
|
9
|
+
function applyActionResult(form, result) {
|
|
10
|
+
if (result.type !== "failure" || !isFormActionData(result.data)) return false;
|
|
11
|
+
const state = result.data.form;
|
|
12
|
+
form.setServerState(state.data, state.errors);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { applyActionResult };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=SvelteKit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SvelteKit.js","names":[],"sources":["../src/SvelteKit.ts"],"sourcesContent":["/**\n * @copyright Thomas Foster\n *\n * SvelteKit interop for the form store. The framework's `$app/forms` `enhance`\n * / `applyAction` are app-build virtuals (not resolvable from a library), so\n * the SvelteKit-specific glue stays in the consuming app; this module owns the\n * reusable half — applying a derived form action's result back to the store.\n *\n * @module effect-forms-svelte/SvelteKit\n */\n\nimport type { Errors, Result, State } from \"@thomasfosterau/effect-forms\";\nimport type { FormStore } from \"./FormStore.svelte.ts\";\n\ntype FormActionData<A = unknown, Message = unknown> = Result.FormActionData<A, Message>;\ntype FormState<A, Message = unknown> = State.FormState<A, Message>;\ntype FormErrors = Errors.FormErrors;\n\n/**\n * The minimal view of SvelteKit's `ActionResult` that {@link applyActionResult}\n * inspects — a failure carries the derived action's {@link FormActionData}.\n */\nexport interface ServerActionResult {\n readonly type: \"success\" | \"failure\" | \"redirect\" | \"error\";\n readonly data?: FormActionData | Record<string, unknown> | undefined;\n}\n\nconst isFormActionData = (data: unknown): data is FormActionData & { form: FormState<unknown> } =>\n typeof data === \"object\" &&\n data !== null &&\n \"form\" in data &&\n Boolean((data as { form?: unknown }).form);\n\n/**\n * Apply a derived form action's result to `form`. On a validation `failure`\n * carrying `{ form }`, repopulate the store's errors + posted values and return\n * `true` (consumed). Otherwise return `false` — the caller should defer to the\n * framework (`applyAction`) for success / redirect / error.\n */\nexport function applyActionResult(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any concrete store\n form: FormStore<any, any, any>,\n result: ServerActionResult,\n): boolean {\n if (result.type !== \"failure\" || !isFormActionData(result.data)) return false;\n const state = result.data.form;\n form.setServerState(state.data, state.errors as FormErrors);\n return true;\n}\n"],"mappings":";AA2BA,MAAM,oBAAoB,SACxB,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,QAAS,KAA4B,IAAI;;;;;;;AAQ3C,SAAgB,kBAEd,MACA,QACS;CACT,IAAI,OAAO,SAAS,aAAa,CAAC,iBAAiB,OAAO,IAAI,GAAG,OAAO;CACxE,MAAM,QAAQ,OAAO,KAAK;CAC1B,KAAK,eAAe,MAAM,MAAM,MAAM,MAAoB;CAC1D,OAAO;AACT"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Issues } from "@thomasfosterau/effect-forms";
|
|
2
|
+
//#region src/Tainted.d.ts
|
|
3
|
+
type FormPath = Issues.FormPath;
|
|
4
|
+
/** A nested tree mirroring the form data, `true` where a leaf has changed. */
|
|
5
|
+
interface TaintedFields {
|
|
6
|
+
readonly [key: string]: boolean | TaintedFields | undefined;
|
|
7
|
+
}
|
|
8
|
+
/** Structural deep equality for plain JSON-ish values. */
|
|
9
|
+
declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Compare `current` against `original`, returning the nested tainted tree.
|
|
12
|
+
* A changed scalar leaf is `true`; an unchanged leaf is omitted; nested objects
|
|
13
|
+
* **and arrays** recurse (array elements keyed by index), so a single changed
|
|
14
|
+
* row taints only that index — `taintedAt(["items", 2, "name"])` stays precise.
|
|
15
|
+
* Keys present in either side are considered.
|
|
16
|
+
*/
|
|
17
|
+
declare function computeTainted(original: unknown, current: unknown): TaintedFields;
|
|
18
|
+
/** Whether anything in the tree is tainted. */
|
|
19
|
+
declare function isAnyTainted(tainted: TaintedFields): boolean;
|
|
20
|
+
/** Whether the field at `path` (or anything beneath it) is tainted. */
|
|
21
|
+
declare function isTaintedAt(tainted: TaintedFields, path: FormPath): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Flatten a {@link TaintedFields} tree into the list of leaf {@link FormPath}s
|
|
24
|
+
* that actually changed, in depth-first order — the per-field-changed events
|
|
25
|
+
* a reactive store needs to run per-field validators / clear a superseded
|
|
26
|
+
* server error at exactly the field that moved (as opposed to `isAnyTainted`,
|
|
27
|
+
* which only says *something* changed).
|
|
28
|
+
*/
|
|
29
|
+
declare function taintedLeafPaths(tainted: TaintedFields): FormPath[];
|
|
30
|
+
//#endregion
|
|
31
|
+
export { TaintedFields, computeTainted, deepEqual, isAnyTainted, isTaintedAt, taintedLeafPaths };
|
|
32
|
+
//# sourceMappingURL=Tainted.d.ts.map
|
package/dist/Tainted.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//#region src/Tainted.ts
|
|
2
|
+
const isObject = (value) => typeof value === "object" && value !== null;
|
|
3
|
+
/** Structural deep equality for plain JSON-ish values. */
|
|
4
|
+
function deepEqual(a, b) {
|
|
5
|
+
if (a === b) return true;
|
|
6
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
|
|
7
|
+
if (isObject(a) && isObject(b)) {
|
|
8
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
9
|
+
for (const key of keys) if (!deepEqual(a[key], b[key])) return false;
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
/** Whether two containers are the same kind (both arrays, or both plain objects). */
|
|
15
|
+
const sameKind = (a, b) => Array.isArray(a) === Array.isArray(b);
|
|
16
|
+
/**
|
|
17
|
+
* Compare `current` against `original`, returning the nested tainted tree.
|
|
18
|
+
* A changed scalar leaf is `true`; an unchanged leaf is omitted; nested objects
|
|
19
|
+
* **and arrays** recurse (array elements keyed by index), so a single changed
|
|
20
|
+
* row taints only that index — `taintedAt(["items", 2, "name"])` stays precise.
|
|
21
|
+
* Keys present in either side are considered.
|
|
22
|
+
*/
|
|
23
|
+
function computeTainted(original, current) {
|
|
24
|
+
if (!isObject(original) || !isObject(current) || !sameKind(original, current)) return {};
|
|
25
|
+
const out = {};
|
|
26
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(original), ...Object.keys(current)]);
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
const before = original[key];
|
|
29
|
+
const after = current[key];
|
|
30
|
+
if (isObject(before) && isObject(after) && sameKind(before, after)) {
|
|
31
|
+
const nested = computeTainted(before, after);
|
|
32
|
+
if (Object.keys(nested).length > 0) out[key] = nested;
|
|
33
|
+
} else if (!deepEqual(before, after)) out[key] = true;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/** Whether anything in the tree is tainted. */
|
|
38
|
+
function isAnyTainted(tainted) {
|
|
39
|
+
for (const key of Object.keys(tainted)) {
|
|
40
|
+
const value = tainted[key];
|
|
41
|
+
if (value === true) return true;
|
|
42
|
+
if (isObject(value) && isAnyTainted(value)) return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
/** Whether the field at `path` (or anything beneath it) is tainted. */
|
|
47
|
+
function isTaintedAt(tainted, path) {
|
|
48
|
+
let node = tainted;
|
|
49
|
+
for (const segment of path) {
|
|
50
|
+
if (node === void 0 || typeof node === "boolean") return node === true;
|
|
51
|
+
node = node[String(segment)];
|
|
52
|
+
}
|
|
53
|
+
if (node === void 0) return false;
|
|
54
|
+
if (typeof node === "boolean") return node;
|
|
55
|
+
return isAnyTainted(node);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Flatten a {@link TaintedFields} tree into the list of leaf {@link FormPath}s
|
|
59
|
+
* that actually changed, in depth-first order — the per-field-changed events
|
|
60
|
+
* a reactive store needs to run per-field validators / clear a superseded
|
|
61
|
+
* server error at exactly the field that moved (as opposed to `isAnyTainted`,
|
|
62
|
+
* which only says *something* changed).
|
|
63
|
+
*/
|
|
64
|
+
function taintedLeafPaths(tainted) {
|
|
65
|
+
const out = [];
|
|
66
|
+
const walk = (node, path) => {
|
|
67
|
+
for (const key of Object.keys(node)) {
|
|
68
|
+
const value = node[key];
|
|
69
|
+
if (value === void 0) continue;
|
|
70
|
+
const segment = /^\d+$/.test(key) ? Number(key) : key;
|
|
71
|
+
if (value === true) out.push([...path, segment]);
|
|
72
|
+
else if (typeof value === "object") walk(value, [...path, segment]);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
walk(tainted, []);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { computeTainted, deepEqual, isAnyTainted, isTaintedAt, taintedLeafPaths };
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=Tainted.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Tainted.js","names":[],"sources":["../src/Tainted.ts"],"sourcesContent":["/**\n * @copyright Thomas Foster\n *\n * Tainted-field tracking — which fields the user has changed since the form\n * was seeded.\n *\n * A `tainted` store lets a UI show \"unsaved changes\",\n * guard navigation, or submit only what changed. This is the pure half: a deep\n * diff of the current data against the original snapshot producing a nested\n * boolean tree mirroring the data's shape. The reactive store\n * (`superForm.svelte.ts`) re-derives it as the data changes.\n *\n * @module effect-forms-svelte/Tainted\n */\n\nimport type { Issues } from \"@thomasfosterau/effect-forms\";\n\ntype FormPath = Issues.FormPath;\n\n/** A nested tree mirroring the form data, `true` where a leaf has changed. */\nexport interface TaintedFields {\n readonly [key: string]: boolean | TaintedFields | undefined;\n}\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\n\n/** Structural deep equality for plain JSON-ish values. */\nexport function deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));\n }\n if (isObject(a) && isObject(b)) {\n const keys = new Set([...Object.keys(a), ...Object.keys(b)]);\n for (const key of keys) {\n if (!deepEqual(a[key], b[key])) return false;\n }\n return true;\n }\n return false;\n}\n\n/** Whether two containers are the same kind (both arrays, or both plain objects). */\nconst sameKind = (a: unknown, b: unknown): boolean => Array.isArray(a) === Array.isArray(b);\n\n/**\n * Compare `current` against `original`, returning the nested tainted tree.\n * A changed scalar leaf is `true`; an unchanged leaf is omitted; nested objects\n * **and arrays** recurse (array elements keyed by index), so a single changed\n * row taints only that index — `taintedAt([\"items\", 2, \"name\"])` stays precise.\n * Keys present in either side are considered.\n */\nexport function computeTainted(original: unknown, current: unknown): TaintedFields {\n if (!isObject(original) || !isObject(current) || !sameKind(original, current)) return {};\n\n const out: Record<string, boolean | TaintedFields> = {};\n const keys = new Set([...Object.keys(original), ...Object.keys(current)]);\n for (const key of keys) {\n const before = original[key];\n const after = current[key];\n if (isObject(before) && isObject(after) && sameKind(before, after)) {\n const nested = computeTainted(before, after);\n if (Object.keys(nested).length > 0) out[key] = nested;\n } else if (!deepEqual(before, after)) {\n out[key] = true;\n }\n }\n return out;\n}\n\n/** Whether anything in the tree is tainted. */\nexport function isAnyTainted(tainted: TaintedFields): boolean {\n for (const key of Object.keys(tainted)) {\n const value = tainted[key];\n if (value === true) return true;\n if (isObject(value) && isAnyTainted(value)) return true;\n }\n return false;\n}\n\n/** Whether the field at `path` (or anything beneath it) is tainted. */\nexport function isTaintedAt(tainted: TaintedFields, path: FormPath): boolean {\n let node: boolean | TaintedFields | undefined = tainted;\n for (const segment of path) {\n if (node === undefined || typeof node === \"boolean\") return node === true;\n node = node[String(segment)];\n }\n if (node === undefined) return false;\n if (typeof node === \"boolean\") return node;\n return isAnyTainted(node);\n}\n\n/**\n * Flatten a {@link TaintedFields} tree into the list of leaf {@link FormPath}s\n * that actually changed, in depth-first order — the per-field-changed events\n * a reactive store needs to run per-field validators / clear a superseded\n * server error at exactly the field that moved (as opposed to `isAnyTainted`,\n * which only says *something* changed).\n */\nexport function taintedLeafPaths(tainted: TaintedFields): FormPath[] {\n const out: FormPath[] = [];\n\n const walk = (node: TaintedFields, path: Array<string | number>): void => {\n for (const key of Object.keys(node)) {\n const value = node[key];\n if (value === undefined) continue;\n const segment: string | number = /^\\d+$/.test(key) ? Number(key) : key;\n if (value === true) out.push([...path, segment]);\n else if (typeof value === \"object\") walk(value, [...path, segment]);\n }\n };\n\n walk(tainted, []);\n return out;\n}\n"],"mappings":";AAwBA,MAAM,YAAY,UAChB,OAAO,UAAU,YAAY,UAAU;;AAGzC,SAAgB,UAAU,GAAY,GAAqB;CACzD,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GACrC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,MAAM,MAAM,UAAU,MAAM,EAAE,EAAE,CAAC;CAE5E,IAAI,SAAS,CAAC,KAAK,SAAS,CAAC,GAAG;EAC9B,MAAM,uBAAO,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC;EAC3D,KAAK,MAAM,OAAO,MAChB,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAG,OAAO;EAEzC,OAAO;CACT;CACA,OAAO;AACT;;AAGA,MAAM,YAAY,GAAY,MAAwB,MAAM,QAAQ,CAAC,MAAM,MAAM,QAAQ,CAAC;;;;;;;;AAS1F,SAAgB,eAAe,UAAmB,SAAiC;CACjF,IAAI,CAAC,SAAS,QAAQ,KAAK,CAAC,SAAS,OAAO,KAAK,CAAC,SAAS,UAAU,OAAO,GAAG,OAAO,CAAC;CAEvF,MAAM,MAA+C,CAAC;CACtD,MAAM,uBAAO,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,QAAQ,GAAG,GAAG,OAAO,KAAK,OAAO,CAAC,CAAC;CACxE,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,SAAS,SAAS;EACxB,MAAM,QAAQ,QAAQ;EACtB,IAAI,SAAS,MAAM,KAAK,SAAS,KAAK,KAAK,SAAS,QAAQ,KAAK,GAAG;GAClE,MAAM,SAAS,eAAe,QAAQ,KAAK;GAC3C,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAAG,IAAI,OAAO;EACjD,OAAO,IAAI,CAAC,UAAU,QAAQ,KAAK,GACjC,IAAI,OAAO;CAEf;CACA,OAAO;AACT;;AAGA,SAAgB,aAAa,SAAiC;CAC5D,KAAK,MAAM,OAAO,OAAO,KAAK,OAAO,GAAG;EACtC,MAAM,QAAQ,QAAQ;EACtB,IAAI,UAAU,MAAM,OAAO;EAC3B,IAAI,SAAS,KAAK,KAAK,aAAa,KAAK,GAAG,OAAO;CACrD;CACA,OAAO;AACT;;AAGA,SAAgB,YAAY,SAAwB,MAAyB;CAC3E,IAAI,OAA4C;CAChD,KAAK,MAAM,WAAW,MAAM;EAC1B,IAAI,SAAS,KAAA,KAAa,OAAO,SAAS,WAAW,OAAO,SAAS;EACrE,OAAO,KAAK,OAAO,OAAO;CAC5B;CACA,IAAI,SAAS,KAAA,GAAW,OAAO;CAC/B,IAAI,OAAO,SAAS,WAAW,OAAO;CACtC,OAAO,aAAa,IAAI;AAC1B;;;;;;;;AASA,SAAgB,iBAAiB,SAAoC;CACnE,MAAM,MAAkB,CAAC;CAEzB,MAAM,QAAQ,MAAqB,SAAuC;EACxE,KAAK,MAAM,OAAO,OAAO,KAAK,IAAI,GAAG;GACnC,MAAM,QAAQ,KAAK;GACnB,IAAI,UAAU,KAAA,GAAW;GACzB,MAAM,UAA2B,QAAQ,KAAK,GAAG,IAAI,OAAO,GAAG,IAAI;GACnE,IAAI,UAAU,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC;QAC1C,IAAI,OAAO,UAAU,UAAU,KAAK,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC;EACpE;CACF;CAEA,KAAK,SAAS,CAAC,CAAC;CAChB,OAAO;AACT"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { validateData, validateField } from "./ClientValidate.js";
|
|
2
|
+
import { DataApplication, FormUpdate, computeDataApplication, objectToEntries, objectToFormData, parseActionResult, resultToUpdate } from "./EnhanceCore.js";
|
|
3
|
+
import { CommandKeys, CommandStructLike, DefineEntityFormOptions, EntityFormDefinition, EntityFormEntry, EntryBuilders, MessageThunk, ScalarEntry, SelectEntry, SelectEntryOptions, SubmitMessages, TextEntryOptions, WidgetEntry, WidgetEntryOptions, defineEntityForm } from "./EntityForm.js";
|
|
4
|
+
import { FieldListener, FormListeners, ListenerEvent, changeEvents, dispatchChange } from "./Listeners.js";
|
|
5
|
+
import { TaintedFields, computeTainted, deepEqual, isAnyTainted, isTaintedAt, taintedLeafPaths } from "./Tainted.js";
|
|
6
|
+
import { FieldHandle, FieldMeta, FieldValidators, FormBuilderOptions, FormMode, FormOptions, FormSeed, FormStore, Submission, SubmitFn, createForm } from "./FormStore.svelte.js";
|
|
7
|
+
import { ServerActionResult, applyActionResult } from "./SvelteKit.js";
|
|
8
|
+
export { CommandKeys, CommandStructLike, DataApplication, DefineEntityFormOptions, EntityFormDefinition, EntityFormEntry, EntryBuilders, FieldHandle, FieldListener, FieldMeta, FieldValidators, FormBuilderOptions, FormListeners, FormMode, FormOptions, FormSeed, FormStore, FormUpdate, ListenerEvent, MessageThunk, ScalarEntry, SelectEntry, SelectEntryOptions, ServerActionResult, Submission, SubmitFn, SubmitMessages, TaintedFields, TextEntryOptions, WidgetEntry, WidgetEntryOptions, applyActionResult, changeEvents, computeDataApplication, computeTainted, createForm, deepEqual, defineEntityForm, dispatchChange, isAnyTainted, isTaintedAt, objectToEntries, objectToFormData, parseActionResult, resultToUpdate, taintedLeafPaths, validateData, validateField };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { validateData, validateField } from "./ClientValidate.js";
|
|
2
|
+
import { computeDataApplication, objectToEntries, objectToFormData, parseActionResult, resultToUpdate } from "./EnhanceCore.js";
|
|
3
|
+
import { defineEntityForm } from "./EntityForm.js";
|
|
4
|
+
import { computeTainted, deepEqual, isAnyTainted, isTaintedAt, taintedLeafPaths } from "./Tainted.js";
|
|
5
|
+
import { changeEvents, dispatchChange } from "./Listeners.js";
|
|
6
|
+
import { FieldHandle, FormStore, createForm } from "./FormStore.svelte.js";
|
|
7
|
+
import { applyActionResult } from "./SvelteKit.js";
|
|
8
|
+
export { FieldHandle, FormStore, applyActionResult, changeEvents, computeDataApplication, computeTainted, createForm, deepEqual, defineEntityForm, dispatchChange, isAnyTainted, isTaintedAt, objectToEntries, objectToFormData, parseActionResult, resultToUpdate, taintedLeafPaths, validateData, validateField };
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thomasfosterau/effect-forms-svelte",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Svelte 5 (runes) bindings for @thomasfosterau/effect-forms — a reactive form store, an enhance action, tainted tracking, and client-side validation.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"effect",
|
|
7
|
+
"form-validation",
|
|
8
|
+
"forms",
|
|
9
|
+
"functional-programming",
|
|
10
|
+
"runes",
|
|
11
|
+
"svelte",
|
|
12
|
+
"sveltekit",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/thomasfosterau/effect-forms/tree/main/packages/svelte#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/thomasfosterau/effect-forms/issues"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Thomas Foster <me@thomasfoster.au>",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/thomasfosterau/effect-forms.git",
|
|
24
|
+
"directory": "packages/svelte"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"module": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"svelte": "./dist/index.js",
|
|
40
|
+
"import": "./dist/index.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"clean": "rm -rf dist"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@thomasfosterau/effect-forms": "workspace:^"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"effect": "^4.0.0-beta.102",
|
|
54
|
+
"svelte": "^5.40.0"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
58
|
+
}
|
|
59
|
+
}
|