@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.
- package/README.md +19 -19
- package/dist/ClientValidate.d.ts +1 -2
- package/dist/ClientValidate.js +1 -2
- package/dist/EnhanceCore.d.ts +34 -4
- package/dist/EnhanceCore.js +106 -39
- package/dist/EnhanceCore.js.map +1 -1
- package/dist/EntityForm.d.ts +0 -2
- package/dist/EntityForm.js +29 -30
- package/dist/EntityForm.js.map +1 -1
- package/dist/FormStore.svelte.d.ts +11 -5
- package/dist/FormStore.svelte.js +94 -61
- package/dist/FormStore.svelte.js.map +1 -1
- package/dist/Listeners.d.ts +1 -2
- package/dist/Listeners.js +4 -4
- package/dist/Listeners.js.map +1 -1
- package/dist/SvelteKit.d.ts +1 -2
- package/dist/SvelteKit.js +3 -3
- package/dist/SvelteKit.js.map +1 -1
- package/dist/Tainted.d.ts +1 -2
- package/dist/Tainted.js +28 -41
- package/dist/Tainted.js.map +1 -1
- package/dist/_virtual/_rolldown/runtime.js +1 -2
- package/dist/kit/ActionRedirect.d.ts +0 -2
- package/dist/kit/ActionRedirect.js +12 -15
- package/dist/kit/ActionRedirect.js.map +1 -1
- package/dist/kit/CommandFailure.d.ts +1 -2
- package/dist/kit/CommandFailure.js +18 -20
- package/dist/kit/CommandFailure.js.map +1 -1
- package/dist/kit/Enhance.d.ts +0 -2
- package/dist/kit/Enhance.js +1 -2
- package/dist/kit/FormAction.d.ts +0 -2
- package/dist/kit/FormAction.js +39 -33
- package/dist/kit/FormAction.js.map +1 -1
- package/dist/kit/FormStateReaders.d.ts +0 -2
- package/dist/kit/FormStateReaders.js +4 -4
- package/dist/kit/FormStateReaders.js.map +1 -1
- package/dist/kit/Pointer.d.ts +0 -2
- package/dist/kit/Pointer.js +1 -2
- package/package.json +6 -6
package/dist/EntityForm.js.map
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
-
|
|
385
|
+
export {};
|
|
380
386
|
//# sourceMappingURL=FormStore.svelte.d.ts.map
|
package/dist/FormStore.svelte.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
387
|
-
this.#listenerTimers
|
|
388
|
-
this.#listenerTimers
|
|
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 &&
|
|
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)
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
871
|
+
isSuccess: Predicate.isTagged(result, "Success"),
|
|
843
872
|
appliedData: data,
|
|
844
873
|
initial: this.#initial,
|
|
845
|
-
submittedData
|
|
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 || (
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
964
|
+
Arr.forEach(changed, (path) => this.#onFieldChanged(path));
|
|
936
965
|
});
|
|
937
966
|
});
|
|
938
967
|
return { destroy: () => {
|
|
939
968
|
node.removeEventListener("submit", handleSubmit);
|
|
940
|
-
if (
|
|
969
|
+
if (!Predicate.isUndefined(globalThis.window)) window.removeEventListener("beforeunload", beforeUnload);
|
|
941
970
|
stopWatching?.();
|
|
942
971
|
stopFieldWatching?.();
|
|
943
972
|
clearTimeout(this.#validateTimer);
|
|
944
|
-
|
|
945
|
-
this.#listenerTimers
|
|
946
|
-
|
|
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) =>
|
|
952
|
-
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
|
972
|
-
|
|
973
|
-
|
|
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:
|
|
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
|