diff --git a/.changeset/fresh-shallows-navigate.md b/.changeset/fresh-shallows-navigate.md new file mode 100644 index 000000000000..98f8eca18620 --- /dev/null +++ b/.changeset/fresh-shallows-navigate.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +breaking: adjust how `pushState/replaceState` behave diff --git a/.changeset/late-lands-watch.md b/.changeset/late-lands-watch.md new file mode 100644 index 000000000000..6f52cfba61ad --- /dev/null +++ b/.changeset/late-lands-watch.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: provide possibility to preserve state across reloads when using `pushState/replaceState` diff --git a/documentation/docs/30-advanced/67-shallow-routing.md b/documentation/docs/30-advanced/67-shallow-routing.md index 09dbd8e4f178..cf8095f41937 100644 --- a/documentation/docs/30-advanced/67-shallow-routing.md +++ b/documentation/docs/30-advanced/67-shallow-routing.md @@ -15,8 +15,8 @@ SvelteKit makes this possible with the [`pushState`]($app-navigation#pushState) import { page } from '$app/state'; import Modal from './Modal.svelte'; - function showModal() { - pushState('', { + async function showModal() { + await pushState('', { showModal: true }); } @@ -31,11 +31,25 @@ The modal can be dismissed by navigating back (unsetting `page.state.showModal`) ## API -The first argument to `pushState` is the URL, relative to the current URL. To stay on the current URL, use `''`. +The first argument to `pushState` is the URL, relative to the current URL. A non-empty URL performs a shallow navigation: [`beforeNavigate`]($app-navigation#beforeNavigate), [`onNavigate`]($app-navigation#onNavigate), [`onNavigate`]($app-navigation#onNavigate), [`onNavigate`]($app-navigation#onNavigate) and [`afterNavigate`]($app-navigation#afterNavigate) will run with `navigation.type === 'shallow'`. + +Once shallow routing is active, `page.shallow` is set with the URL, parameters and route id that is user-visible. `page.url`, `page.params` and `page.route` remain as they were before the shallow navigation. + +To keep the current URL, use `''`. This only updates history and page state, and does not call navigation lifecycle functions. If the page is already showing a shallow route, `page.shallow` is preserved; otherwise it remains `null`. + +Passing `null` as the first argument will exit shallow routing (as do `goto` or link clicks). This reverts the visible URL to what it was before and sets `page.shallow` to `null`. The second argument is the new page state, which can be accessed via the [page object]($app-state#page) as `page.state`. You can make page state type-safe by declaring an [`App.PageState`](types#PageState) interface (usually in `src/app.d.ts`). -To set page state without creating a new history entry, use `replaceState` instead of `pushState`. +After a shallow navigation, `page.shallow` contains the target `url`, `params` and `route`. The actual page has not changed, so `page.url`, `page.params` and `page.route` continue to describe the page that is currently rendered. + +`pushState` and `replaceState` return promises that resolve after the page state change has been applied to the DOM. To set page state without creating a new history entry, use `replaceState` instead of `pushState`. + +By default, state set through `pushState` or `replaceState` is discarded on a full page reload. Pass `{ persist: true }` as the third argument to restore it in the browser after reloading, independently of whether the call performs a shallow navigation: + +```js +await pushState('', { showModal: true }, { persist: true }); +``` ## Loading data for a route @@ -74,7 +88,7 @@ For this to work, you need to load the data that the `+page.svelte` expects. A c const result = await preloadData(href); if (result.type === 'loaded' && result.status === 200) { - pushState(href, { selected: result.data }); + await pushState(href, { selected: result.data }); } else { // something bad happened! try navigating goto(href); @@ -96,6 +110,6 @@ For this to work, you need to load the data that the `+page.svelte` expects. A c ## Caveats -During server-side rendering, `page.state` is always an empty object. The same is true for the first page the user lands on — if the user reloads the page (or returns from another document), state will _not_ be applied until they navigate. +During server-side rendering, `page.state` is always an empty object. State set through `goto` is always restored in the browser after a reload. State set through `pushState` or `replaceState` is only restored if `{ persist: true }` was passed. Shallow routing is a feature that requires JavaScript to work. Be mindful when using it and try to think of sensible fallback behavior in case JavaScript isn't available. diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index bbdc331cec80..09fa6045d45b 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -1268,8 +1268,9 @@ export interface NavigationTarget< * - `leave`: The app is being left either because the tab is being closed or a navigation to a different document is occurring * - `link`: Navigation was triggered by a link click * - `popstate`: Navigation was triggered by back/forward navigation + * - `shallow`: Navigation was triggered by a `pushState(...)` or `replaceState(...)` call with a non-empty URL */ -export type NavigationType = 'enter' | 'form' | 'leave' | 'link' | 'goto' | 'popstate'; +export type NavigationType = 'enter' | 'form' | 'leave' | 'link' | 'goto' | 'popstate' | 'shallow'; export interface NavigationBase { /** @@ -1280,6 +1281,7 @@ export interface NavigationBase { * - `leave`: The app is being left either because the tab is being closed or a navigation to a different document is occurring * - `link`: Navigation was triggered by a link click * - `popstate`: Navigation was triggered by back/forward navigation + * - `shallow`: Navigation was triggered by a `pushState(...)` or `replaceState(...)` call with a non-empty URL */ type: NavigationType; /** @@ -1327,6 +1329,13 @@ export interface NavigationGoto extends NavigationBase { type: 'goto'; } +/** + * A navigation triggered by a `pushState(...)` or `replaceState(...)` call with a non-empty URL + */ +export interface NavigationShallow extends NavigationBase { + type: 'shallow'; +} + /** * A navigation triggered by the tab being closed, or the user navigating to a different document */ @@ -1379,7 +1388,8 @@ export type Navigation = | NavigationExternal | NavigationFormSubmit | NavigationPopState - | NavigationLink; + | NavigationLink + | NavigationShallow; /** * The argument passed to [`beforeNavigate`](https://svelte.dev/docs/kit/$app-navigation#beforeNavigate) callbacks. @@ -1453,6 +1463,17 @@ export interface Page< * The page state, which can be manipulated using the [`pushState`](https://svelte.dev/docs/kit/$app-navigation#pushState) and [`replaceState`](https://svelte.dev/docs/kit/$app-navigation#replaceState) functions from `$app/navigation`. */ state: App.PageState; + /** + * Information about the target of the most recent shallow navigation, or `null` if no shallow navigation has occurred. + */ + shallow: { + /** Parameters of the target route, or `null` if the URL does not resolve to a route. */ + params: AppLayoutParams<'/'> | null; + /** Info about the target route, or `null` if the URL does not resolve to a route. */ + route: { id: AppRouteId } | null; + /** The normalized URL passed to `pushState` or `replaceState`. */ + url: ReadonlyURL; + } | null; /** * Filled only after a form submission. See [form actions](https://svelte.dev/docs/kit/form-actions) for more info. */ diff --git a/packages/kit/src/runtime/app/state/client.js b/packages/kit/src/runtime/app/state/client.js index 7abed9d50169..9b76fb9e2d32 100644 --- a/packages/kit/src/runtime/app/state/client.js +++ b/packages/kit/src/runtime/app/state/client.js @@ -20,6 +20,9 @@ export const page = { get route() { return _page.route; }, + get shallow() { + return _page.shallow; + }, get state() { return _page.state; }, diff --git a/packages/kit/src/runtime/app/state/index.js b/packages/kit/src/runtime/app/state/index.js index 70ed0eaab425..bd1153fac6ac 100644 --- a/packages/kit/src/runtime/app/state/index.js +++ b/packages/kit/src/runtime/app/state/index.js @@ -15,7 +15,7 @@ import { BROWSER } from 'esm-env'; * - retrieving the combined `data` of all pages/layouts anywhere in your component tree (also see [loading data](https://svelte.dev/docs/kit/load)) * - retrieving the current value of the `form` prop anywhere in your component tree (also see [form actions](https://svelte.dev/docs/kit/form-actions)) * - retrieving the page state that was set through `goto`, `pushState` or `replaceState` (also see [goto](https://svelte.dev/docs/kit/$app-navigation#goto) and [shallow routing](https://svelte.dev/docs/kit/shallow-routing)) - * - retrieving metadata such as the URL you're on, the current route and its parameters, and whether or not there was an error + * - retrieving metadata such as the URL you're on, the current route and its parameters, the target of a shallow navigation, and whether or not there was an error * * ```svelte * diff --git a/packages/kit/src/runtime/app/state/server.js b/packages/kit/src/runtime/app/state/server.js index 0be169314fc4..5c0e703b692a 100644 --- a/packages/kit/src/runtime/app/state/server.js +++ b/packages/kit/src/runtime/app/state/server.js @@ -33,6 +33,9 @@ export const page = { get route() { return (DEV ? context_dev('page.route') : context()).page.route; }, + get shallow() { + return (DEV ? context_dev('page.shallow') : context()).page.shallow; + }, get state() { return (DEV ? context_dev('page.state') : context()).page.state; }, diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 1edc63853a4c..341e40ba5963 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -5,7 +5,7 @@ /** @import { Query } from './remote-functions/query/instance.svelte.js' */ /** @import { LiveQuery } from './remote-functions/query-live/instance.svelte.js' */ import { BROWSER, DEV } from 'esm-env'; -import * as svelte from 'svelte'; +import { settled, tick, fork, onMount, untrack } from 'svelte'; import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal'; import { decode_pathname, strip_hash, make_trackable, normalize_path } from '../../utils/url.js'; import { dev_fetch, initial_fetch, lock_fetch, subsequent_fetch, unlock_fetch } from './fetcher.js'; @@ -29,6 +29,7 @@ import { PRELOAD_PRIORITIES, SCROLL_KEY, STATES_KEY, + STATES_PERSISTED_KEY, SNAPSHOT_KEY, PAGE_URL_KEY } from './constants.js'; @@ -100,6 +101,8 @@ let current_tree = /** @type {RenderNode} */ ({}); if (DEV && BROWSER) { let warned = false; + const current_module_url = import.meta.url.split('?')[0]; // remove query params that vite adds to the URL when it is loaded from node_modules + const warn = () => { if (warned) return; @@ -112,9 +115,10 @@ if (DEV && BROWSER) { // skip over `warn` and the place where `warn` was called const frame = stack[2]; - // ignore calls that happen inside dependencies, including SvelteKit. + // Ignore calls that happen inside dependencies, including SvelteKit. + // The second condition is only relevant when developing SvelteKit and running it, as there's no node_modules in the stack then (but we still do it to not get repeatedly confused) // `frame` can be falsy if we came from an anonymous function - if (frame?.includes('node_modules')) return; + if (frame?.includes('node_modules') || frame?.includes(current_module_url)) return; warned = true; @@ -415,7 +419,8 @@ export async function start(_app, _target, hydrate) { await navigate({ type: 'enter', url: resolve_url(app.hash ? decode_hash(new URL(location.href)) : location.href), - replace_state: true + replace_state: true, + state: history.state?.[STATES_PERSISTED_KEY] ? (history.state[STATES_KEY] ?? {}) : {} }); restore_scroll(); @@ -464,6 +469,7 @@ async function _invalidate(reset_page_state = true) { } const prev_state = page.state; + const prev_shallow = page.shallow; const navigation_result = intent && (await load_route(intent)); if (!navigation_result || token !== invalidation_token || nav_token !== navigation_token) { return; @@ -488,6 +494,7 @@ async function _invalidate(reset_page_state = true) { if (!reset_page_state) { navigation_result.props.page.state = prev_state; } + navigation_result.props.page.shallow = prev_shallow; update(navigation_result.props.page); current_tree = navigation_result.props.tree; current = { ...navigation_result.state, nav: current.nav }; @@ -603,9 +610,8 @@ export async function _goto(url, options, redirect_count, nav_token, intent) { if (options.refreshAll) { // TODO the ticks shouldn't be necessary, something inside Svelte itself is buggy // when a query in a layout that still exists after page change is refreshed earlier than this - void svelte - .tick() - .then(svelte.tick) + void tick() + .then(tick) .then(() => { for (const [id, entries] of query_map) { for (const [payload, { resource }] of entries) { @@ -647,7 +653,7 @@ async function _preload_data(intent) { load_cache.promise.catch(discard_load_cache); - if (__SVELTEKIT_FORK_PRELOADS__ && svelte.fork) { + if (__SVELTEKIT_FORK_PRELOADS__) { const lc = load_cache; lc.fork = lc.promise.then((result) => { @@ -655,7 +661,7 @@ async function _preload_data(intent) { // resolve, bail rather than creating an orphan fork if (lc === load_cache && result.type === 'loaded') { try { - return svelte.fork(() => { + return fork(() => { root.$set(result.props); update(result.props.page); current_tree = result.props.tree; @@ -882,6 +888,7 @@ async function get_navigation_result_from_branch({ id: route?.id ?? null }, state: {}, + shallow: null, status: status ?? error?.status ?? 200, url: new URL(url), form: form ?? null, @@ -1662,6 +1669,9 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) { if (!is_navigating) { // Don't run the event during redirects + // TODO this isn't fully right: if you do a goto(...) while another goto(...) is in progress, + // or you click a link while a navigation is in progress, the beforNavigate calls are not triggered, + // and maybe they should be? before_navigate_callbacks.forEach((fn) => fn(cancellable)); } @@ -1856,6 +1866,7 @@ async function navigate({ const entry = { [HISTORY_INDEX]: (current_history_index += change), [NAVIGATION_INDEX]: (current_navigation_index += change), + [STATES_PERSISTED_KEY]: true, [STATES_KEY]: state }; @@ -1879,6 +1890,7 @@ async function navigate({ } navigation_result.props.page.state = state; + navigation_result.props.page.shallow = null; /** * @type {Promise | undefined} @@ -1962,7 +1974,7 @@ async function navigate({ } update(navigation_result.props.page); - commit_promise = svelte.settled?.(); + commit_promise = settled(); } has_navigated = true; @@ -1972,7 +1984,7 @@ async function navigate({ const { activeElement } = document; - await (commit_promise ?? svelte.tick()); + await commit_promise; if (navigation_token !== nav_token) { // a new navigation happened while we were waiting for the DOM to update, so abort @@ -2261,7 +2273,7 @@ export async function handle_error(error, event) { * @param {T} callback */ function add_navigation_callback(callbacks, callback) { - svelte.onMount(() => { + onMount(() => { callbacks.add(callback); return () => { @@ -2282,7 +2294,7 @@ export function afterNavigate(callback) { } /** - * A navigation interceptor that triggers before we navigate to a URL, whether by clicking a link, calling `goto(...)`, or using the browser back/forward controls. + * A navigation interceptor that triggers before we navigate to a URL, whether by clicking a link, calling `goto(...)`, `pushState(...)` or `replaceState(...)` with a non-empty URL, or using the browser back/forward controls. * * Calling `cancel()` will prevent the navigation from completing. If `navigation.type === 'leave'` — meaning the user is navigating away from the app (or closing the tab) — calling `cancel` will trigger the native browser unload confirmation dialog. In this case, the navigation may or may not be cancelled depending on the user's response. * @@ -2560,63 +2572,57 @@ export async function preloadCode(pathname) { } /** - * Programmatically create a new history entry with the given `page.state`. To use the current URL, you can pass `''` as the first argument. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing). + * Programmatically create a new history entry with the given `page.state`. To keep the current URL and shallow routing context, pass `''` as the first argument. Otherwise, this triggers the navigation lifecycle hooks with a `navigation.type` of `'shallow'`. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing). * - * @param {string | URL} url + * Passing `null` as the first argument ends shallow routing and reverts the URL to the value of page.url. + * + * @param {string | URL | null} url * @param {App.PageState} state - * @returns {void} + * @param {Object} [options] + * @param {boolean} [options.persist] Whether to persist the state across a full page reload. Defaults to `false`. + * @returns {Promise} */ -export function pushState(url, state) { +export async function pushState(url, state, options = {}) { if (!BROWSER) { throw new Error('Cannot call pushState(...) on the server'); } - if (DEV) { - if (!started) { - throw new Error('Cannot call pushState(...) before router is initialized'); - } - - try { - // use `devalue.stringify` as a convenient way to ensure we exclude values that can't be properly rehydrated, such as custom class instances - devalue.stringify(state); - } catch (error) { - // @ts-expect-error - throw new Error(`Could not serialize state${error.path}`, { cause: error }); - } - } - - update_scroll_positions(current_history_index); - - const opts = { - [HISTORY_INDEX]: (current_history_index += 1), - [NAVIGATION_INDEX]: current_navigation_index, - [PAGE_URL_KEY]: page.url.href, - [STATES_KEY]: state - }; - - history.pushState(opts, '', resolve_url(url)); - has_navigated = true; - - page.state = state; - - clear_onward_history(current_history_index, current_navigation_index); + // Untrack to avoid triggering outer reactive contexts because we access page.X inside + await untrack(() => update_state(url, state, false, options.persist ?? false)); } /** - * Programmatically replace the current history entry with the given `page.state`. To use the current URL, you can pass `''` as the first argument. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing). + * Programmatically replace the current history entry with the given `page.state`. To keep the current URL and shallow routing context, pass `''` as the first argument. Otherwise, this triggers the navigation lifecycle hooks with a `navigation.type` of `'shallow'`. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing). * - * @param {string | URL} url + * Passing `null` as the first argument ends shallow routing and reverts the URL to the value of page.url. + * + * @param {string | URL | null} url * @param {App.PageState} state - * @returns {void} + * @param {Object} [options] + * @param {boolean} [options.persist] Whether to persist the state across a full page reload. Defaults to `false`. + * @returns {Promise} */ -export function replaceState(url, state) { +export async function replaceState(url, state, options = {}) { if (!BROWSER) { throw new Error('Cannot call replaceState(...) on the server'); } + // Untrack to avoid triggering outer reactive contexts because we access page.X inside + await untrack(() => update_state(url, state, true, options.persist ?? false)); +} + +/** + * @param {string | URL | null} url + * @param {App.PageState} state + * @param {boolean} replace + * @param {boolean} persist + */ +async function update_state(url, state, replace, persist) { if (DEV) { if (!started) { - throw new Error('Cannot call replaceState(...) before router is initialized'); + throw new Error( + `Cannot call ${replace ? 'replaceState' : 'pushState'}(...) before router is initialized` + ); } try { @@ -2628,16 +2634,97 @@ export function replaceState(url, state) { } } - const opts = { - [HISTORY_INDEX]: current_history_index, + const resolved = !url ? null : resolve_url(url); + const intent = resolved ? await get_navigation_intent(resolved, false) : undefined; + const nav = resolved ? _before_navigate({ url: resolved, type: 'shallow', intent }) : undefined; + + if (resolved && !nav) return; + + const nav_token = {}; + + if (nav) { + navigation_token = invalidation_token = nav_token; + is_navigating = true; + navigating.current = nav.navigation; + updating = true; + } + + if (!replace) update_scroll_positions(current_history_index); + + const entry = { + [HISTORY_INDEX]: (current_history_index += replace ? 0 : 1), [NAVIGATION_INDEX]: current_navigation_index, - [PAGE_URL_KEY]: page.url.href, + ...((resolved || (url === '' && page.shallow)) && { [PAGE_URL_KEY]: page.url.href }), + [STATES_PERSISTED_KEY]: persist, [STATES_KEY]: state }; - history.replaceState(opts, '', resolve_url(url)); + const fn = replace ? history.replaceState : history.pushState; + if (resolved) { + fn.call(history, entry, '', resolved); + } else { + fn.call(history, entry, '', url == null ? page.url.href : undefined); + } + + if (!replace) { + has_navigated = true; + clear_onward_history(current_history_index, current_navigation_index); + } + + if (nav) { + const after_navigate = ( + await Promise.all( + // eslint-disable-next-line @typescript-eslint/await-thenable -- we need to await because they can be asynchronous + Array.from(on_navigate_callbacks, (fn) => + fn(/** @type {import('@sveltejs/kit').OnNavigate} */ (nav.navigation)) + ) + ) + ).filter(/** @returns {value is () => void} */ (value) => typeof value === 'function'); + + if (after_navigate.length > 0) { + function cleanup() { + after_navigate.forEach((fn) => after_navigate_callbacks.delete(fn)); + } + + after_navigate.push(cleanup); + after_navigate.forEach((fn) => after_navigate_callbacks.add(fn)); + } + } page.state = state; + if (resolved) { + page.shallow = { + params: intent?.params ?? null, + route: intent ? { id: intent.route.id } : null, + url: resolved + }; + } else if (url === null) { + page.shallow = null; + } + + if (!nav) return; + + await settled(); + + if (navigation_token !== nav_token) { + // a new navigation happened while we were waiting for the DOM to update, so abort + nav.reject(new Error('navigation aborted')); + return; + } + + is_navigating = false; + nav.fulfil(undefined); + + if (nav.navigation.to) { + nav.navigation.to.scroll = scroll_state(); + } + + after_navigate_callbacks.forEach((fn) => + fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation)) + ); + + navigating.current = null; + updating = false; } /** @@ -2669,7 +2756,7 @@ export async function applyAction(result) { }); // ...so that setting the `form` prop takes effect and isn't ignored - await svelte.tick(); + await tick(); root.$set({ form: result.data }); if (result.type === 'success') { @@ -2704,7 +2791,7 @@ export async function set_nearest_error_page(error) { current_tree = navigation_result.props.tree; update(navigation_result.props.page); - void svelte.tick().then(() => reset_focus(current.url)); + void tick().then(() => reset_focus(current.url)); } } @@ -2951,6 +3038,16 @@ function _start_router() { page.state = state; } + const shallow_url = event.state[PAGE_URL_KEY] ? new URL(location.href) : null; + const intent = shallow_url ? await get_navigation_intent(shallow_url, false) : undefined; + page.shallow = shallow_url + ? { + params: intent?.params ?? null, + route: intent ? { id: intent.route.id } : null, + url: shallow_url + } + : null; + update_url(url); scroll_positions[current_history_index] = scroll_state(); @@ -3151,7 +3248,9 @@ async function _hydrate( if (!result) return; if (result.props.page) { - result.props.page.state = {}; + result.props.page.state = history.state?.[STATES_PERSISTED_KEY] + ? (history.state[STATES_KEY] ?? {}) + : {}; } await initialize(result, target, hydrate); @@ -3414,7 +3513,7 @@ function create_navigation(current, intent, url, type, target_scroll = null) { url, scroll: target_scroll }, - willUnload: !intent, + willUnload: type !== 'shallow' && !intent, type, complete }); diff --git a/packages/kit/src/runtime/client/constants.js b/packages/kit/src/runtime/client/constants.js index ef529228245e..dc8e87e2523a 100644 --- a/packages/kit/src/runtime/client/constants.js +++ b/packages/kit/src/runtime/client/constants.js @@ -1,6 +1,7 @@ export const SNAPSHOT_KEY = 'sveltekit:snapshot'; export const SCROLL_KEY = 'sveltekit:scroll'; export const STATES_KEY = 'sveltekit:states'; +export const STATES_PERSISTED_KEY = 'sveltekit:states-persisted'; export const PAGE_URL_KEY = 'sveltekit:pageurl'; export const HISTORY_INDEX = 'sveltekit:history'; diff --git a/packages/kit/src/runtime/client/state.svelte.js b/packages/kit/src/runtime/client/state.svelte.js index f29bc9d3728c..8fc8de0a8de4 100644 --- a/packages/kit/src/runtime/client/state.svelte.js +++ b/packages/kit/src/runtime/client/state.svelte.js @@ -9,6 +9,7 @@ export const page = new (class Page { error = $state.raw(null); params = $state.raw({}); route = $state.raw({ id: null }); + shallow = $state.raw(null); state = $state.raw({}); status = $state.raw(-1); url = $state.raw(new URL('a:')); diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index ef411446d0c0..58d49f75f5d4 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -153,6 +153,7 @@ export async function render_response({ url: event.url, data: {}, form: form_value, + shallow: null, state: {} } }; diff --git a/packages/kit/src/types/ambient.d.ts b/packages/kit/src/types/ambient.d.ts index d01f3b4a426d..94e650a288ea 100644 --- a/packages/kit/src/types/ambient.d.ts +++ b/packages/kit/src/types/ambient.d.ts @@ -44,7 +44,7 @@ declare namespace App { export interface PageData {} /** - * The shape of the `page.state` object, which can be manipulated using the [`pushState`](https://svelte.dev/docs/kit/$app-navigation#pushState) and [`replaceState`](https://svelte.dev/docs/kit/$app-navigation#replaceState) functions from `$app/navigation`. + * The shape of the `page.state` object, which can be manipulated using [`goto`](https://svelte.dev/docs/kit/$app-navigation#goto), [`pushState`](https://svelte.dev/docs/kit/$app-navigation#pushState) and [`replaceState`](https://svelte.dev/docs/kit/$app-navigation#replaceState). */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface PageState {} diff --git a/packages/kit/test/apps/basics/src/app.d.ts b/packages/kit/test/apps/basics/src/app.d.ts index e3198c7bd1c7..8e3063590293 100644 --- a/packages/kit/test/apps/basics/src/app.d.ts +++ b/packages/kit/test/apps/basics/src/app.d.ts @@ -14,6 +14,17 @@ declare global { count?: number; } } + + interface Window { + shallow_navigation_log: Array<{ + hook: string; + params?: Record | null; + path?: string; + route?: string | null; + state?: string | null; + type?: string; + }>; + } } export {}; diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+layout.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+layout.svelte index 28de28c4ab08..7ff9e395d65a 100644 --- a/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+layout.svelte +++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+layout.svelte @@ -1,3 +1,48 @@ + + push-state a b diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.svelte index 07c88d358be5..46e15ef155ad 100644 --- a/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.svelte @@ -1,15 +1,22 @@ @@ -17,7 +24,31 @@ + + + + + + +

active: {page.state.active ?? false}

-{data.now} + + {page.shallow + ? `${page.shallow.url.pathname} ${page.shallow.route?.id ?? 'null'} ${JSON.stringify(page.shallow.params)}` + : 'null'} + +{resolved} +{data.now} diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/[param]/+page.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/[param]/+page.svelte new file mode 100644 index 000000000000..f4630b6bb5b0 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/[param]/+page.svelte @@ -0,0 +1 @@ +

parameterized child

diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/a/+page.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/a/+page.svelte index 4a184133f3ea..330c86719cea 100644 --- a/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/a/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/a/+page.svelte @@ -5,3 +5,4 @@

a

active: {page.state.active ?? false}

+{page.shallow ? page.shallow.url.pathname : 'null'} diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/+layout.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/+layout.svelte index 7e6089f7721f..a86a603bb52c 100644 --- a/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/+layout.svelte +++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/+layout.svelte @@ -1,3 +1,30 @@ + + replace-state a b diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/+page.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/+page.svelte index a27536df1a55..2bd176f95e6b 100644 --- a/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/+page.svelte @@ -2,12 +2,12 @@ import { replaceState } from '$app/navigation'; import { page } from '$app/state'; - function one() { - replaceState('', { active: true }); + async function one() { + await replaceState('', { active: true }); } - function two() { - replaceState('/shallow-routing/replace-state/a', { active: true }); + async function two() { + await replaceState('/shallow-routing/replace-state/a', { active: true }); } @@ -15,5 +15,12 @@ + +

active: {page.state.active ?? false}

+{page.shallow ? page.shallow.url.pathname : 'null'} diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/effect/+page.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/effect/+page.svelte index cf298aac8150..dda44dd05f2c 100644 --- a/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/effect/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/effect/+page.svelte @@ -1,5 +1,5 @@