--- title: 'Progressively enhancing forms with enhance' slug: progressively-enhancing-forms-with-enhance description: 'Progressively enhance HTML forms to submit via fetch when JavaScript is available.' --- ## Progressively enhancing forms with enhance `enhance` is a Svelte attachment that progressively enhances `
`. The same server action runs whether JS is available or not; with `{@attach enhance(...)}` the client submits over `fetch`, the server returns a JSON `MochiEnhanceResult` envelope, and there is no full-page reload. ```svelte
``` Place the form inside a hydrated island (`mochi:hydrate`, `mochi:hydrate:visible`, or `mochi:defer mochi:hydrate`). When hydration is skipped the attachment never runs and the form falls back to a native HTML POST — that is the progressive-enhancement contract. Do **NOT** call `enhance()` outside a hydrated island; instead, mark the surrounding component with `mochi:hydrate*` so the attachment can attach in the browser. `enhance` is a factory: call it as `{@attach enhance()}` even with no options. Attachments require Svelte 5.29+. ### Wire format `enhance` adds `Accept: application/json` and `x-mochi-action: true` to the POST. The server detects either header and responds with one of four shapes: ```ts type MochiEnhanceResult = | { type: 'success'; status: number; data?: unknown } | { type: 'failure'; status: number; data?: unknown } | { type: 'redirect'; status: number; location: string } | { type: 'error'; status?: number; error: unknown }; ``` HTTP status is `200` for `success`, `failure`, and `redirect`; the body's `status` field carries the action's status. For `error`, the HTTP status matches the error code. `data` is encoded with [devalue](https://www.npmjs.com/package/devalue) so `Date`, `Map`, `Set`, `BigInt`, and cyclic references survive the wire. ### Default fallback Without a callback, `enhance` runs a minimal default per result type: | `result.type` | Default | | ------------- | ------------------------------------------------- | | `success` | `form.reset()` | | `failure` | nothing (provide a callback to update the UI) | | `redirect` | `window.location.assign(result.location)` | | `error` | `console.error('[mochi] enhance:', result.error)` | **The default fallback is intentionally lean.** Mochi has no client-side `page.form` store, no `goto`, and no `invalidateAll` — the framework cannot auto-update component props or re-run server data after a submission. Pass a `submit` callback to react to `failure` or to do anything beyond a redirect. When the same component renders both as a hydrated island (where `enhance` will fire) and as a plain SSR-only child (where it won't), read the [auto-injected `isHydratable` prop](/docs/environment-constants/#auto-injected-props) to skip the SSR `form`-prop peek when the client will take over. ### Submit callback Pass a function as the argument. It runs once per submit and may return a result handler that fully replaces the default fallback: ```svelte
``` The result handler receives `{ result, formElement, formData, action, update }`. Call `update({ reset?: boolean })` to re-invoke the default fallback — useful when layering extra behavior on top of it. ### onPending Pass an options object with `onPending` instead of tracking a `pending` flag inside the submit function. It fires `true` immediately before the fetch and `false` once the result handler settles (or when the submission is cancelled): ```svelte
``` `onPending` fires `false` from a `finally` block, so it resets even if the fetch throws or the abort signal fires. ### Cancelling The `submit` callback receives `cancel` and `controller`: - `cancel()` — bail out before `fetch` is issued. No callback runs. - `controller.abort()` — cancel an in-flight request. The `AbortError` is silently swallowed. ### Server-side No server changes are needed beyond declaring the action. The same `Mochi.page(path, { actions })` definition serves both the no-JS HTML POST flow and the enhanced JSON flow: ```ts // file: src/index.ts import { Mochi, fail, success } from 'mochi-framework'; await Mochi.serve({ routes: { '/login': Mochi.page('./src/Login.svelte', { actions: { default: ({ formData }) => { const username = String(formData.get('username') ?? ''); if (!username) return fail(400, { error: 'Username required' }); return success({ username }); }, }, }), }, }); ``` Returning a `Response` directly from an action bypasses the JSON envelope on enhanced submissions — treat that path as an escape hatch. **Wrap data in `success()` to round-trip it to the client.** A plain return like `return { username }` strips the data on the enhanced path and the result handler receives an empty `data` object. This matches the non-enhanced behavior. Always use `success()` when the client needs the returned data. Do **NOT** return a plain object from an action when the enhanced client needs `data`; instead, wrap it with `success()` (or `fail()` for errors). ### deserialize `deserialize(text)` decodes a raw `MochiEnhanceResult` envelope. Use it when rolling your own `onsubmit` instead of `{@attach enhance(...)}`: ```svelte ``` ### When to use enhance Reach for `enhance` when the action's outcome should update UI without a navigation flicker — interactive forms, optimistic patterns, inline validation. Stick with a plain `
` when the action ends in a redirect anyway and the JS bundle is not worth shipping.