@thomasfosterau/effect-forms-svelte 0.2.0 → 0.3.2

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.
Files changed (39) hide show
  1. package/README.md +19 -19
  2. package/dist/ClientValidate.d.ts +1 -2
  3. package/dist/ClientValidate.js +1 -2
  4. package/dist/EnhanceCore.d.ts +34 -4
  5. package/dist/EnhanceCore.js +106 -39
  6. package/dist/EnhanceCore.js.map +1 -1
  7. package/dist/EntityForm.d.ts +0 -2
  8. package/dist/EntityForm.js +29 -30
  9. package/dist/EntityForm.js.map +1 -1
  10. package/dist/FormStore.svelte.d.ts +11 -5
  11. package/dist/FormStore.svelte.js +94 -61
  12. package/dist/FormStore.svelte.js.map +1 -1
  13. package/dist/Listeners.d.ts +1 -2
  14. package/dist/Listeners.js +4 -4
  15. package/dist/Listeners.js.map +1 -1
  16. package/dist/SvelteKit.d.ts +1 -2
  17. package/dist/SvelteKit.js +3 -3
  18. package/dist/SvelteKit.js.map +1 -1
  19. package/dist/Tainted.d.ts +1 -2
  20. package/dist/Tainted.js +28 -41
  21. package/dist/Tainted.js.map +1 -1
  22. package/dist/_virtual/_rolldown/runtime.js +1 -2
  23. package/dist/kit/ActionRedirect.d.ts +0 -2
  24. package/dist/kit/ActionRedirect.js +12 -15
  25. package/dist/kit/ActionRedirect.js.map +1 -1
  26. package/dist/kit/CommandFailure.d.ts +1 -2
  27. package/dist/kit/CommandFailure.js +18 -20
  28. package/dist/kit/CommandFailure.js.map +1 -1
  29. package/dist/kit/Enhance.d.ts +0 -2
  30. package/dist/kit/Enhance.js +1 -2
  31. package/dist/kit/FormAction.d.ts +0 -2
  32. package/dist/kit/FormAction.js +39 -33
  33. package/dist/kit/FormAction.js.map +1 -1
  34. package/dist/kit/FormStateReaders.d.ts +0 -2
  35. package/dist/kit/FormStateReaders.js +4 -4
  36. package/dist/kit/FormStateReaders.js.map +1 -1
  37. package/dist/kit/Pointer.d.ts +0 -2
  38. package/dist/kit/Pointer.js +1 -2
  39. package/package.json +6 -6
