Auto-save profile
There is no save button. Edit a field and pause — the form auto-submits once you stop typing (or immediately when a field commits on blur). Unchanged data is never re-submitted.
What's going on here
autoSubmit: { debounceMs: 800 } makes the form submit itself via requestSubmit() once input settles — a real submit event, so preflight
validation and the enhance callbacks run exactly as they would for a button press. A change event (like a text field blur) flushes immediately instead of waiting out
the debounce, and data identical to the last submission is never re-submitted.
The status line is driven by the form's state machine plus one page-local flag: form.result stays true until the next submission starts, so the page compares the
current values against the last saved ones to avoid showing "Saved" over unsaved edits while
the debounce is still settling.
<script lang="ts">
import { enhancedForm } from '@opensky/remotes'
import { profileForm } from './form.remote'
import { profileSchema } from './schema'
// No save button anywhere: edits submit on their own once input settles.
// autoSubmit implies the form never resets after a successful save.
const form = enhancedForm(profileForm, {
autoSubmit: { debounceMs: 800 },
delayMs: 300
})
const initialProfile = {
displayName: 'Ada Lovelace',
bio: 'Wrote the first computer program.'
}
// `form.result` stays true until the next submission begins, so the page
// tracks unsaved edits itself by comparing against the last saved values —
// otherwise "Saved" would show over unsaved edits while the debounce settles
let saved = $state(initialProfile)
let editing = $state(false)
function checkEditing(element: HTMLFormElement) {
const data = new FormData(element)
editing = data.get('displayName') !== saved.displayName || data.get('bio') !== saved.bio
}
$effect(() => {
if (form.pending) {
editing = false
}
})
</script>
<form
class="demo"
{...form.handlers}
{...profileForm.preflight(profileSchema).enhance((instance) =>
form.enhance(instance, {
onReturn: ({ form: submitted }) => {
saved = { ...saved, ...submitted.fields.value() }
}
})
)}
oninput={(event) => checkEditing(event.currentTarget)}
>
<label>
Display name
<input
{...profileForm.fields.displayName.as('text', 'Ada Lovelace')}
{...form.fields.displayName.validate}
autocomplete="nickname"
class:invalid={form.fields.displayName.issues}
/>
</label>
{#if form.fields.displayName.issues}
{#each form.fields.displayName.issues as issue (issue)}
<p class="error">{issue}</p>
{/each}
{/if}
<label>
Bio
<textarea
{...profileForm.fields.bio.as('text', 'Wrote the first computer program.')}
{...form.fields.bio.validate}
rows="3"
class:invalid={form.fields.bio.issues}
></textarea>
</label>
{#if form.fields.bio.issues}
{#each form.fields.bio.issues as issue (issue)}
<p class="error">{issue}</p>
{/each}
{/if}
<p class="status" aria-live="polite">
{#if form.pending}
<span class="saving">Saving…</span>
{:else if form.issues}
<span class="error">Fix the highlighted fields to save</span>
{:else if editing}
<span class="saving">Unsaved changes — pausing will save them</span>
{:else if form.result}
<span class="saved">Saved at {profileForm.result?.savedAt}</span>
{:else}
<span class="idle">Changes save automatically</span>
{/if}
</p>
</form>import { form } from '$app/server'
import { profileSchema } from './schema'
export const profileForm = form(profileSchema, async ({ displayName, bio }) => {
console.log('Saving profile', displayName, bio)
// Simulate a save round-trip so the auto-save indicator is visible
await new Promise((resolve) => setTimeout(resolve, 600))
return {
savedAt: new Date().toLocaleTimeString()
}
})import * as v from 'valibot'
export const profileSchema = v.object({
displayName: v.pipe(v.string(), v.minLength(2, 'Display name is too short')),
bio: v.pipe(v.string(), v.maxLength(160, 'Bio must fit in 160 characters'))
})