The Router and Forms
Morphs and the Router
Same-origin clicks and back/forward fetch and morph, no full reload. A form action or navigation morphs the page; layouts stay mounted.
goto('/login') // or goto(url, { replace: true })
invalidate() // run this page's load again
page.value.url.pathname // page: { url, status, form, state }
navigating.value // { from, to } while loading, else null
pushState('?tab=2', { tab: 2 }) // history entry, no navigation
replaceState('', { tab: 3 }) // this entry's state ('' keeps the URL)
- A component instance on an element the morph keeps keeps its state and gets the new
data.data-wisp-reseton an element around it starts it fresh. - Global stores (
store,persisted) are module state: they outlive every navigation. - Prefetch on hover (60 ms) and touch;
data-wisp-preload="off"opts out. - Scroll is restored; focus moves to
[autofocus]. A navigation lands at once, as a page load does, even underscroll-behavior: smooth, which still smooths same-page#links. - View transitions when available.
data-wisp-notransitionon a link, or on<body>for the app, skips them; reduced motion skips them too. data-wisp-reloadon a link or parent forces a full load. Links withtarget,download,rel="external"and/_app/are left alone.pushState(url, state)(shallow routing, for tabs and modals) adds an entry aturl('': this one) and loads nothing;page.value.stateis reactive ({}on other entries). Back/forward restores it with no request; a reload keeps it only at its URL.documentevents:wisp:navigate wisp:update wisp:goto wisp:refresh wisp:error wisp:push wisp:pop; forms:wisp:submit(cancelable),wisp:result.
Link and Navigation Options
data-wisp-noscroll, data-wisp-keepfocus, data-wisp-replacestate, data-wisp-notransition (on or around a link) keep scroll, keep focus, replace history, skip the view transition. goto(url, { noscroll, keepfocus, replace, novt }) does the same.
Hooks from 'wisp' return an unsubscribe:
| Hook | Does |
|---|---|
beforeNavigate(({ from, to, pop, cancel }) => ..) | Before leaving; cancel() does not stop back/forward. |
afterNavigate | After the swap. |
onNavigate | After fetch, before the swap; a returned promise is awaited, a returned function runs after. |
preloadData(url), preloadCode(url) | Fetch ahead. |
invalidateAll() | Rerun every load. |
updated.value | A newer wisp.js or build exists. |
+page.js load gets depends(key) (a fetched URL counts); invalidate('key') reruns only those loads, no page request.
Snapshots
Back, forward and reload restore each changed <input>, <textarea>, <select> (never passwords, files, hidden, autocomplete="off") from sessionStorage. A script keeps its own state; snapshot is the only export a script may have:
<script>
let open = false
export const snapshot = { capture: () => open, restore: (v) => (open = v) } // capture: any JSON
</script>
Phones and Offline
- Pages leave with
pagehide(back/forward cache; scroll restored). <body data-wisp-revalidate="30">refetches data when the tab or network returns (at most every N s, default 30; the morph keeps focus, scroll, typed text).- Offline,
<form data-wisp-queue>(safe to send twice) waits insessionStorage, is sent in order when back, then the page refreshes (wisp:sent). Only urlencoded forms queue. - Other forms show "You are offline" and fire
wisp:resultwitherror: "offline". - A navigation focuses the
<h1>(else<main>) and announces the title; view transitions skip underprefers-reduced-motion.
Forms: use:enhance
Plain forms already update in place; use:enhance adds hooks:
<form method="post" action="?/add" use:enhance="submit">
<input name="text" bind:value="text">
<button disabled={:pending}>Send</button>
</form>
<script>
let text = '', pending = false
function submit({ formData, cancel }) {
pending = true
return (result) => { pending = false } // after the page updated
}
</script>
- The first function gets
{ form, formData, submitter, action, cancel }; the returned one{ ok, status, location?, data?, error? }. - An action answering JSON (
Response::json_of(…)) leaves the form on the page;dataandpage.value.formhold it.
+page.js
Runs in the browser on every navigation, not on the server. Its return is the script's data.
export async function load({ data, url, params, route, fetch }) {
return { ...data, results: await (await fetch('/api/search?q=' + url.searchParams.get('q'))).json() }
}
With a server load the whole Data is sent (#[derive(Json)]). params.slug for blog/[slug]; route.id is /blog/[slug].
Is This Page Useful?