@@ -1 +1 @@
1
- {"version":3,"file":"EntityForm.js","names":[],"sources":["../src/EntityForm.ts"],"sourcesContent":["/**\n * The typed entity-form definition builder.\n * `defineEntityForm(commands, (f) => [...])` pairs a `Command.plan`\n * derivation (form-input schemas, blank defaults, tri-state adapters — see\n * `effect-forms/Command`) with an ordered list of typed field\n * entries: shared scalar primitives by kind, and arbitrary widget components\n * for the bespoke fields (pickers, contributor lists). One generic renderer\n * component walks `definition.entries`; per-entity code shrinks to one\n * definition module.\n *\n * Field keys are type-constrained to the command structs' keys, and the\n * builder enforces exhaustiveness at definition time (a runtime check, kept\n * deliberately simple over deep conditional types): every derivable command\n * field must be declared exactly once, so adding a field to a command struct\n * breaks the entity's definition module until the form declares it.\n *\n * Labels and other message-bearing props are **thunks** (`() => string`,\n * e.g. an i18n message function) resolved at render time, never at\n * module scope — module-scope resolution would freeze the import-time locale.\n *\n * @copyright Thomas Foster\n * @module effect-forms-svelte/EntityForm\n */\n\nimport type { Component } from \"svelte\";\nimport { Command } from \"@thomasfosterau/effect-forms\";\nimport * as Schema from \"effect/Schema\";\n\n/** A render-time message thunk — pass the message function itself, uncalled. */\nexport type MessageThunk = () => string;\n\n/** Presentation options for a scalar text entry. */\nexport interface TextEntryOptions {\n readonly label: MessageThunk;\n /** Presentation `type` attribute (`url`, `date`). Omitted → text input. */\n readonly type?: \"text\" | \"url\" | \"date\";\n /** Keyboard hint for numeric text fields; defaults from the field's derived kind. */\n readonly inputmode?: \"numeric\";\n /** Force required-ness (defaults to the schema-derived constraint). */\n readonly required?: boolean;\n readonly placeholder?: MessageThunk;\n}\n\n/** A scalar field rendered by the shared text/textarea primitives. */\nexport interface ScalarEntry {\n readonly _tag: \"scalar\";\n readonly key: string;\n readonly multiline: boolean;\n readonly options: TextEntryOptions;\n}\n\n/** Presentation options for a `<select>` entry over a literal-union (`enum` kind) field. */\nexport interface SelectEntryOptions {\n readonly label: MessageThunk;\n readonly required?: boolean;\n /**\n * Per-option render-time label thunks, keyed by the field's derived option\n * *value* (`Command.FieldPlan.options`, read via `fieldFor` — not declared\n * here). `defineEntityForm` throws at definition time if a derived value has\n * no matching thunk.\n */\n readonly optionLabels: Readonly<Record<string, MessageThunk>>;\n}\n\n/**\n * A `<select>` field over a literal-union (`enum` kind) command field. Option\n * *values* derive from the field's plan; this entry supplies only the\n * per-option labels — `defineEntityForm` validates the field is `enum`-kind\n * and that every derived value has a label.\n */\nexport interface SelectEntry {\n readonly _tag: \"select\";\n readonly key: string;\n readonly options: SelectEntryOptions;\n}\n\n/** Options for a widget entry (picker / rows list). */\nexport interface WidgetEntryOptions {\n /** Gate the submit button while this field's value is empty (required pickers). */\n readonly gate?: boolean;\n}\n\n/**\n * A bespoke widget field. The renderer bridges the store to the widget through\n * the widget contract — `name`, `value`, `onChange`, `errors` — which\n * are **reserved keys**: the bridge is spread last, so a `props()` thunk or a\n * load-supplied field prop can never silently sever the store binding.\n */\nexport interface WidgetEntry {\n readonly _tag: \"widget\";\n readonly key: string;\n /** `\"value\"` bridges a string FK; `\"rows\"` bridges a controlled row list. */\n readonly bridge: \"value\" | \"rows\";\n // reason: the renderer composes arbitrary widgets; per-widget prop checking\n // happens in the definition module via the builder's generic signature.\n readonly component: Component<any>;\n readonly props: () => Record<string, unknown>;\n readonly options: WidgetEntryOptions;\n}\n\n/** One ordered entry in an entity form definition. */\nexport type EntityFormEntry = ScalarEntry | WidgetEntry | SelectEntry;\n\n/** The per-mode submit labels. */\nexport interface SubmitMessages {\n readonly create: MessageThunk;\n readonly update: MessageThunk;\n}\n\n/** The runtime shape `defineEntityForm` accepts for a command struct. */\nexport interface CommandStructLike {\n readonly fields: Record<string, Schema.Top>;\n}\n\n/** The value `defineEntityForm` returns — everything one renderer needs. */\nexport interface EntityFormDefinition {\n readonly plan: Command.CommandPlan;\n readonly entries: readonly EntityFormEntry[];\n readonly createInput: Schema.Top;\n readonly updateInput: Schema.Top;\n readonly createDefaults: Readonly<Record<string, string>>;\n readonly updateDefaults: Readonly<Record<string, string>>;\n readonly adaptCreate: (input: Command.FormInputRecord) => Record<string, unknown>;\n readonly adaptUpdate: (input: Command.FormInputRecord) => Record<string, unknown>;\n readonly submit: SubmitMessages;\n /** The create struct carries a client-generated id (idempotent retries). */\n readonly clientId: boolean;\n /** Entries visible in one mode, in declaration order. */\n readonly entriesFor: (mode: \"create\" | \"update\") => readonly EntityFormEntry[];\n /** The plan field behind an entry key. */\n readonly fieldFor: (key: string) => Command.FieldPlan;\n}\n\n/** The writable command keys a definition may declare (the `id` key is renderer-owned). */\nexport type CommandKeys<C extends CommandStructLike, U extends CommandStructLike> = Exclude<\n Extract<keyof C[\"fields\"] | keyof U[\"fields\"], string>,\n \"id\"\n>;\n\n/** The entry builders handed to the definition callback, key-typed to the command pair. */\nexport interface EntryBuilders<K extends string> {\n /** A scalar field rendered by the shared `TextInputField`. */\n text(key: K, options: TextEntryOptions): EntityFormEntry;\n /** A scalar field rendered by the shared `TextAreaInputField`. */\n textarea(key: K, options: TextEntryOptions): EntityFormEntry;\n /**\n * A `<select>` field over a literal-union (`enum` kind) command field.\n * Option values are derived from the field's plan; `options.optionLabels`\n * supplies each derived value's render-time label thunk.\n */\n select(key: K, options: SelectEntryOptions): EntityFormEntry;\n /**\n * A bespoke string-valued widget (an FK picker). The renderer supplies\n * `name` / `value` / `onChange` / `errors`; `props()` supplies the rest at\n * render time (labels stay thunk-resolved).\n */\n custom<P extends Record<string, unknown>>(\n key: K,\n component: Component<never>,\n props: () => P,\n options?: WidgetEntryOptions,\n ): EntityFormEntry;\n /**\n * A bespoke row-list widget (a contributor list). The renderer owns the row\n * state and supplies `name` / `value` / `onChange` / `errors`; the widget\n * owns the wire serialisation (its hidden input).\n */\n rows<P extends Record<string, unknown>>(\n key: K,\n component: Component<never>,\n props: () => P,\n options?: WidgetEntryOptions,\n ): EntityFormEntry;\n}\n\n/** Everything `defineEntityForm` needs beyond the entries. */\nexport interface DefineEntityFormOptions<C extends CommandStructLike, U extends CommandStructLike> {\n readonly create: C;\n readonly update: U;\n readonly submit: SubmitMessages;\n /** Per-field adapter overrides, per mode — the irregular-coercion escape hatch. */\n readonly adaptOverrides?: {\n readonly create?: Command.AdaptOverrides;\n readonly update?: Command.AdaptOverrides;\n };\n /** Per-field Form-Input schema overrides, per mode (e.g. an optional create title with a server default). */\n readonly inputOverrides?: {\n readonly create?: Readonly<Record<string, Schema.Top>>;\n readonly update?: Readonly<Record<string, Schema.Top>>;\n };\n /**\n * Command fields deliberately absent from the standalone form (a carve-out,\n * e.g. BookSeries' `editors` — managed through the Book form's inline-create\n * path instead). Omitted fields are stripped from the create/update field\n * records before `Command.plan` runs, so they need no entry, are never\n * classified (a kind-less omitted field can't throw at definition time),\n * and never appear in the derived input schema, defaults, or adapters.\n */\n readonly omit?: readonly CommandKeys<C, U>[];\n}\n\nconst builders: EntryBuilders<string> = {\n text: (key, options) => ({ _tag: \"scalar\", key, multiline: false, options }),\n textarea: (key, options) => ({ _tag: \"scalar\", key, multiline: true, options }),\n select: (key, options) => ({ _tag: \"select\", key, options }),\n custom: (key, component, props, options = {}) => ({\n _tag: \"widget\",\n key,\n bridge: \"value\",\n component: component as Component<any>,\n props,\n options,\n }),\n rows: (key, component, props, options = {}) => ({\n _tag: \"widget\",\n key,\n bridge: \"rows\",\n component: component as Component<any>,\n props,\n options,\n }),\n};\n\n/**\n * Validate an `f.select` entry against its plan field at definition time: the\n * field must be `enum`-kind (the only kind `Command.plan` derives option\n * values for), and every derived value must have a matching `optionLabels`\n * thunk.\n */\nconst validateSelectEntry = (entry: SelectEntry, field: Command.FieldPlan): void => {\n if (field.kind !== \"enum\") {\n throw new Error(\n `defineEntityForm: entry \"${entry.key}\" is f.select but its command field is \"${field.kind}\"-kind, not enum`,\n );\n }\n const missingLabels = (field.options ?? []).filter(\n (value) => !Object.prototype.hasOwnProperty.call(entry.options.optionLabels, value),\n );\n if (missingLabels.length > 0) {\n throw new Error(\n `defineEntityForm: entry \"${entry.key}\" is missing optionLabels for: ${missingLabels.join(\", \")}`,\n );\n }\n};\n\n/**\n * Build an entity-form definition from a command-struct pair and an ordered\n * entry list. Runs once at module load. Throws when the entries don't cover\n * the derivable command fields exactly (exhaustiveness) or name an unknown\n * key — a command-struct change must surface here, at definition time.\n */\nexport const defineEntityForm = <C extends CommandStructLike, U extends CommandStructLike>(\n options: DefineEntityFormOptions<C, U>,\n build: (f: EntryBuilders<CommandKeys<C, U>>) => readonly EntityFormEntry[],\n): EntityFormDefinition => {\n const omitted = new Set<string>(options.omit ?? []);\n const withoutOmitted = (struct: CommandStructLike): CommandStructLike => {\n if (omitted.size === 0) return struct;\n const fields: Record<string, Schema.Top> = {};\n for (const key in struct.fields) if (!omitted.has(key)) fields[key] = struct.fields[key];\n return { fields };\n };\n const plan = Command.plan(withoutOmitted(options.create), withoutOmitted(options.update));\n const entries = build(builders as EntryBuilders<CommandKeys<C, U>>);\n\n const byKey = new Map<string, Command.FieldPlan>(plan.fields.map((field) => [field.key, field]));\n const declared = new Set<string>();\n for (const entry of entries) {\n const field = byKey.get(entry.key);\n if (field === undefined) {\n throw new Error(\n `defineEntityForm: entry \"${entry.key}\" is not a field of either command struct`,\n );\n }\n if (declared.has(entry.key)) {\n throw new Error(`defineEntityForm: entry \"${entry.key}\" is declared twice`);\n }\n declared.add(entry.key);\n if (entry._tag === \"select\") validateSelectEntry(entry, field);\n }\n const missing = plan.fields.filter((field) => !declared.has(field.key)).map((field) => field.key);\n if (missing.length > 0) {\n throw new Error(\n `defineEntityForm: command fields not declared by any entry: ${missing.join(\", \")} — declare or explicitly widget them`,\n );\n }\n\n const entriesFor = (mode: \"create\" | \"update\") =>\n entries.filter((entry) => {\n const field = byKey.get(entry.key);\n return field !== undefined && (mode === \"create\" ? field.inCreate : field.inUpdate);\n });\n\n return {\n plan,\n entries,\n createInput: Schema.Struct(\n Command.formInputFields(plan, \"create\", options.inputOverrides?.create) as Record<\n string,\n never\n >,\n ),\n updateInput: Schema.Struct(\n Command.formInputFields(plan, \"update\", options.inputOverrides?.update) as Record<\n string,\n never\n >,\n ),\n createDefaults: Command.blankDefaults(plan, \"create\"),\n updateDefaults: Command.blankDefaults(plan, \"update\"),\n adaptCreate: Command.createAdapter(plan, options.adaptOverrides?.create),\n adaptUpdate: Command.updateAdapter(plan, options.adaptOverrides?.update),\n submit: options.submit,\n clientId: plan.clientId,\n entriesFor,\n fieldFor: (key: string) => {\n const field = byKey.get(key);\n if (field === undefined) throw new Error(`EntityFormDefinition: unknown field \"${key}\"`);\n return field;\n },\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"EntityForm.js","names":[],"sources":["../src/EntityForm.ts"],"sourcesContent":["/**\n * The typed entity-form definition builder.\n * `defineEntityForm(commands, (f) => [...])` pairs a `Command.plan`\n * derivation (form-input schemas, blank defaults, tri-state adapters — see\n * `effect-forms/Command`) with an ordered list of typed field\n * entries: shared scalar primitives by kind, and arbitrary widget components\n * for the bespoke fields (pickers, contributor lists). One generic renderer\n * component walks `definition.entries`; per-entity code shrinks to one\n * definition module.\n *\n * Field keys are type-constrained to the command structs' keys, and the\n * builder enforces exhaustiveness at definition time (a runtime check, kept\n * deliberately simple over deep conditional types): every derivable command\n * field must be declared exactly once, so adding a field to a command struct\n * breaks the entity's definition module until the form declares it.\n *\n * Labels and other message-bearing props are **thunks** (`() => string`,\n * e.g. an i18n message function) resolved at render time, never at\n * module scope — module-scope resolution would freeze the import-time locale.\n *\n * @copyright Thomas Foster\n * @module effect-forms-svelte/EntityForm\n */\n\nimport type { Component } from \"svelte\";\nimport { Command } from \"@thomasfosterau/effect-forms\";\nimport * as Arr from \"effect/Array\";\nimport * as HashMap from \"effect/HashMap\";\nimport * as HashSet from \"effect/HashSet\";\nimport * as Option from \"effect/Option\";\nimport * as Predicate from \"effect/Predicate\";\nimport * as Record from \"effect/Record\";\nimport * as Schema from \"effect/Schema\";\n\n/** A render-time message thunk — pass the message function itself, uncalled. */\nexport type MessageThunk = () => string;\n\n/** Presentation options for a scalar text entry. */\nexport interface TextEntryOptions {\n readonly label: MessageThunk;\n /** Presentation `type` attribute (`url`, `date`). Omitted → text input. */\n readonly type?: \"text\" | \"url\" | \"date\";\n /** Keyboard hint for numeric text fields; defaults from the field's derived kind. */\n readonly inputmode?: \"numeric\";\n /** Force required-ness (defaults to the schema-derived constraint). */\n readonly required?: boolean;\n readonly placeholder?: MessageThunk;\n}\n\n/** A scalar field rendered by the shared text/textarea primitives. */\nexport interface ScalarEntry {\n readonly _tag: \"scalar\";\n readonly key: string;\n readonly multiline: boolean;\n readonly options: TextEntryOptions;\n}\n\n/** Presentation options for a `<select>` entry over a literal-union (`enum` kind) field. */\nexport interface SelectEntryOptions {\n readonly label: MessageThunk;\n readonly required?: boolean;\n /**\n * Per-option render-time label thunks, keyed by the field's derived option\n * *value* (`Command.FieldPlan.options`, read via `fieldFor` — not declared\n * here). `defineEntityForm` throws at definition time if a derived value has\n * no matching thunk.\n */\n readonly optionLabels: Readonly<Record<string, MessageThunk>>;\n}\n\n/**\n * A `<select>` field over a literal-union (`enum` kind) command field. Option\n * *values* derive from the field's plan; this entry supplies only the\n * per-option labels — `defineEntityForm` validates the field is `enum`-kind\n * and that every derived value has a label.\n */\nexport interface SelectEntry {\n readonly _tag: \"select\";\n readonly key: string;\n readonly options: SelectEntryOptions;\n}\n\n/** Options for a widget entry (picker / rows list). */\nexport interface WidgetEntryOptions {\n /** Gate the submit button while this field's value is empty (required pickers). */\n readonly gate?: boolean;\n}\n\n/**\n * A bespoke widget field. The renderer bridges the store to the widget through\n * the widget contract — `name`, `value`, `onChange`, `errors` — which\n * are **reserved keys**: the bridge is spread last, so a `props()` thunk or a\n * load-supplied field prop can never silently sever the store binding.\n */\nexport interface WidgetEntry {\n readonly _tag: \"widget\";\n readonly key: string;\n /** `\"value\"` bridges a string FK; `\"rows\"` bridges a controlled row list. */\n readonly bridge: \"value\" | \"rows\";\n // reason: the renderer composes arbitrary widgets; per-widget prop checking\n // happens in the definition module via the builder's generic signature.\n readonly component: Component<any>;\n readonly props: () => Record<string, unknown>;\n readonly options: WidgetEntryOptions;\n}\n\n/** One ordered entry in an entity form definition. */\nexport type EntityFormEntry = ScalarEntry | WidgetEntry | SelectEntry;\n\n/** The per-mode submit labels. */\nexport interface SubmitMessages {\n readonly create: MessageThunk;\n readonly update: MessageThunk;\n}\n\n/** The runtime shape `defineEntityForm` accepts for a command struct. */\nexport interface CommandStructLike {\n readonly fields: Record<string, Schema.Top>;\n}\n\n/** The value `defineEntityForm` returns — everything one renderer needs. */\nexport interface EntityFormDefinition {\n readonly plan: Command.CommandPlan;\n readonly entries: readonly EntityFormEntry[];\n readonly createInput: Schema.Top;\n readonly updateInput: Schema.Top;\n readonly createDefaults: Readonly<Record<string, string>>;\n readonly updateDefaults: Readonly<Record<string, string>>;\n readonly adaptCreate: (input: Command.FormInputRecord) => Record<string, unknown>;\n readonly adaptUpdate: (input: Command.FormInputRecord) => Record<string, unknown>;\n readonly submit: SubmitMessages;\n /** The create struct carries a client-generated id (idempotent retries). */\n readonly clientId: boolean;\n /** Entries visible in one mode, in declaration order. */\n readonly entriesFor: (mode: \"create\" | \"update\") => readonly EntityFormEntry[];\n /** The plan field behind an entry key. */\n readonly fieldFor: (key: string) => Command.FieldPlan;\n}\n\n/** The writable command keys a definition may declare (the `id` key is renderer-owned). */\nexport type CommandKeys<C extends CommandStructLike, U extends CommandStructLike> = Exclude<\n Extract<keyof C[\"fields\"] | keyof U[\"fields\"], string>,\n \"id\"\n>;\n\n/** The entry builders handed to the definition callback, key-typed to the command pair. */\nexport interface EntryBuilders<K extends string> {\n /** A scalar field rendered by the shared `TextInputField`. */\n text(key: K, options: TextEntryOptions): EntityFormEntry;\n /** A scalar field rendered by the shared `TextAreaInputField`. */\n textarea(key: K, options: TextEntryOptions): EntityFormEntry;\n /**\n * A `<select>` field over a literal-union (`enum` kind) command field.\n * Option values are derived from the field's plan; `options.optionLabels`\n * supplies each derived value's render-time label thunk.\n */\n select(key: K, options: SelectEntryOptions): EntityFormEntry;\n /**\n * A bespoke string-valued widget (an FK picker). The renderer supplies\n * `name` / `value` / `onChange` / `errors`; `props()` supplies the rest at\n * render time (labels stay thunk-resolved).\n */\n custom<P extends Record<string, unknown>>(\n key: K,\n component: Component<never>,\n props: () => P,\n options?: WidgetEntryOptions,\n ): EntityFormEntry;\n /**\n * A bespoke row-list widget (a contributor list). The renderer owns the row\n * state and supplies `name` / `value` / `onChange` / `errors`; the widget\n * owns the wire serialisation (its hidden input).\n */\n rows<P extends Record<string, unknown>>(\n key: K,\n component: Component<never>,\n props: () => P,\n options?: WidgetEntryOptions,\n ): EntityFormEntry;\n}\n\n/** Everything `defineEntityForm` needs beyond the entries. */\nexport interface DefineEntityFormOptions<C extends CommandStructLike, U extends CommandStructLike> {\n readonly create: C;\n readonly update: U;\n readonly submit: SubmitMessages;\n /** Per-field adapter overrides, per mode — the irregular-coercion escape hatch. */\n readonly adaptOverrides?: {\n readonly create?: Command.AdaptOverrides;\n readonly update?: Command.AdaptOverrides;\n };\n /** Per-field Form-Input schema overrides, per mode (e.g. an optional create title with a server default). */\n readonly inputOverrides?: {\n readonly create?: Readonly<Record<string, Schema.Top>>;\n readonly update?: Readonly<Record<string, Schema.Top>>;\n };\n /**\n * Command fields deliberately absent from the standalone form (a carve-out,\n * e.g. BookSeries' `editors` — managed through the Book form's inline-create\n * path instead). Omitted fields are stripped from the create/update field\n * records before `Command.plan` runs, so they need no entry, are never\n * classified (a kind-less omitted field can't throw at definition time),\n * and never appear in the derived input schema, defaults, or adapters.\n */\n readonly omit?: readonly CommandKeys<C, U>[];\n}\n\nconst builders: EntryBuilders<string> = {\n text: (key, options) => ({ _tag: \"scalar\", key, multiline: false, options }),\n textarea: (key, options) => ({ _tag: \"scalar\", key, multiline: true, options }),\n select: (key, options) => ({ _tag: \"select\", key, options }),\n custom: (key, component, props, options = {}) => ({\n _tag: \"widget\",\n key,\n bridge: \"value\",\n // SAFETY: `Component<never>` is the widest component type (every component\n // is assignable to it); the entry erases its props to `any` because the\n // renderer only ever mounts it with this entry's own `props()` plus the\n // widget-contract bridge props, the props the definition module chose\n // this component for.\n component: component as Component<any>,\n props,\n options,\n }),\n rows: (key, component, props, options = {}) => ({\n _tag: \"widget\",\n key,\n bridge: \"rows\",\n // SAFETY: `Component<never>` is the widest component type (every component\n // is assignable to it); the entry erases its props to `any` because the\n // renderer only ever mounts it with this entry's own `props()` plus the\n // widget-contract bridge props, the props the definition module chose\n // this component for.\n component: component as Component<any>,\n props,\n options,\n }),\n};\n\n/**\n * A definition-time error: the entries don't match the command structs. Thrown\n * synchronously from `defineEntityForm` (and `fieldFor`), never as an Effect\n * failure — a definition module that doesn't match its commands must fail at\n * import.\n */\nclass EntityFormDefinitionError extends Schema.TaggedError<\n EntityFormDefinitionError,\n { readonly brand: unique symbol }\n>()(\"EntityFormDefinitionError\", { message: Schema.String }) {}\n\n/**\n * Validate an `f.select` entry against its plan field at definition time: the\n * field must be `enum`-kind (the only kind `Command.plan` derives option\n * values for), and every derived value must have a matching `optionLabels`\n * thunk.\n */\nconst validateSelectEntry = (entry: SelectEntry, field: Command.FieldPlan): void => {\n if (field.kind !== \"enum\") {\n throw new EntityFormDefinitionError({\n message: `defineEntityForm: entry \"${entry.key}\" is f.select but its command field is \"${field.kind}\"-kind, not enum`,\n });\n }\n const missingLabels = (field.options ?? []).filter(\n (value) => !Object.prototype.hasOwnProperty.call(entry.options.optionLabels, value),\n );\n if (Arr.isReadonlyArrayNonEmpty(missingLabels)) {\n throw new EntityFormDefinitionError({\n message: `defineEntityForm: entry \"${entry.key}\" is missing optionLabels for: ${missingLabels.join(\", \")}`,\n });\n }\n};\n\n/**\n * Build an entity-form definition from a command-struct pair and an ordered\n * entry list. Runs once at module load. Throws when the entries don't cover\n * the derivable command fields exactly (exhaustiveness) or name an unknown\n * key — a command-struct change must surface here, at definition time.\n */\nexport const defineEntityForm = <C extends CommandStructLike, U extends CommandStructLike>(\n options: DefineEntityFormOptions<C, U>,\n build: (f: EntryBuilders<CommandKeys<C, U>>) => readonly EntityFormEntry[],\n): EntityFormDefinition => {\n const omitted = HashSet.fromIterable<string>(options.omit ?? []);\n const withoutOmitted = (struct: CommandStructLike): CommandStructLike =>\n HashSet.size(omitted) === 0\n ? struct\n : { fields: Record.filter(struct.fields, (_, key) => !HashSet.has(omitted, key)) };\n const plan = Command.plan(withoutOmitted(options.create), withoutOmitted(options.update));\n const entries = build(builders);\n\n const byKey = HashMap.fromIterable(Arr.map(plan.fields, (field) => [field.key, field] as const));\n const declared = Arr.reduce(entries, HashSet.empty<string>(), (seen, entry) => {\n const field = HashMap.get(byKey, entry.key);\n if (Option.isNone(field)) {\n throw new EntityFormDefinitionError({\n message: `defineEntityForm: entry \"${entry.key}\" is not a field of either command struct`,\n });\n }\n if (HashSet.has(seen, entry.key)) {\n throw new EntityFormDefinitionError({\n message: `defineEntityForm: entry \"${entry.key}\" is declared twice`,\n });\n }\n if (Predicate.isTagged(entry, \"select\")) validateSelectEntry(entry, field.value);\n return HashSet.add(seen, entry.key);\n });\n const missing = Arr.map(\n Arr.filter(plan.fields, (field) => !HashSet.has(declared, field.key)),\n (field) => field.key,\n );\n if (Arr.isReadonlyArrayNonEmpty(missing)) {\n throw new EntityFormDefinitionError({\n message: `defineEntityForm: command fields not declared by any entry: ${missing.join(\", \")} — declare or explicitly widget them`,\n });\n }\n\n const entriesFor = (mode: \"create\" | \"update\") =>\n Arr.filter(entries, (entry) =>\n Option.exists(HashMap.get(byKey, entry.key), (field) =>\n mode === \"create\" ? field.inCreate : field.inUpdate,\n ),\n );\n\n return {\n plan,\n entries,\n createInput: Schema.Struct(\n Command.formInputFields(plan, \"create\", options.inputOverrides?.create),\n ),\n updateInput: Schema.Struct(\n Command.formInputFields(plan, \"update\", options.inputOverrides?.update),\n ),\n createDefaults: Command.blankDefaults(plan, \"create\"),\n updateDefaults: Command.blankDefaults(plan, \"update\"),\n adaptCreate: Command.createAdapter(plan, options.adaptOverrides?.create),\n adaptUpdate: Command.updateAdapter(plan, options.adaptOverrides?.update),\n submit: options.submit,\n clientId: plan.clientId,\n entriesFor,\n fieldFor: (key: string) =>\n Option.getOrThrowWith(\n HashMap.get(byKey, key),\n () =>\n new EntityFormDefinitionError({\n message: `EntityFormDefinition: unknown field \"${key}\"`,\n }),\n ),\n };\n};\n"],"mappings":"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"}
@@ -4,7 +4,6 @@ import { TaintedFields } from "./Tainted.js";
4
4
  import { Constraints, Errors, Field, FormBuilder, Issues, Mode, Path, Result, State, Validate } from "@thomasfosterau/effect-forms";
