;
};
diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js
index 05e59b21bc8a..d205f48cdc42 100644
--- a/packages/kit/src/runtime/client/utils.js
+++ b/packages/kit/src/runtime/client/utils.js
@@ -149,7 +149,7 @@ export function get_link_info(a, base) {
*/
export function get_router_options(element) {
/** @type {ValidLinkOptions<'keepfocus'> | null} */
- let keep_focus = null;
+ let keepfocus = null;
/** @type {ValidLinkOptions<'noscroll'> | null} */
let noscroll = null;
@@ -172,7 +172,7 @@ export function get_router_options(element) {
while (el && el !== document.documentElement) {
if (preload_code === null) preload_code = link_option(el, 'preload-code');
if (preload_data === null) preload_data = link_option(el, 'preload-data');
- if (keep_focus === null) keep_focus = link_option(el, 'keepfocus');
+ if (keepfocus === null) keepfocus = link_option(el, 'keepfocus');
if (noscroll === null) noscroll = link_option(el, 'noscroll');
if (reload === null) reload = link_option(el, 'reload');
if (replace_state === null) replace_state = link_option(el, 'replacestate');
@@ -190,14 +190,14 @@ export function get_router_options(element) {
case 'false':
return false;
default:
- return null;
+ return undefined;
}
}
return {
preload_code: levels[preload_code ?? 'off'],
preload_data: levels[preload_data ?? 'off'],
- keep_focus: get_option_state(keep_focus),
+ keepfocus: get_option_state(keepfocus),
noscroll: get_option_state(noscroll),
reload: get_option_state(reload),
replace_state: get_option_state(replace_state)
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index 3b673648135f..fbe8f9d35548 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -141,7 +141,8 @@ export async function render_response({
status,
url: event.url,
data,
- form: form_value
+ form: form_value,
+ state: {}
};
// use relative paths during rendering, so that the resulting HTML is as
diff --git a/packages/kit/src/types/ambient.d.ts b/packages/kit/src/types/ambient.d.ts
index c59ed2d3700a..fa32a8a0662b 100644
--- a/packages/kit/src/types/ambient.d.ts
+++ b/packages/kit/src/types/ambient.d.ts
@@ -7,6 +7,7 @@
* // interface Error {}
* // interface Locals {}
* // interface PageData {}
+ * // interface PageState {}
* // interface Platform {}
* }
* }
@@ -39,6 +40,11 @@ declare namespace App {
*/
export interface PageData {}
+ /**
+ * The shape of the `$page.state` object, which can be manipulated using the [`pushState`](https://kit.svelte.dev/docs/modules#$app-navigation-pushstate) and [`replaceState`](https://kit.svelte.dev/docs/modules#$app-navigation-replacestate) functions from `$app/navigation`.
+ */
+ export interface PageState {}
+
/**
* If your adapter provides [platform-specific context](https://kit.svelte.dev/docs/adapters#platform-specific-context) via `event.platform`, you can specify it here.
*/
diff --git a/packages/kit/src/utils/url.js b/packages/kit/src/utils/url.js
index ebcd3702ea86..9f6391e42486 100644
--- a/packages/kit/src/utils/url.js
+++ b/packages/kit/src/utils/url.js
@@ -77,6 +77,14 @@ export function decode_uri(uri) {
}
}
+/**
+ * Returns everything up to the first `#` in a URL
+ * @param {{href: string}} url_like
+ */
+export function strip_hash({ href }) {
+ return href.split('#')[0];
+}
+
/**
* URL properties that could change during the lifetime of the page,
* which excludes things like `origin`
diff --git a/packages/kit/test/apps/basics/src/app.d.ts b/packages/kit/test/apps/basics/src/app.d.ts
index adba879735d9..16bdf501b907 100644
--- a/packages/kit/test/apps/basics/src/app.d.ts
+++ b/packages/kit/test/apps/basics/src/app.d.ts
@@ -8,6 +8,10 @@ declare global {
url?: URL;
}
+ interface PageState {
+ active: boolean;
+ }
+
interface Platform {}
}
}
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
new file mode 100644
index 000000000000..28de28c4ab08
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+layout.svelte
@@ -0,0 +1,5 @@
+push-state
+a
+b
+
+
diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.js b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.js
new file mode 100644
index 000000000000..e05ece04e7cd
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.js
@@ -0,0 +1,5 @@
+export function load() {
+ return {
+ now: Date.now()
+ };
+}
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
new file mode 100644
index 000000000000..40c616c3b2a5
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.svelte
@@ -0,0 +1,23 @@
+
+
+parent
+
+
+
+
+
+active: {$page.state.active ?? false}
+{data.now}
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
new file mode 100644
index 000000000000..3be7397f2aca
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/a/+page.svelte
@@ -0,0 +1,7 @@
+
+
+a
+
+active: {$page.state.active ?? false}
diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/b/+page.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/b/+page.svelte
new file mode 100644
index 000000000000..30c06a44b409
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/b/+page.svelte
@@ -0,0 +1,7 @@
+
+
+b
+
+active: {$page.state.active ?? false}
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
new file mode 100644
index 000000000000..7e6089f7721f
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/+layout.svelte
@@ -0,0 +1,5 @@
+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
new file mode 100644
index 000000000000..a19674234665
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/+page.svelte
@@ -0,0 +1,19 @@
+
+
+parent
+
+
+
+
+active: {$page.state.active ?? false}
diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/a/+page.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/a/+page.svelte
new file mode 100644
index 000000000000..3be7397f2aca
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/a/+page.svelte
@@ -0,0 +1,7 @@
+
+
+a
+
+active: {$page.state.active ?? false}
diff --git a/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/b/+page.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/b/+page.svelte
new file mode 100644
index 000000000000..30c06a44b409
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/b/+page.svelte
@@ -0,0 +1,7 @@
+
+
+b
+
+active: {$page.state.active ?? false}
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index a201b9cea9d8..7ebc90b310ae 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -913,3 +913,91 @@ test.describe('goto', () => {
await expect(page.locator('p')).toHaveText(message);
});
});
+
+test.describe('Shallow routing', () => {
+ test('Pushes state to the current URL', async ({ page }) => {
+ await page.goto('/shallow-routing/push-state');
+ await expect(page.locator('p')).toHaveText('active: false');
+
+ await page.locator('[data-id="one"]').click();
+ await expect(page.locator('p')).toHaveText('active: true');
+
+ await page.goBack();
+ await expect(page.locator('p')).toHaveText('active: false');
+ });
+
+ test('Pushes state to a new URL', async ({ baseURL, page }) => {
+ await page.goto('/shallow-routing/push-state');
+ await expect(page.locator('p')).toHaveText('active: false');
+
+ await page.locator('[data-id="two"]').click();
+ expect(page.url()).toBe(`${baseURL}/shallow-routing/push-state/a`);
+ await expect(page.locator('h1')).toHaveText('parent');
+ await expect(page.locator('p')).toHaveText('active: true');
+
+ await page.reload();
+ await expect(page.locator('h1')).toHaveText('a');
+ await expect(page.locator('p')).toHaveText('active: false');
+
+ await page.goBack();
+ expect(page.url()).toBe(`${baseURL}/shallow-routing/push-state`);
+ await expect(page.locator('h1')).toHaveText('parent');
+ await expect(page.locator('p')).toHaveText('active: false');
+
+ await page.goForward();
+ expect(page.url()).toBe(`${baseURL}/shallow-routing/push-state/a`);
+ await expect(page.locator('h1')).toHaveText('parent');
+ await expect(page.locator('p')).toHaveText('active: true');
+ });
+
+ test('Invalidates the correct route after pushing state to a new URL', async ({
+ baseURL,
+ page
+ }) => {
+ await page.goto('/shallow-routing/push-state');
+ await expect(page.locator('p')).toHaveText('active: false');
+
+ const now = await page.locator('span').textContent();
+
+ await page.locator('[data-id="two"]').click();
+ expect(page.url()).toBe(`${baseURL}/shallow-routing/push-state/a`);
+
+ await page.locator('[data-id="invalidate"]').click();
+ await expect(page.locator('h1')).toHaveText('parent');
+ await expect(page.locator('span')).not.toHaveText(now);
+ });
+
+ test('Replaces state on the current URL', async ({ baseURL, page, clicknav }) => {
+ await page.goto('/shallow-routing/replace-state/b');
+ await clicknav('[href="/shallow-routing/replace-state"]');
+
+ await page.locator('[data-id="one"]').click();
+ await expect(page.locator('p')).toHaveText('active: true');
+
+ await page.goBack();
+ expect(page.url()).toBe(`${baseURL}/shallow-routing/replace-state/b`);
+ await expect(page.locator('h1')).toHaveText('b');
+
+ await page.goForward();
+ expect(page.url()).toBe(`${baseURL}/shallow-routing/replace-state`);
+ await expect(page.locator('h1')).toHaveText('parent');
+ await expect(page.locator('p')).toHaveText('active: true');
+ });
+
+ test('Replaces state on a new URL', async ({ baseURL, page, clicknav }) => {
+ await page.goto('/shallow-routing/replace-state/b');
+ await clicknav('[href="/shallow-routing/replace-state"]');
+
+ await page.locator('[data-id="two"]').click();
+ await expect(page.locator('p')).toHaveText('active: true');
+
+ await page.goBack();
+ expect(page.url()).toBe(`${baseURL}/shallow-routing/replace-state/b`);
+ await expect(page.locator('h1')).toHaveText('b');
+
+ await page.goForward();
+ expect(page.url()).toBe(`${baseURL}/shallow-routing/replace-state/a`);
+ await expect(page.locator('h1')).toHaveText('parent');
+ await expect(page.locator('p')).toHaveText('active: true');
+ });
+});
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index 59c18f3cf923..07840d091f5c 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -943,6 +943,10 @@ declare module '@sveltejs/kit' {
* The merged result of all data from all `load` functions on the current page. You can type a common denominator through `App.PageData`.
*/
data: App.PageData & Record;
+ /**
+ * The page state, which can be manipulated using the [`pushState`](https://kit.svelte.dev/docs/modules#$app-navigation-pushstate) and [`replaceState`](https://kit.svelte.dev/docs/modules#$app-navigation-replacestate) functions from `$app/navigation`.
+ */
+ state: App.PageState;
/**
* Filled only after a form submission. See [form actions](https://kit.svelte.dev/docs/form-actions) for more info.
*/
@@ -1927,7 +1931,6 @@ declare module '$app/navigation' {
noScroll?: boolean;
keepFocus?: boolean;
invalidateAll?: boolean;
- state?: any;
}) => Promise;
/**
* Causes any `load` functions belonging to the currently active page to re-run if they depend on the `url` in question, via `fetch` or `depends`. Returns a `Promise` that resolves when the page is subsequently updated.
@@ -1958,11 +1961,11 @@ declare module '$app/navigation' {
*
* This is the same behaviour that SvelteKit triggers when the user taps or mouses over an `` element with `data-sveltekit-preload-data`.
* If the next navigation is to `href`, the values returned from load will be used, making navigation instantaneous.
- * Returns a Promise that resolves when the preload is complete.
+ * Returns a Promise that resolves with the result of running the new route's `load` functions once the preload is complete.
*
* @param href Page to preload
* */
- export const preloadData: (href: string) => Promise;
+ export const preloadData: (href: string) => Promise>;
/**
* Programmatically imports the code for routes that haven't yet been fetched.
* Typically, you might call this to speed up subsequent navigation.
@@ -2002,6 +2005,16 @@ declare module '$app/navigation' {
* `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
* */
export const afterNavigate: (callback: (navigation: import('@sveltejs/kit').AfterNavigate) => void) => void;
+ /**
+ * 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://kit.svelte.dev/docs/shallow-routing).
+ *
+ * */
+ export const pushState: (url: string | URL, state: App.PageState) => void;
+ /**
+ * 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://kit.svelte.dev/docs/shallow-routing).
+ *
+ * */
+ export const replaceState: (url: string | URL, state: App.PageState) => void;
type MaybePromise = T | Promise;
}
@@ -2064,6 +2077,7 @@ declare module '$app/stores' {
* // interface Error {}
* // interface Locals {}
* // interface PageData {}
+ * // interface PageState {}
* // interface Platform {}
* }
* }
@@ -2096,6 +2110,11 @@ declare namespace App {
*/
export interface PageData {}
+ /**
+ * The shape of the `$page.state` object, which can be manipulated using the [`pushState`](https://kit.svelte.dev/docs/modules#$app-navigation-pushstate) and [`replaceState`](https://kit.svelte.dev/docs/modules#$app-navigation-replacestate) functions from `$app/navigation`.
+ */
+ export interface PageState {}
+
/**
* If your adapter provides [platform-specific context](https://kit.svelte.dev/docs/adapters#platform-specific-context) via `event.platform`, you can specify it here.
*/