v1.0.0: merge into a single enhancedForm, add draft persistence and autoSubmit#1
Conversation
The two helpers were coupled in practice: createEnhancedForm took the validation instance as an option required for correctness, both built the same typed field proxy, and both required their own spreads in templates. enhancedForm merges them into one object with validation always wired into the submission lifecycle, so the manual updateIssues()/validateAll() calls in enhance handlers (and the silent miswiring when the validation option was forgotten) no longer exist. - validation.svelte.ts becomes an internal core (createValidationCore); the field proxy and form-level handlers are assembled by enhancedForm - Field spread renamed: fields.path.handlers -> fields.path.validate - Form-level spread renamed: formHandler -> handlers - resetOnSuccess (default true) replaced by the opt-in preventResetOnSuccess (default false) - reset() now resets the form element as well as the submission state; resetState() keeps the old state-only behavior - updateIssues() is internal; validateAll(), clearAllIssues(), allIssues, allKnownIssues, and formIssues remain public
Spread form.fields.<path>.persist onto an input and its value is saved to
web storage as the user types (debounced 300ms, flushed on change) and
restored when the input mounts — so a reload no longer loses what was
typed. Restored values go through kit's field.set() so as() spreads stay
in sync, and restored fields are marked dirty in validation.
Spreading .persist is the whole opt-in. The persist creation option only
overrides defaults: key (defaults to the remote form's action id, a
deterministic hash of the remote file path + export name that
self-invalidates when the function moves), storage ('local' default or
'session'), and maxAgeMs (expiry checked at restore; default none).
Drafts are discarded on successful submission, on form reset, on expiry,
or via the new discardPersisted(). File inputs are skipped. The attachment
spread is memoized per field path so template re-evaluations don't detach
and re-restore the field.
autoSubmit: boolean | { debounceMs?: number } (default 600ms). Once input
settles, the form submits itself via element.requestSubmit() — a real
submit event, so preflight validation, the submit-attempt issue display,
and the enhance pipeline all run exactly as they would for a button press.
The listener rides in the form-level handlers spread as an attachment.
Built-in behaviors rather than options:
- Dirty check: data identical to the last submission is never re-submitted
- In-flight coalescing: a debounce firing mid-submission waits for it to
settle, then submits once more only if the data changed since
- Commit flush: a change event (text blur, select/checkbox pick) submits
immediately instead of waiting out the debounce
- A debounce still pending at teardown is dropped (persistence holds the
draft)
Auto-submitting forms never reset after success, so preventResetOnSuccess
is not accepted alongside autoSubmit — the options are mutually exclusive
at the type level.
43 tests across three suites: - form.test.ts: the submission state machine (success/issues/error flows, default reset vs preventResetOnSuccess, cancel, delayed/timeout timer progression, the superseded-submission race, reset vs resetState) and autoSubmit behavior (debounce window, custom debounceMs, change flush, the unchanged-data dirty check, mid-submission coalescing, dropped debounce at teardown, no reset after auto-save) - persist.test.ts: restore into element/kit state/dirty tracking, expiry, debounced writes with change flush, checkbox round-trip, shared drafts, sessionStorage selection, discard paths, memoized attachment identity - validation.test.ts: issue-layer merging and clearing, the allIssues tree, validator lifecycle, dirty-gated debounced blur validation Vitest is used rather than bun test because the runes in .svelte.ts modules need the Svelte compiler and the persist/autoSubmit tests need a DOM. jsdom ships no web storage, so a minimal in-memory implementation is installed in the test setup. CI's test job runs the suite after the type checks.
Rewrite the READMEs around the single enhancedForm export: creation options, the three template spreads (handlers on the form, validate and persist per field), draft persistence, autoSubmit, and the submission state/callbacks. The old wiring caveats (passing validation to createEnhancedForm, calling updateIssues/validateAll manually in enhance handlers) are gone since that class of miswiring no longer exists. The contact-form example now uses enhancedForm and demonstrates draft persistence on all three fields — type into the form, reload, and the values come back. The changelog records the merge and the new features, with migration notes for each breaking change.
The API has settled: one enhancedForm export covering submission state, validation, draft persistence, and auto-submit, locked in by compile-time type tests and a runtime suite. Stamp the changelog and bump the package version for the 1.0.0 tag.
The section opened with the new features, burying the fact that the entire public API changed. Open with a breaking-rewrite notice and a 0.1.0 -> 1.0.0 migration table, and list the BREAKING entries before the additions.
There was a problem hiding this comment.
6 issues found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/remotes/src/lib/form.svelte.ts">
<violation number="1" location="packages/remotes/src/lib/form.svelte.ts:8">
P1: Consumers on allowed Svelte 5.0–5.28 installs can fail at import time because this file now depends on `svelte/attachments`, which is only available since Svelte 5.29. The Svelte peer range should be raised to match the new API requirement.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| RemoteQueryUpdate | ||
| } from '@sveltejs/kit' | ||
| import type { Attachment } from 'svelte/attachments' | ||
| import { createAttachmentKey } from 'svelte/attachments' |
There was a problem hiding this comment.
P1: Consumers on allowed Svelte 5.0–5.28 installs can fail at import time because this file now depends on svelte/attachments, which is only available since Svelte 5.29. The Svelte peer range should be raised to match the new API requirement.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/remotes/src/lib/form.svelte.ts, line 8:
<comment>Consumers on allowed Svelte 5.0–5.28 installs can fail at import time because this file now depends on `svelte/attachments`, which is only available since Svelte 5.29. The Svelte peer range should be raised to match the new API requirement.</comment>
<file context>
@@ -0,0 +1,703 @@
+ RemoteQueryUpdate
+} from '@sveltejs/kit'
+import type { Attachment } from 'svelte/attachments'
+import { createAttachmentKey } from 'svelte/attachments'
+import { tick } from 'svelte'
+import {
</file context>
A kit checkbox-array field (as('checkbox', value)) renders several
same-named checkboxes that all belong to one string[] field, so they all
share one draft key. Storing each element's checked flag as a bare boolean
meant the last change overwrote the group's state, and restore applied
that boolean to every box — check only 'music', reload, and every box in
the group came back checked. The corrupted restore also leaked into
validation through both of its inputs: field.set(true) put a boolean into
a string[] kit field (whose checked getter calls .includes on it), and
preflight validated the wrongly-checked DOM.
Group members are detected by kit's []-suffixed name. readValue now
collects the group's selected values through el.form and stores the array
kit itself uses; restore checks each box by membership.
Also coerce restored values before writing them into kit's field state,
using the same name-prefix encoding kit uses to coerce FormData strings
(n:count -> number, b:agree -> boolean), so validators and fields.value()
see correctly typed values mid-session.
- Raise the svelte peer dependency to ^5.29.0 — the package imports svelte/attachments, which was introduced in that release - Forward updates() to kit even when called with zero arguments: kit treats submit().updates() with no args as taking control of updates (suppressing invalidateAll), so it must not be dropped - Reject persisted drafts whose fields property is null instead of crashing field mount (typeof null === 'object' passed the shape guard) - Use Object.hasOwn when restoring so inherited properties like toString can't be mistaken for saved fields - Only clear shared auto-submit state at teardown when the cleanup still owns it (guard against a newer attachment racing an older cleanup) - Split the README creation-options example into two enhancedForm calls so it compiles (preventResetOnSuccess and autoSubmit are mutually exclusive and were shown in one options object) - Export ValidationHandlers — it is the type of form.fields.x.validate
- Cancel pending debounced draft writes on discard() via a write generation: a write scheduled just before a successful submission's discard no longer fires afterwards and resurrects the stale draft (input after the discard schedules at the new generation and persists normally) - Cancel the auto-submit debounce and queued flag on form reset — the debounce could previously fire after a reset and submit the freshly-emptied form - Flush a queued auto-submit on the onSubmit-threw path like every other settle path, and clear the queued flag in resetState() (the invalidated submission can never flush it) - Capture the auto-submit dirty-check snapshot on the submit event — the moment kit captures its FormData — instead of when the enhance callback runs. Input arriving during an async preflight was recorded as submitted without being sent, so the next debounce compared equal and skipped genuinely unsaved data.
There was a problem hiding this comment.
1 issue found across 9 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/remotes/src/lib/form.svelte.ts">
<violation number="1" location="packages/remotes/src/lib/form.svelte.ts:8">
P1: Consumers on allowed Svelte 5.0–5.28 installs can fail at import time because this file now depends on `svelte/attachments`, which is only available since Svelte 5.29. The Svelte peer range should be raised to match the new API requirement.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
An array passes the typeof-object and null checks, but JSON.stringify drops string-keyed properties on arrays — so a corrupt array draft would silently swallow every subsequent write and never self-heal. Treat it like any other malformed draft: remove it, and let the next write start a fresh object draft.
- /contact — the existing validation + submission-state demo - /profile — auto-save: autoSubmit with no save button, a live Saving/Saved status line driven by form state - /application — draft persistence across text inputs, a select, a textarea, and a checkbox group, with maxAgeMs expiry and a discard-draft button A shared layout adds a tab bar to switch between examples, and every page ends with a 'View the code' panel showing its +page.svelte, remote function, and schema via ?raw imports. Remote files can't be imported with ?raw directly (kit's remote transform matches them by filename and rejects the raw module's default export), so a small vite plugin serves them under a ?raw-source query through a virtual id kit never matches.
There was a problem hiding this comment.
6 issues found across 17 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="examples/contact-form/src/routes/+layout.svelte">
<violation number="1" location="examples/contact-form/src/routes/+layout.svelte:7">
P2: These root-relative navigation links won't work correctly if the app is ever deployed with a `paths.base` subpath in SvelteKit. Root-relative `/contact`, `/profile`, and `/application` hrefs do not automatically include the configured base prefix, so they'd navigate to the domain root rather than the mounted path. The `class:active` comparison against `page.url.pathname` would also mismatch under a base path. Consider importing `base` from `$app/paths` and composing links as `${base}/contact`, or using the `resolve` helper from `$app/paths` so the example remains robust regardless of deployment path.</violation>
</file>
<file name="packages/remotes/src/lib/form.svelte.ts">
<violation number="1" location="packages/remotes/src/lib/form.svelte.ts:8">
P1: Consumers on allowed Svelte 5.0–5.28 installs can fail at import time because this file now depends on `svelte/attachments`, which is only available since Svelte 5.29. The Svelte peer range should be raised to match the new API requirement.</violation>
</file>
<file name="examples/contact-form/src/lib/CodeViewer.svelte">
<violation number="1" location="examples/contact-form/src/lib/CodeViewer.svelte:14">
P2: The tab UI uses partial ARIA tab roles without completing the required pattern. The `role="tablist"` and `role="tab"` attributes tell assistive technologies this is a tab widget, but the implementation is missing `role="tabpanel"` on the content area, `aria-controls`/`aria-labelledby` linking tabs to panels, roving `tabindex`, and arrow/Home/End keyboard handling. Screen reader users will hear "tab" but cannot navigate it with expected keyboard shortcuts. Consider either completing the ARIA tabs implementation or removing the tab roles and keeping this as a simple button group.</violation>
</file>
<file name="examples/contact-form/package.json">
<violation number="1" location="examples/contact-form/package.json:19">
P2: Adding `@types/node@^26.1.0` introduces two maintainability risks for a SvelteKit example app:
1. The types target Node 26, but CI (`release.yml`) runs on Node 24, so TypeScript may accept APIs that fail at runtime.
2. `tsconfig.json` does not specify `compilerOptions.types`, so Node globals like `process` and `Buffer` leak into all browser/client modules, masking client-side runtime errors.
Consider either downgrading `@types/node` to match the CI runtime, scoping it with `types: ["node"]` only where needed, or adding `@types/node` to a server-only tsconfig/project reference rather than the root example package.</violation>
</file>
<file name="examples/contact-form/src/routes/profile/+page.svelte">
<violation number="1" location="examples/contact-form/src/routes/profile/+page.svelte:41">
P1: The invalid fields and error messages lack ARIA state/relationship attributes, making the form inaccessible to screen-reader users. The inputs only receive a CSS `invalid` class, and the error `<p>` elements have no IDs or `aria-describedby` linkage. This pattern will be copied by consumers since it is example code for the library.
Adding `aria-invalid="true"` when a field has issues, and connecting the error paragraphs to the input via `aria-describedby` with generated IDs, ensures assistive technologies announce validation state and messages.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| {...profileForm.fields.displayName.as('text', 'Ada Lovelace')} | ||
| {...form.fields.displayName.validate} | ||
| autocomplete="nickname" | ||
| class:invalid={form.fields.displayName.issues} |
There was a problem hiding this comment.
P1: The invalid fields and error messages lack ARIA state/relationship attributes, making the form inaccessible to screen-reader users. The inputs only receive a CSS invalid class, and the error <p> elements have no IDs or aria-describedby linkage. This pattern will be copied by consumers since it is example code for the library.
Adding aria-invalid="true" when a field has issues, and connecting the error paragraphs to the input via aria-describedby with generated IDs, ensures assistive technologies announce validation state and messages.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/contact-form/src/routes/profile/+page.svelte, line 41:
<comment>The invalid fields and error messages lack ARIA state/relationship attributes, making the form inaccessible to screen-reader users. The inputs only receive a CSS `invalid` class, and the error `<p>` elements have no IDs or `aria-describedby` linkage. This pattern will be copied by consumers since it is example code for the library.
Adding `aria-invalid="true"` when a field has issues, and connecting the error paragraphs to the input via `aria-describedby` with generated IDs, ensures assistive technologies announce validation state and messages.</comment>
<file context>
@@ -0,0 +1,152 @@
+ {...profileForm.fields.displayName.as('text', 'Ada Lovelace')}
+ {...form.fields.displayName.validate}
+ autocomplete="nickname"
+ class:invalid={form.fields.displayName.issues}
+ />
+ </label>
</file context>
| let { children } = $props() | ||
|
|
||
| const examples = [ | ||
| { href: '/contact', label: 'Contact form', hint: 'validation & submission state' }, |
There was a problem hiding this comment.
P2: These root-relative navigation links won't work correctly if the app is ever deployed with a paths.base subpath in SvelteKit. Root-relative /contact, /profile, and /application hrefs do not automatically include the configured base prefix, so they'd navigate to the domain root rather than the mounted path. The class:active comparison against page.url.pathname would also mismatch under a base path. Consider importing base from $app/paths and composing links as ${base}/contact, or using the resolve helper from $app/paths so the example remains robust regardless of deployment path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/contact-form/src/routes/+layout.svelte, line 7:
<comment>These root-relative navigation links won't work correctly if the app is ever deployed with a `paths.base` subpath in SvelteKit. Root-relative `/contact`, `/profile`, and `/application` hrefs do not automatically include the configured base prefix, so they'd navigate to the domain root rather than the mounted path. The `class:active` comparison against `page.url.pathname` would also mismatch under a base path. Consider importing `base` from `$app/paths` and composing links as `${base}/contact`, or using the `resolve` helper from `$app/paths` so the example remains robust regardless of deployment path.</comment>
<file context>
@@ -0,0 +1,93 @@
+ let { children } = $props()
+
+ const examples = [
+ { href: '/contact', label: 'Contact form', hint: 'validation & submission state' },
+ { href: '/profile', label: 'Profile settings', hint: 'auto-save' },
+ { href: '/application', label: 'Job application', hint: 'draft persistence' }
</file context>
| </button> | ||
|
|
||
| {#if open} | ||
| <div class="tabs" role="tablist"> |
There was a problem hiding this comment.
P2: The tab UI uses partial ARIA tab roles without completing the required pattern. The role="tablist" and role="tab" attributes tell assistive technologies this is a tab widget, but the implementation is missing role="tabpanel" on the content area, aria-controls/aria-labelledby linking tabs to panels, roving tabindex, and arrow/Home/End keyboard handling. Screen reader users will hear "tab" but cannot navigate it with expected keyboard shortcuts. Consider either completing the ARIA tabs implementation or removing the tab roles and keeping this as a simple button group.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/contact-form/src/lib/CodeViewer.svelte, line 14:
<comment>The tab UI uses partial ARIA tab roles without completing the required pattern. The `role="tablist"` and `role="tab"` attributes tell assistive technologies this is a tab widget, but the implementation is missing `role="tabpanel"` on the content area, `aria-controls`/`aria-labelledby` linking tabs to panels, roving `tabindex`, and arrow/Home/End keyboard handling. Screen reader users will hear "tab" but cannot navigate it with expected keyboard shortcuts. Consider either completing the ARIA tabs implementation or removing the tab roles and keeping this as a simple button group.</comment>
<file context>
@@ -0,0 +1,86 @@
+ </button>
+
+ {#if open}
+ <div class="tabs" role="tablist">
+ {#each files as file, index (file.name)}
+ <button
</file context>
| "@sveltejs/adapter-auto": "^6.1.0", | ||
| "@sveltejs/kit": "^2.69.1", | ||
| "@sveltejs/vite-plugin-svelte": "^7.1.2", | ||
| "@types/node": "^26.1.0", |
There was a problem hiding this comment.
P2: Adding @types/node@^26.1.0 introduces two maintainability risks for a SvelteKit example app:
- The types target Node 26, but CI (
release.yml) runs on Node 24, so TypeScript may accept APIs that fail at runtime. tsconfig.jsondoes not specifycompilerOptions.types, so Node globals likeprocessandBufferleak into all browser/client modules, masking client-side runtime errors.
Consider either downgrading @types/node to match the CI runtime, scoping it with types: ["node"] only where needed, or adding @types/node to a server-only tsconfig/project reference rather than the root example package.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/contact-form/package.json, line 19:
<comment>Adding `@types/node@^26.1.0` introduces two maintainability risks for a SvelteKit example app:
1. The types target Node 26, but CI (`release.yml`) runs on Node 24, so TypeScript may accept APIs that fail at runtime.
2. `tsconfig.json` does not specify `compilerOptions.types`, so Node globals like `process` and `Buffer` leak into all browser/client modules, masking client-side runtime errors.
Consider either downgrading `@types/node` to match the CI runtime, scoping it with `types: ["node"]` only where needed, or adding `@types/node` to a server-only tsconfig/project reference rather than the root example package.</comment>
<file context>
@@ -16,6 +16,7 @@
"@sveltejs/adapter-auto": "^6.1.0",
"@sveltejs/kit": "^2.69.1",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
+ "@types/node": "^26.1.0",
"svelte": "^5.56.4",
"svelte-check": "^4.7.1",
</file context>
…READMEs - Watch the real .remote.ts path from the raw-source virtual module so edits invalidate the code viewer in dev - Track editing state on the profile page: form.result stays true until the next submission begins, so the status line showed 'Saved' over unsaved edits while the auto-submit debounce settled; issues now also rank above the stale result - Mention the svelte >=5.29 requirement alongside kit in both READMEs
There was a problem hiding this comment.
1 issue found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="examples/contact-form/src/routes/+layout.svelte">
<violation number="1" location="examples/contact-form/src/routes/+layout.svelte:7">
P2: These root-relative navigation links won't work correctly if the app is ever deployed with a `paths.base` subpath in SvelteKit. Root-relative `/contact`, `/profile`, and `/application` hrefs do not automatically include the configured base prefix, so they'd navigate to the domain root rather than the mounted path. The `class:active` comparison against `page.url.pathname` would also mismatch under a base path. Consider importing `base` from `$app/paths` and composing links as `${base}/contact`, or using the `resolve` helper from `$app/paths` so the example remains robust regardless of deployment path.</violation>
</file>
<file name="packages/remotes/src/lib/form.svelte.ts">
<violation number="1" location="packages/remotes/src/lib/form.svelte.ts:8">
P1: Consumers on allowed Svelte 5.0–5.28 installs can fail at import time because this file now depends on `svelte/attachments`, which is only available since Svelte 5.29. The Svelte peer range should be raised to match the new API requirement.</violation>
</file>
<file name="examples/contact-form/src/lib/CodeViewer.svelte">
<violation number="1" location="examples/contact-form/src/lib/CodeViewer.svelte:14">
P2: The tab UI uses partial ARIA tab roles without completing the required pattern. The `role="tablist"` and `role="tab"` attributes tell assistive technologies this is a tab widget, but the implementation is missing `role="tabpanel"` on the content area, `aria-controls`/`aria-labelledby` linking tabs to panels, roving `tabindex`, and arrow/Home/End keyboard handling. Screen reader users will hear "tab" but cannot navigate it with expected keyboard shortcuts. Consider either completing the ARIA tabs implementation or removing the tab roles and keeping this as a simple button group.</violation>
</file>
<file name="examples/contact-form/package.json">
<violation number="1" location="examples/contact-form/package.json:19">
P2: Adding `@types/node@^26.1.0` introduces two maintainability risks for a SvelteKit example app:
1. The types target Node 26, but CI (`release.yml`) runs on Node 24, so TypeScript may accept APIs that fail at runtime.
2. `tsconfig.json` does not specify `compilerOptions.types`, so Node globals like `process` and `Buffer` leak into all browser/client modules, masking client-side runtime errors.
Consider either downgrading `@types/node` to match the CI runtime, scoping it with `types: ["node"]` only where needed, or adding `@types/node` to a server-only tsconfig/project reference rather than the root example package.</violation>
</file>
<file name="examples/contact-form/src/routes/profile/+page.svelte">
<violation number="1" location="examples/contact-form/src/routes/profile/+page.svelte:24">
P2: The local `editing` flag can get permanently stuck in the wrong state, which will mislead users. It flips to `true` on every keystroke but is only cleared when a submission actually starts (`form.pending`). Because the example's auto-submit skips sending data that hasn't really changed, a user who types and then reverts before the debounce ends will never trigger `pending`, leaving `editing === true` and the status message stuck on "Unsaved changes — pausing will save them" indefinitely. Instead of relying only on `oninput` and `pending`, the component should tie `editing` more directly to whether the current field values differ from the last saved state (for example, by diffing against `form.result` data or by using a library-level dirty indicator if available).</violation>
<violation number="2" location="examples/contact-form/src/routes/profile/+page.svelte:41">
P1: The invalid fields and error messages lack ARIA state/relationship attributes, making the form inaccessible to screen-reader users. The inputs only receive a CSS `invalid` class, and the error `<p>` elements have no IDs or `aria-describedby` linkage. This pattern will be copied by consumers since it is example code for the library.
Adding `aria-invalid="true"` when a field has issues, and connecting the error paragraphs to the input via `aria-describedby` with generated IDs, ensures assistive technologies announce validation state and messages.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Typing and then reverting to the saved value before the debounce fired left the keystroke-latched editing flag stuck on 'Unsaved changes' — auto-submit correctly skips unchanged data, so no submission ever cleared it. Derive the flag by comparing the form's current data against the last saved values instead, updated from onReturn.
fields.value() types every property as optional; spreading over the previous snapshot keeps the required shape.
Summary
Rewrites the package around a single
enhancedFormexport and adds the two features planned for this release: per-field draft persistence and auto-submit. Prepared as the v1.0.0 release (version bumped, changelog stamped — tagv1.0.0after merge to publish).The merge (breaking)
createValidation+createEnhancedForm→enhancedForm. The two were coupled in practice — one took the other as an option required for correctness, both built the same typed field proxy, and both demanded their own template spreads. Now there is one object, validation is always wired into the submission lifecycle (the manualupdateIssues()/validateAll()calls in enhance handlers are gone), and the failure mode of forgetting thevalidationoption no longer exists.Migration:
createValidation(remote)+createEnhancedForm(remote, { validation })→enhancedForm(remote, options){...valid.formHandler}→{...form.handlers}{...valid.fields.x.handlers}→{...form.fields.x.validate}resetOnSuccess: false→preventResetOnSuccess: true(opt-in polarity)reset()now resets the form element as well as the state;resetState()is the old state-only behaviorDraft persistence
Spread
form.fields.<path>.persiston an input and its value survives reloads: debounced writes to web storage, restore on mount (through kit'sfield.set()soas()spreads stay in sync, marked dirty in validation), discarded on successful submit / form reset / expiry /discardPersisted(). The storage key defaults to the remote form's action id — a deterministic hash of the remote file path + export name, verified against the kit source, so it's stable across builds and self-invalidates on rename. Options:key,storage: 'local' | 'session',maxAgeMs.autoSubmit
autoSubmit: true | { debounceMs }(default 600ms) submits viarequestSubmit()once input settles — a real submit event, so preflight, submit-attempt issue display, and enhance callbacks run as for a button press. Built in: unchanged-data dirty check, mid-submission coalescing, immediate flush onchange. Mutually exclusive withpreventResetOnSuccessat the type level (auto-submitting forms never reset).Testing
New vitest + jsdom suite (43 tests): state machine flows and races, autoSubmit debounce/dirty-check/coalescing, persist restore/expiry/discard, validation issue layers and validators. CI's test job runs it after the type checks (
scripts/ci/test.shnow calls the vitest script — bun test can't compile runes or provide a DOM). Compile-time type tests extended for the new conditional surface.Reviewable commit-by-commit: merge → persist → autoSubmit → tests → docs → release prep. Each commit type-checks; lint/test/build CI scripts all pass at HEAD.
Not verified interactively: persist restore and autoSubmit timing in a real browser — the example app (
examples/contact-form) now has.persiston all fields for a quick manual check.🤖 Generated with Claude Code
Summary by cubic
Consolidates
@opensky/remotesaround a singleenhancedFormexport and adds per‑field draft persistence and auto‑submit for a simpler, faster form experience. Polishes v1.0.0 with stability fixes for auto‑submit and persistence, safer draft restore, updated docs noting thesvelte >=5.29requirement, and a three‑demo example app with live code viewing.New Features
enhancedForm(remote, options)with a clear submission state machine and lifecycle callbacks.form.fields.<path>.persist(debounced writes, restore on mount; options:key,storage,maxAgeMs). Correctly persists checkbox groups as selected values and restores typed values for validators andfields.value().autoSubmit: true | { debounceMs }(dirty check, in-flight coalescing, change flush). Auto-submit forms never reset on success..validatefield spreads and per-field validators; no manualupdateIssues()/validateAll()calls.raw-remote-sourceVite plugin. The code viewer watches real.remote.tsfiles for live updates; the profile demo compares current data against the last saved values for its editing indicator and now merges submitted values over the saved snapshot to keep a stable shape (issues also outrank stale results).discard(), cancel auto-submit debounce/queue on reset and flush after thrown submits, capture the dirty-check snapshot at submit time, guard draft restore (fieldsnull check, array rejection, andObject.hasOwn), forwardupdates()calls with no args, exportValidationHandlers, and bumpsveltepeer to^5.29.0(READMEs notesvelte >=5.29).Migration
createValidation(remote)+createEnhancedForm(remote, { validation })withenhancedForm(remote, options).{...valid.formHandler}with{...form.handlers}.{...valid.fields.x.handlers}with{...form.fields.x.validate}.resetOnSuccess: false→preventResetOnSuccess: true.reset()now resets the form element and state; useresetState()for state-only.autoSubmitcannot be used withpreventResetOnSuccess(mutually exclusive).Written for commit 5062c29. Summary will update on new commits.