5
5
  import * as Schema from "effect/Schema";
6
6
  import * as Layer from "effect/Layer";
7
- //#region src/FormStore.svelte.d.ts
8
7
  type FormActionResult<A, Message = unknown> = Result.FormActionResult<A, Message>;
9
8
  type FormErrors = Errors.FormErrors;
10
9
  type FormPath = Issues.FormPath;
@@ -45,7 +44,7 @@ export declare class FieldHandle<V> {
45
44
  #private;
46
45
  /** The path in flat field-name notation — use as the input's `name` attribute. */
47
46
  readonly name: string;
48
- constructor(store: FormStore<Schema.Top, object, unknown>, path: FormPath);
47
+ constructor(store: FormStore<Schema.Top, object>, path: FormPath);
49
48
  /** The field's current value. Assignable — `bind:value={field.value}` works. */
50
49
  get value(): V;
51
50
  set value(next: V);
@@ -108,7 +107,9 @@ export interface FormOptions<S extends Schema.Top, A, Message, R = never> {
108
107
  /**
109
108
  * The builder itself, set automatically when {@link createForm} is given a
110
109
  * {@link FormBuilder}. Its `refineEffect` refinements run client-side as a
111
- * whole-form async validator whenever {@link FormStore.validate} runs.
110
+ * whole-form async validator whenever {@link FormStore.validate} runs, on the
111
+ * form data decoded through the builder's schema (as on the server); while
112
+ * the data doesn't decode they don't run, and the field errors say why.
112
113
  */
113
114
  readonly builder?: FormBuilder.FormBuilder<Field.FieldsRecord, R>;
114
115
  /** The action URL the form posts to (defaults to the current page's action). */
@@ -346,7 +347,12 @@ export declare class FormStore<S extends Schema.Top, A = S["Type"], Message = un
346
347
  enhance: (node: HTMLFormElement) => {
347
348
  destroy: () => void;
348
349
  };
349
- /** Build a {@link FormData} body from the current data (SPA / manual submit). */
350
+ /**
351
+ * Build a {@link FormData} body from the current data (SPA / manual submit).
352
+ *
353
+ * Each value is encoded as `objectToFormData` encodes it: a `File` or
354
+ * `Blob` is appended as a file and a `Date` as its UTC `YYYY-MM-DD`.
355
+ */
350
356
  toFormData(): FormData;
351
357
  }
352
358
  /** A schema plus its blank-form defaults — what {@link createForm} wraps `Form.make` over. */
@@ -376,5 +382,5 @@ export type FormBuilderOptions<TFields extends Field.FieldsRecord, R, Message> =
376
382
  export declare function createForm<TFields extends Field.FieldsRecord, R, Message = unknown>(builder: FormBuilder.FormBuilder<TFields, R>, options?: FormBuilderOptions<TFields, R, Message>): FormStore<BuiltSchema<TFields, R>, Field.EncodedFromFields<TFields>, Message, R>;
377
383
  export declare function createForm<S extends Schema.Top, Message = unknown, R = never>(seed: FormSeed<S>, options?: FormOptions<S, S["Type"], Message, R>): FormStore<S, S["Type"], Message, R>;
378
384
  export declare function createForm<S extends Schema.Top, A = S["Type"], Message = unknown, R = never>(initial: FormState<A, Message>, options: FormOptions<S, A, Message, R>): FormStore<S, A, Message, R>;
379
- //#endregion
385
+ export {};
380
386
  //# sourceMappingURL=FormStore.svelte.d.ts.map
@@ -3,13 +3,18 @@ import { computeDataApplication, objectToFormData, parseActionResult, resultToUp
3
3
  import { computeTainted, isAnyTainted, isTaintedAt, taintedLeafPaths } from "./Tainted.js";
4
4
  import { dispatchChange } from "./Listeners.js";
5
5
  import { Constraints, Errors, Field, Form, FormBuilder, Mode, Path, Validate } from "@thomasfosterau/effect-forms";
6
+ import * as Arr from "effect/Array";
7
+ import * as Predicate from "effect/Predicate";
8
+ import * as R from "effect/Record";
9
+ import * as Option from "effect/Option";
6
10
  import * as Duration from "effect/Duration";
7
11
  import * as Effect from "effect/Effect";
8
12
  import * as Fiber from "effect/Fiber";
9
13
  import * as Layer from "effect/Layer";
10
14
  import * as ManagedRuntime from "effect/ManagedRuntime";
15
+ import * as SchemaParser from "effect/SchemaParser";
11
16
  import { createAttachmentKey } from "svelte/attachments";
12
- //#region src/FormStore.svelte.ts
17
+ // src/FormStore.svelte.ts
13
18
  /**
14
19
  * The reactive Svelte 5 form store.
15
20
  *
@@ -56,6 +61,14 @@ import { createAttachmentKey } from "svelte/attachments";
56
61
  const { clearSourceAt, emptySourced, hasErrors, mergeSourced, noErrors, replaceAt, setSourceAt } = Errors;
57
62
  const { getAt, setAt } = Path;
58
63
  const makeForm = Form.make;
64
+ /** View a runtime {@link FormErrors} tree through the data-shaped {@link TypedFormErrors} lens. */
65
+ const typedErrors = (errors) => {
66
+ return errors;
67
+ };
68
+ /** Forget the data-shaped typing of a {@link TypedFormErrors} tree, back to the runtime {@link FormErrors}. */
69
+ const untypedErrors = (errors) => {
70
+ return errors;
71
+ };
59
72
  /**
60
73
  * A typed, reactive handle to one field, returned by {@link FormStore.field}.
61
74
  * Bundles everything a single input needs — `bind:value={field.value}`,
@@ -115,17 +128,18 @@ const defaultSubmit = async (body, action) => {
115
128
  body,
116
129
  headers: { accept: "application/json" }
117
130
  });
118
- let parsed;
119
- try {
120
- parsed = await response.json();
121
- } catch {
122
- return {
123
- _tag: "Error",
124
- status: response.status,
125
- message: response.statusText || "Request failed"
126
- };
127
- }
128
- return parseActionResult(parsed, response.status);
131
+ return response.json().then((parsed) => parseActionResult(parsed, response.status), () => ({
132
+ _tag: "Error",
133
+ status: response.status,
134
+ message: response.statusText || "Request failed"
135
+ }));
136
+ };
137
+ /**
138
+ * Erase a store's type parameters for {@link FieldHandle}, which only calls
139
+ * its path-addressed methods.
140
+ */
141
+ const eraseStore = (store) => {
142
+ return store;
129
143
  };
130
144
  /** Whether `index` is a valid position in an array of `length`. */
131
145
  const inRange = (index, length) => index >= 0 && index < length;
@@ -185,23 +199,29 @@ var FormStore = class {
185
199
  #autoSubmitNode;
186
200
  #validateTimer;
187
201
  /** Per-field-listener debounce timers, keyed by `"change:<path>"` / `"blur:<path>"`. */
188
- #listenerTimers = /* @__PURE__ */ new Map();
202
+ #listenerTimers = {};
189
203
  /** Per-field validators, keyed by {@link Path.toString}; seeded from `options.validators`. */
190
204
  #fieldValidators;
191
205
  /** In-flight async-validator fibers, keyed by `"<path>:<trigger>"` (fields) or `""` (whole-form `refineEffect`). */
192
206
  #fibers = {};
193
207
  /** The error paths the last `refineEffect` run set, so the next run can clear exactly those before merging fresh ones. */
194
208
  #lastRefinementPaths = [];
209
+ /**
210
+ * The decoder that turns the form data into the builder's decoded values
211
+ * before its `refineEffect` refinements see them — `None` unless the store
212
+ * was seeded from a {@link FormBuilder}.
213
+ */
214
+ #decodeForRefinements;
195
215
  /** The data snapshot the field-tracking watcher last diffed against. */
196
216
  #previousData;
197
217
  /** The current nested error tree (server errors merged with client errors), typed to the form data's shape. */
198
- errors = $derived.by(() => mergeSourced(this.#sourced));
218
+ errors = $derived.by(() => typedErrors(mergeSourced(this.#sourced)));
199
219
  /** Client-side-sourced errors only (schema, per-field validators, `refineEffect`). */
200
- clientErrors = $derived.by(() => this.#sourced.client);
220
+ clientErrors = $derived.by(() => typedErrors(this.#sourced.client));
201
221
  /** Server-sourced errors only (the last submission's response). */
202
- serverErrors = $derived.by(() => this.#sourced.server);
222
+ serverErrors = $derived.by(() => typedErrors(this.#sourced.server));
203
223
  /** Whether any async field validator or `refineEffect` run is currently in flight. */
204
- isValidating = $derived(Object.values(this.#validating).some(Boolean));
224
+ isValidating = $derived(Arr.some(R.values(this.#validating), Boolean));
205
225
  /** Whether the form has ever been submitted (attempted). */
206
226
  isSubmitted = $derived(this.submissionAttempts > 0);
207
227
  /** Whether the most recently *completed* submission succeeded. `false` before any submit completes. */
@@ -229,6 +249,7 @@ var FormStore = class {
229
249
  this.#mode = Mode.parse(options.mode);
230
250
  this.#runtime = ManagedRuntime.make(options.layer ?? Layer.empty);
231
251
  this.#fieldValidators = { ...options.validators };
252
+ this.#decodeForRefinements = Option.map(Option.fromUndefinedOr(options.builder), (builder) => SchemaParser.decodeUnknownEffect(FormBuilder.buildSchema(builder), { errors: "all" }));
232
253
  this.data = structuredClone(initial.data);
233
254
  this.#sourced = {
234
255
  client: noErrors,
@@ -246,9 +267,13 @@ var FormStore = class {
246
267
  * progressive-enhancement submit handler via a Svelte attachment.
247
268
  */
248
269
  get props() {
249
- return {
270
+ const action = this.#options.action;
271
+ return action === void 0 ? {
272
+ method: "post",
273
+ [this.#attachmentKey]: this.#enhanceAttachment
274
+ } : {
250
275
  method: "post",
251
- ...this.#options.action !== void 0 ? { action: this.#options.action } : {},
276
+ action,
252
277
  [this.#attachmentKey]: this.#enhanceAttachment
253
278
  };
254
279
  }
@@ -258,7 +283,7 @@ var FormStore = class {
258
283
  }
259
284
  /** The messages currently attached to `path` (for dynamic / nested paths). */
260
285
  errorsAt(path) {
261
- return Errors.getError(this.errors, path) ?? [];
286
+ return Errors.getError(untypedErrors(this.errors), path) ?? [];
262
287
  }
263
288
  /** Whether the async validator(s) at `path` are currently running. */
264
289
  isValidatingAt(path) {
@@ -350,7 +375,7 @@ var FormStore = class {
350
375
  * Path.FieldPath}); a typo'd segment or wrong-shaped path is a compile error.
351
376
  */
352
377
  field(path) {
353
- return new FieldHandle(this, Path.fromString(path));
378
+ return new FieldHandle(eraseStore(this), Path.fromString(path));
354
379
  }
355
380
  /**
356
381
  * Handle a field losing focus: mark it touched, run its `onBlur` /
@@ -383,11 +408,11 @@ var FormStore = class {
383
408
  run();
384
409
  return;
385
410
  }
386
- clearTimeout(this.#listenerTimers.get(key));
387
- this.#listenerTimers.set(key, setTimeout(() => {
388
- this.#listenerTimers.delete(key);
411
+ clearTimeout(this.#listenerTimers[key]);
412
+ this.#listenerTimers[key] = setTimeout(() => {
413
+ delete this.#listenerTimers[key];
389
414
  run();
390
- }, Duration.toMillis(debounce)));
415
+ }, Duration.toMillis(debounce));
391
416
  }
392
417
  /**
393
418
  * The listeners table to dispatch change events against, with each field's
@@ -401,11 +426,10 @@ var FormStore = class {
401
426
  const listeners = this.#options.listeners;
402
427
  if (!listeners || listeners.onChange === void 0 && !listeners.fields) return void 0;
403
428
  if (!listeners.fields) return listeners;
404
- const fields = {};
405
- for (const [key, field] of Object.entries(listeners.fields)) fields[key] = field.onChange ? {
429
+ const fields = R.fromEntries(Arr.map(R.toEntries(listeners.fields), ([key, field]) => [key, field.onChange ? {
406
430
  ...field,
407
431
  onChange: (input) => this.#scheduleListener(`change:${key}`, field.debounce, () => field.onChange?.(input))
408
- } : field;
432
+ } : field]));
409
433
  return {
410
434
  ...listeners,
411
435
  fields
@@ -553,7 +577,7 @@ var FormStore = class {
553
577
  #defaultItem(path) {
554
578
  if (this.#options.schema) return Field.defaultItemAt(this.#options.schema, path);
555
579
  const fields = this.#options.fields;
556
- if (fields && path.length === 1 && typeof path[0] === "string") {
580
+ if (fields && path.length === 1 && Predicate.isString(path[0])) {
557
581
  const def = fields[path[0]];
558
582
  if (def && Field.isArrayFieldDef(def)) return Field.defaultValue(def.itemSchema);
559
583
  }
@@ -702,11 +726,15 @@ var FormStore = class {
702
726
  */
703
727
  #runRefineEffect() {
704
728
  const builder = this.#options.builder;
705
- if (!builder || FormBuilder.asyncRefinements(builder).length === 0) return;
706
- this.#runValidatorFiber("", FormBuilder.runAsyncRefinements(builder, this.data).pipe(Effect.tap((issues) => Effect.sync(() => {
707
- let client = this.#sourcedSnapshot().client;
708
- for (const path of this.#lastRefinementPaths) client = replaceAt(client, path, []);
709
- client = Errors.mergeErrors(client, Errors.issuesToErrors(issues));
729
+ if (!builder || Arr.isReadonlyArrayEmpty(FormBuilder.asyncRefinements(builder))) return;
730
+ if (Option.isNone(this.#decodeForRefinements)) return;
731
+ const decoded = this.#decodeForRefinements.value(this.#detachedData());
732
+ this.#runValidatorFiber("", decoded.pipe(Effect.matchEffect({
733
+ onFailure: () => Effect.succeed([]),
734
+ onSuccess: (values) => FormBuilder.runAsyncRefinements(builder, values)
735
+ }), Effect.tap((issues) => Effect.sync(() => {
736
+ const cleared = Arr.reduce(this.#lastRefinementPaths, this.#sourcedSnapshot().client, (client, path) => replaceAt(client, path, []));
737
+ const client = Errors.mergeErrors(cleared, Errors.issuesToErrors(issues));
710
738
  this.#lastRefinementPaths = issues.map((issue) => issue.path);
711
739
  this.#sourced = {
712
740
  ...this.#sourced,
@@ -722,7 +750,7 @@ var FormStore = class {
722
750
  */
723
751
  reset(values) {
724
752
  if (values !== void 0) this.#initial = structuredClone(values);
725
- for (const key of Object.keys(this.#fibers)) this.#interruptFiber(key);
753
+ Arr.forEach(R.keys(this.#fibers), (key) => this.#interruptFiber(key));
726
754
  this.data = structuredClone(this.#initial);
727
755
  this.#previousData = this.#detachedData();
728
756
  this.#sourced = emptySourced;
@@ -745,7 +773,7 @@ var FormStore = class {
745
773
  valid: this.valid,
746
774
  posted: this.posted,
747
775
  data: this.#detachedData(),
748
- errors: this.errors,
776
+ errors: untypedErrors(this.errors),
749
777
  message: this.message,
750
778
  constraints: this.constraints
751
779
  };
@@ -812,14 +840,14 @@ var FormStore = class {
812
840
  this.posted = true;
813
841
  this.#options.onUpdated?.({ form: {
814
842
  data: this.data,
815
- errors: this.errors,
843
+ errors: untypedErrors(this.errors),
816
844
  valid: this.valid
817
845
  } });
818
846
  }
819
847
  #applyRedirect(location) {
820
848
  this.#lastSubmitOutcome = "success";
821
849
  if (this.#options.onRedirect) this.#options.onRedirect({ location });
822
- else if (typeof window !== "undefined") window.location.assign(location);
850
+ else if (!Predicate.isUndefined(globalThis.window)) window.location.assign(location);
823
851
  }
824
852
  #applyError(error) {
825
853
  this.#lastSubmitOutcome = "failure";
@@ -838,11 +866,12 @@ var FormStore = class {
838
866
  * actually just saved, not the original seed.
839
867
  */
840
868
  #applySubmittedData(data, result) {
869
+ const submittedData = $state.snapshot(this.#lastSubmitted ?? this.data);
841
870
  const applied = computeDataApplication({
842
- isSuccess: result._tag === "Success",
871
+ isSuccess: Predicate.isTagged(result, "Success"),
843
872
  appliedData: data,
844
873
  initial: this.#initial,
845
- submittedData: $state.snapshot(this.#lastSubmitted ?? this.data),
874
+ submittedData,
846
875
  resetForm: this.#options.resetForm ?? false,
847
876
  autoSubmit: this.#mode.autoSubmit
848
877
  });
@@ -879,7 +908,7 @@ var FormStore = class {
879
908
  }, delayMs);
880
909
  try {
881
910
  const submit = this.#options.submit ?? defaultSubmit;
882
- const action = node.action || (typeof location !== "undefined" ? location.href : "");
911
+ const action = node.action || (Predicate.isUndefined(globalThis.location) ? "" : location.href);
883
912
  let result;
884
913
  if (resolved !== void 0) result = resolved;
885
914
  else if (this.#options.handler) result = await this.#options.handler(this.data);
@@ -887,7 +916,7 @@ var FormStore = class {
887
916
  this.#options.onResult?.({ result });
888
917
  this.#applyUpdate(resultToUpdate(result), result);
889
918
  } catch (cause) {
890
- const message = cause instanceof Error ? cause.message : "Submission failed";
919
+ const message = Predicate.isError(cause) ? cause.message : "Submission failed";
891
920
  this.#lastSubmitOutcome = "failure";
892
921
  this.#options.onError?.({ error: message });
893
922
  } finally {
@@ -911,7 +940,7 @@ var FormStore = class {
911
940
  const beforeUnload = (event) => {
912
941
  if (this.#options.taintedMessage && this.isTainted) event.preventDefault();
913
942
  };
914
- if (this.#options.taintedMessage && typeof window !== "undefined") window.addEventListener("beforeunload", beforeUnload);
943
+ if (this.#options.taintedMessage && !Predicate.isUndefined(globalThis.window)) window.addEventListener("beforeunload", beforeUnload);
915
944
  this.#autoSubmitNode = node;
916
945
  this.#options.listeners?.onMount?.({ data: $state.snapshot(this.data) });
917
946
  const dispatchListeners = this.#buildDispatchListeners();
@@ -932,24 +961,31 @@ var FormStore = class {
932
961
  const snapshot = $state.snapshot(this.data);
933
962
  const changed = taintedLeafPaths(computeTainted(this.#previousData, snapshot));
934
963
  this.#previousData = snapshot;
935
- for (const path of changed) this.#onFieldChanged(path);
964
+ Arr.forEach(changed, (path) => this.#onFieldChanged(path));
936
965
  });
937
966
  });
938
967
  return { destroy: () => {
939
968
  node.removeEventListener("submit", handleSubmit);
940
- if (typeof window !== "undefined") window.removeEventListener("beforeunload", beforeUnload);
969
+ if (!Predicate.isUndefined(globalThis.window)) window.removeEventListener("beforeunload", beforeUnload);
941
970
  stopWatching?.();
942
971
  stopFieldWatching?.();
943
972
  clearTimeout(this.#validateTimer);
944
- for (const timer of this.#listenerTimers.values()) clearTimeout(timer);
945
- this.#listenerTimers.clear();
946
- for (const key of Object.keys(this.#fibers)) this.#interruptFiber(key);
973
+ Arr.forEach(R.values(this.#listenerTimers), (timer) => clearTimeout(timer));
974
+ this.#listenerTimers = {};
975
+ Arr.forEach(R.keys(this.#fibers), (key) => this.#interruptFiber(key));
947
976
  if (this.#autoSubmitNode === node) this.#autoSubmitNode = void 0;
948
977
  } };
949
978
  };
950
979
  /** Attachment form of {@link FormStore.enhance} for {@link FormStore.props}. */
951
- #enhanceAttachment = (node) => this.enhance(node).destroy;
952
- /** Build a {@link FormData} body from the current data (SPA / manual submit). */
980
+ #enhanceAttachment = (node) => {
981
+ return this.enhance(node).destroy;
982
+ };
983
+ /**
984
+ * Build a {@link FormData} body from the current data (SPA / manual submit).
985
+ *
986
+ * Each value is encoded as `objectToFormData` encodes it: a `File` or
987
+ * `Blob` is appended as a file and a `Date` as its UTC `YYYY-MM-DD`.
988
+ */
953
989
  toFormData() {
954
990
  return objectToFormData(this.data);
955
991
  }
@@ -957,10 +993,10 @@ var FormStore = class {
957
993
  function createForm(source, options = {}) {
958
994
  if (FormBuilder.isFormBuilder(source)) {
959
995
  const schema = FormBuilder.buildSchema(source);
960
- return new FormStore(Form.fromBuilder(source, {
961
- ...options.defaults !== void 0 ? { defaults: options.defaults } : {},
962
- ...options.id !== void 0 ? { id: options.id } : {}
963
- }).empty(), {
996
+ const builderOptions = {};
997
+ if (options.defaults !== void 0) builderOptions.defaults = options.defaults;
998
+ if (options.id !== void 0) builderOptions.id = options.id;
999
+ return new FormStore(Form.fromBuilder(source, builderOptions).empty(), {
964
1000
  ...options,
965
1001
  schema,
966
1002
  fields: source.fields,
@@ -968,18 +1004,15 @@ function createForm(source, options = {}) {
968
1004
  });
969
1005
  }
970
1006
  if ("defaults" in source && "schema" in source) {
971
- const seed = source;
972
- return new FormStore(makeForm(seed.schema, {
973
- defaults: seed.defaults,
974
- ...seed.id !== void 0 ? { id: seed.id } : {}
975
- }).empty(), {
1007
+ const makeOptions = { defaults: source.defaults };
1008
+ if (source.id !== void 0) makeOptions.id = source.id;
1009
+ return new FormStore(makeForm(source.schema, makeOptions).empty(), {
976
1010
  ...options,
977
- schema: seed.schema
1011
+ schema: source.schema
978
1012
  });
979
1013
  }
980
1014
  return new FormStore(source, options);
981
1015
  }
982
- //#endregion
983
1016
  export { FieldHandle, FormStore, createForm };
984
1017
 
985
1018
  //# sourceMappingURL=FormStore.svelte.js.map