From 5a3ed3f5296bc85d0f721555d547d44e61bc0b6a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 20:20:00 -0500 Subject: [PATCH 01/31] shallow routing --- packages/kit/src/runtime/app/navigation.js | 4 + packages/kit/src/runtime/client/client.js | 231 ++++++++++++++---- packages/kit/src/runtime/client/constants.js | 6 +- .../kit/src/runtime/client/session-storage.js | 12 +- packages/kit/src/runtime/client/types.d.ts | 10 +- packages/kit/src/runtime/client/utils.js | 8 +- .../kit/src/runtime/server/page/render.js | 3 +- 7 files changed, 221 insertions(+), 53 deletions(-) diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index fcd2a2507e42..841bd35887a0 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -126,3 +126,7 @@ export const onNavigate = /* @__PURE__ */ client_method('on_navigate'); * @returns {void} */ export const afterNavigate = /* @__PURE__ */ client_method('after_navigate'); + +export const pushState = /* @__PURE__ */ client_method('push_state'); + +export const replaceState = /* @__PURE__ */ client_method('replace_state'); diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 39b326dbc17d..e48d1768ca9f 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -28,26 +28,46 @@ import { import { base } from '__sveltekit/paths'; import * as devalue from 'devalue'; -import { compact } from '../../utils/array.js'; +import { + HISTORY_INDEX, + NAVIGATION_INDEX, + PRELOAD_PRIORITIES, + SCROLL_KEY, + STATES_KEY, + SNAPSHOT_KEY, + PAGE_URL_KEY +} from './constants.js'; import { validate_page_exports } from '../../utils/exports.js'; +import { compact } from '../../utils/array.js'; import { HttpError, Redirect, SvelteKitError } from '../control.js'; import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM, validate_depends } from '../shared.js'; -import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY, SNAPSHOT_KEY } from './constants.js'; import { stores } from './singletons.js'; import { get_message, get_status } from '../../utils/error.js'; let errored = false; +/** @typedef {{ x: number, y: number }} ScrollPosition */ + // We track the scroll position associated with each history entry in sessionStorage, // rather than on history.state itself, because when navigation is driven by // popstate it's too late to update the scroll position associated with the // state we're navigating from - -/** @typedef {{ x: number, y: number }} ScrollPosition */ -/** @type {Record} */ +/** + * history index -> { x, y } + * @type {Record} + */ const scroll_positions = storage.get(SCROLL_KEY) ?? {}; -/** @type {Record} */ +/** + * history index -> any + * @type {Record>} + */ +const states = storage.get(STATES_KEY, devalue.parse) ?? {}; + +/** + * navigation index -> any + * @type {Record} + */ const snapshots = storage.get(SNAPSHOT_KEY) ?? {}; /** @param {number} index */ @@ -56,6 +76,27 @@ function update_scroll_positions(index) { } /** + * @param {number} current_history_index + * @param {number} current_navigation_index + */ +function clear_onward_history(current_history_index, current_navigation_index) { + // if we navigated back, then pushed a new state, we can + // release memory by pruning the scroll/snapshot lookup + let i = current_history_index + 1; + while (scroll_positions[i]) { + delete scroll_positions[i]; + i += 1; + } + + i = current_navigation_index + 1; + while (snapshots[i]) { + delete snapshots[i]; + i += 1; + } +} + +/** + * @param {import('./types.js').SvelteKitApp} app * Loads `href` the old-fashioned way, with a full page reload. * Returns a `Promise` that never resolves (to prevent any * subsequent work, e.g. history manipulation, from happening) @@ -131,16 +172,26 @@ export function create_client(app, target) { let root; // keeping track of the history index in order to prevent popstate navigation events if needed - let current_history_index = history.state?.[INDEX_KEY]; + /** @type {number} */ + let current_history_index = history.state?.[HISTORY_INDEX]; + + /** @type {number} */ + let current_navigation_index = history.state?.[NAVIGATION_INDEX]; + + let has_navigated = false; if (!current_history_index) { // we use Date.now() as an offset so that cross-document navigations // within the app don't result in data loss - current_history_index = Date.now(); + current_history_index = current_navigation_index = Date.now(); // create initial history entry, so we can return here history.replaceState( - { ...history.state, [INDEX_KEY]: current_history_index }, + { + ...history.state, + [HISTORY_INDEX]: current_history_index, + [NAVIGATION_INDEX]: current_navigation_index + }, '', location.href ); @@ -216,8 +267,10 @@ export function create_client(app, target) { update_scroll_positions(current_history_index); storage.set(SCROLL_KEY, scroll_positions); - capture_snapshot(current_history_index); + capture_snapshot(current_navigation_index); storage.set(SNAPSHOT_KEY, snapshots); + + storage.set(STATES_KEY, states, devalue.stringify); } /** @@ -241,6 +294,7 @@ export function create_client(app, target) { return navigate({ url: resolve_url(url), scroll: noScroll ? scroll_state() : null, + state, keepfocus: keepFocus, redirect_count, details: { @@ -300,7 +354,7 @@ export function create_client(app, target) { hydrate: true }); - restore_snapshot(current_history_index); + restore_snapshot(current_navigation_index); /** @type {import('@sveltejs/kit').AfterNavigate} */ const navigation = { @@ -361,7 +415,8 @@ export function create_client(app, target) { }, props: { // @ts-ignore Somehow it's getting SvelteComponent and SvelteComponentDev mixed up - constructors: compact(branch).map((branch_node) => branch_node.node.component) + constructors: compact(branch).map((branch_node) => branch_node.node.component), + page } }; @@ -405,6 +460,7 @@ export function create_client(app, target) { route: { id: route?.id ?? null }, + state: {}, status, url: new URL(url), form: form ?? null, @@ -1006,6 +1062,7 @@ export function create_client(app, target) { * @param {{ * url: URL; * scroll: { x: number, y: number } | null; + * state: Record; * keepfocus: boolean; * redirect_count: number; * details: { @@ -1022,6 +1079,7 @@ export function create_client(app, target) { async function navigate({ url, scroll, + state, keepfocus, redirect_count, details, @@ -1041,6 +1099,7 @@ export function create_client(app, target) { // store this before calling `accepted()`, which may change the index const previous_history_index = current_history_index; + const previous_navigation_index = current_navigation_index; accepted(); @@ -1096,7 +1155,7 @@ export function create_client(app, target) { goto(new URL(navigation_result.location, url).href, {}, redirect_count + 1, nav_token); return false; } - } else if (/** @type {number} */ (navigation_result.props.page?.status) >= 400) { + } else if (/** @type {number} */ (navigation_result.props.page.status) >= 400) { const updated = await stores.updated.check(); if (updated) { await native_navigation(url); @@ -1111,36 +1170,31 @@ export function create_client(app, target) { updating = true; update_scroll_positions(previous_history_index); - capture_snapshot(previous_history_index); + capture_snapshot(previous_navigation_index); // ensure the url pathname matches the page's trailing slash option - if ( - navigation_result.props.page?.url && - navigation_result.props.page.url.pathname !== url.pathname - ) { + if (navigation_result.props.page.url.pathname !== url.pathname) { url.pathname = navigation_result.props.page?.url.pathname; } if (details) { const change = details.replaceState ? 0 : 1; - details.state[INDEX_KEY] = current_history_index += change; + details.state[HISTORY_INDEX] = current_history_index += change; + details.state[NAVIGATION_INDEX] = current_navigation_index += change; history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url); if (!details.replaceState) { - // if we navigated back, then pushed a new state, we can - // release memory by pruning the scroll/snapshot lookup - let i = current_history_index + 1; - while (snapshots[i] || scroll_positions[i]) { - delete snapshots[i]; - delete scroll_positions[i]; - i += 1; - } + clear_onward_history(current_history_index, current_navigation_index); } } + states[current_history_index] = state; + // reset preload synchronously after the history state has been set to avoid race conditions load_cache = null; + navigation_result.props.page.state = state; + if (started) { current = navigation_result.state; @@ -1217,7 +1271,7 @@ export function create_client(app, target) { navigating = false; if (type === 'popstate') { - restore_snapshot(current_history_index); + restore_snapshot(current_navigation_index); } nav.fulfil(undefined); @@ -1228,6 +1282,7 @@ export function create_client(app, target) { stores.navigating.set(null); updating = false; + has_navigated = true; } /** @@ -1469,7 +1524,16 @@ export function create_client(app, target) { throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`); } - await preload_data(intent); + const result = await preload_data(intent); + if (result.type === 'redirect') { + return { + type: result.type, + location: result.location + }; + } + + const { status, data } = result.props.page ?? page; + return { type: result.type, status, data }; }, preload_code: (pathname) => { @@ -1488,6 +1552,59 @@ export function create_client(app, target) { return preload_code(pathname); }, + push_state: (state, url = current.url) => { + if (DEV) { + try { + devalue.stringify(state); + } catch (error) { + // @ts-expect-error + throw new Error(`Could not serialize state${error.path}`); + } + } + + history.pushState( + { + [HISTORY_INDEX]: (current_history_index += 1), + [NAVIGATION_INDEX]: current_navigation_index, + [PAGE_URL_KEY]: page.url.href + }, + '', + new URL(url).href + ); + + page = { ...page, state }; + root.$set({ page }); + + states[current_history_index] = state; + clear_onward_history(current_history_index, current_navigation_index); + }, + + replace_state: (state, url = current.url) => { + if (DEV) { + try { + devalue.stringify(state); + } catch (error) { + // @ts-expect-error + throw new Error(`Could not serialize state${error.path}`); + } + } + + history.replaceState( + { + [HISTORY_INDEX]: current_history_index, + [NAVIGATION_INDEX]: current_navigation_index, + [PAGE_URL_KEY]: page.url.href + }, + '', + new URL(url).href + ); + + page = { ...page, state }; + root.$set({ page }); + + states[current_history_index] = state; + }, + apply_action: async (result) => { if (result.type === 'error') { const url = new URL(location.href); @@ -1671,6 +1788,7 @@ export function create_client(app, target) { navigate({ url, scroll: options.noscroll ? scroll_state() : null, + state: {}, keepfocus: options.keep_focus ?? false, redirect_count: 0, details: { @@ -1725,6 +1843,7 @@ export function create_client(app, target) { navigate({ url, scroll: noscroll ? scroll_state() : null, + state: {}, keepfocus: keep_focus ?? false, redirect_count: 0, details: { @@ -1739,35 +1858,55 @@ export function create_client(app, target) { }); addEventListener('popstate', async (event) => { - token = {}; - if (event.state?.[INDEX_KEY]) { + if (event.state?.[HISTORY_INDEX]) { + const history_index = event.state[HISTORY_INDEX]; + token = {}; + // if a popstate-driven navigation is cancelled, we need to counteract it // with history.go, which means we end up back here, hence this check - if (event.state[INDEX_KEY] === current_history_index) return; + if (history_index === current_history_index) return; + + const scroll = scroll_positions[history_index]; + const state = states[history_index] ?? {}; + const url = new URL(event.state[PAGE_URL_KEY] ?? location.href); - const scroll = scroll_positions[event.state[INDEX_KEY]]; - const url = new URL(location.href); + const navigation_index = event.state[NAVIGATION_INDEX]; - // if the only change is the hash, we don't need to do anything (see https://github.com/sveltejs/kit/pull/10636 for why we need to do `url?.`)... - if (current.url?.href.split('#')[0] === location.href.split('#')[0]) { - // ...except update our internal URL tracking and handle scroll + const is_hash_change = location.href.split('#')[0] === current.url.href.split('#')[0]; + const shallow = + navigation_index === current_navigation_index && (has_navigated || is_hash_change); + + if (shallow) { + // We don't need to navigate, we just need to update scroll and/or state. + // This happens with hash links and `pushState`/`replaceState`. The + // exception is if we haven't navigated yet, since we could have + // got here after a modal navigation then a reload update_url(url); + scroll_positions[current_history_index] = scroll_state(); - current_history_index = event.state[INDEX_KEY]; - scrollTo(scroll.x, scroll.y); + if (scroll) scrollTo(scroll.x, scroll.y); + + if (state !== page.state) { + page = { ...page, state }; + root.$set({ page }); + } + + current_history_index = history_index; return; } - const delta = event.state[INDEX_KEY] - current_history_index; + const delta = history_index - current_history_index; await navigate({ url, scroll, + state, keepfocus: false, redirect_count: 0, details: null, accepted: () => { - current_history_index = event.state[INDEX_KEY]; + current_history_index = history_index; + current_navigation_index = navigation_index; }, blocked: () => { history.go(-delta); @@ -1793,7 +1932,11 @@ export function create_client(app, target) { if (hash_navigating) { hash_navigating = false; history.replaceState( - { ...history.state, [INDEX_KEY]: ++current_history_index }, + { + ...history.state, + [HISTORY_INDEX]: ++current_history_index, + [NAVIGATION_INDEX]: current_navigation_index + }, '', location.href ); @@ -1914,6 +2057,10 @@ export function create_client(app, target) { }); } + if (result.props.page) { + result.props.page.state = {}; + } + initialize(result); } }; diff --git a/packages/kit/src/runtime/client/constants.js b/packages/kit/src/runtime/client/constants.js index e6b515332801..982c25d7debe 100644 --- a/packages/kit/src/runtime/client/constants.js +++ b/packages/kit/src/runtime/client/constants.js @@ -1,6 +1,10 @@ export const SNAPSHOT_KEY = 'sveltekit:snapshot'; export const SCROLL_KEY = 'sveltekit:scroll'; -export const INDEX_KEY = 'sveltekit:index'; +export const STATES_KEY = 'sveltekit:states'; +export const PAGE_URL_KEY = 'sveltekit:pageurl'; + +export const HISTORY_INDEX = 'sveltekit:history'; +export const NAVIGATION_INDEX = 'sveltekit:navigation'; export const PRELOAD_PRIORITIES = /** @type {const} */ ({ tap: 1, diff --git a/packages/kit/src/runtime/client/session-storage.js b/packages/kit/src/runtime/client/session-storage.js index dc2639ef1b5e..e49543bc8b82 100644 --- a/packages/kit/src/runtime/client/session-storage.js +++ b/packages/kit/src/runtime/client/session-storage.js @@ -1,10 +1,11 @@ /** * Read a value from `sessionStorage` * @param {string} key + * @param {(value: string) => any} parse */ -export function get(key) { +export function get(key, parse = JSON.parse) { try { - return JSON.parse(sessionStorage[key]); + return parse(sessionStorage[key]); } catch { // do nothing } @@ -14,11 +15,12 @@ export function get(key) { * Write a value to `sessionStorage` * @param {string} key * @param {any} value + * @param {(value: any) => string} stringify */ -export function set(key, value) { - const json = JSON.stringify(value); +export function set(key, value, stringify = JSON.stringify) { + const data = stringify(value); try { - sessionStorage[key] = json; + sessionStorage[key] = data; } catch { // do nothing } diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index 6466bd916447..2d6937e79eba 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -7,7 +7,9 @@ import { invalidate, invalidateAll, preloadCode, - preloadData + preloadData, + pushState, + replaceState } from '../app/navigation.js'; import { SvelteComponent } from 'svelte'; import { ClientHooks, CSRPageNode, CSRPageNodeLoader, CSRRoute, TrailingSlash, Uses } from 'types'; @@ -51,6 +53,8 @@ export interface Client { invalidate_all: typeof invalidateAll; preload_code: typeof preloadCode; preload_data: typeof preloadData; + push_state: typeof pushState; + replace_state: typeof replaceState; apply_action: typeof applyAction; // private API @@ -91,8 +95,8 @@ export type NavigationFinished = { state: NavigationState; props: { constructors: Array; - components?: Array; - page?: Page; + components: Array; + page: Page; form?: Record | null; [key: `data_${number}`]: Record; }; diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js index 05e59b21bc8a..8432ac181ddd 100644 --- a/packages/kit/src/runtime/client/utils.js +++ b/packages/kit/src/runtime/client/utils.js @@ -220,6 +220,12 @@ export function notifiable_store(value) { store.set(new_value); } + /** @param {(value: any) => any} fn */ + function update(fn) { + ready = false; + store.update(fn); + } + /** @param {(value: any) => void} run */ function subscribe(run) { /** @type {any} */ @@ -231,7 +237,7 @@ export function notifiable_store(value) { }); } - return { notify, set, subscribe }; + return { notify, set, update, subscribe }; } export function create_updated_store() { 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 From f93c41cc40ab4cb8dff7e6e5d6d23ed330e25b11 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 20:39:19 -0500 Subject: [PATCH 02/31] add types --- .../templates/default/src/app.d.ts | 1 + .../templates/skeleton/src/app.d.ts | 1 + .../templates/skeletonlib/src/app.d.ts | 1 + packages/kit/src/exports/public.d.ts | 4 ++ packages/kit/src/runtime/app/navigation.js | 20 ++++++- packages/kit/src/runtime/client/singletons.js | 8 ++- packages/kit/src/types/ambient.d.ts | 6 ++ packages/kit/types/index.d.ts | 59 ++++++++----------- 8 files changed, 63 insertions(+), 37 deletions(-) diff --git a/packages/create-svelte/templates/default/src/app.d.ts b/packages/create-svelte/templates/default/src/app.d.ts index f59b884c51ed..743f07b2e50a 100644 --- a/packages/create-svelte/templates/default/src/app.d.ts +++ b/packages/create-svelte/templates/default/src/app.d.ts @@ -5,6 +5,7 @@ declare global { // interface Error {} // interface Locals {} // interface PageData {} + // interface PageState {} // interface Platform {} } } diff --git a/packages/create-svelte/templates/skeleton/src/app.d.ts b/packages/create-svelte/templates/skeleton/src/app.d.ts index f59b884c51ed..743f07b2e50a 100644 --- a/packages/create-svelte/templates/skeleton/src/app.d.ts +++ b/packages/create-svelte/templates/skeleton/src/app.d.ts @@ -5,6 +5,7 @@ declare global { // interface Error {} // interface Locals {} // interface PageData {} + // interface PageState {} // interface Platform {} } } diff --git a/packages/create-svelte/templates/skeletonlib/src/app.d.ts b/packages/create-svelte/templates/skeletonlib/src/app.d.ts index f59b884c51ed..743f07b2e50a 100644 --- a/packages/create-svelte/templates/skeletonlib/src/app.d.ts +++ b/packages/create-svelte/templates/skeletonlib/src/app.d.ts @@ -5,6 +5,7 @@ declare global { // interface Error {} // interface Locals {} // interface PageData {} + // interface PageState {} // interface Platform {} } } diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index f9de82b1c393..ab88d6031ed7 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -961,6 +961,10 @@ export interface Page< * 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` and `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. */ diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index 841bd35887a0..1ff44bc4d049 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -66,9 +66,9 @@ export const invalidateAll = /* @__PURE__ */ client_method('invalidate_all'); * 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. * - * @type {(href: string) => Promise} + * @type {(href: string) => Promise>} * @param {string} href Page to preload - * @returns {Promise} + * @returns {Promise>} */ export const preloadData = /* @__PURE__ */ client_method('preload_data'); @@ -127,6 +127,22 @@ export const onNavigate = /* @__PURE__ */ client_method('on_navigate'); */ export const afterNavigate = /* @__PURE__ */ client_method('after_navigate'); +/** + * Programmatically create a new history entry with the given `$page.state`. + * + * @type {(state: App.PageState, url?: string | URL) => void} + * @param {App.PageState} state + * @param {string | URL} [url] + * @returns {void} + */ export const pushState = /* @__PURE__ */ client_method('push_state'); +/** + * Programmatically replace the current history entry with the given `$page.state`. + * + * @type {(state: App.PageState, url?: string | URL) => void} + * @param {App.PageState} state + * @param {string | URL} [url] + * @returns {void} + */ export const replaceState = /* @__PURE__ */ client_method('replace_state'); diff --git a/packages/kit/src/runtime/client/singletons.js b/packages/kit/src/runtime/client/singletons.js index e6bf5808eef9..aacaa04907ec 100644 --- a/packages/kit/src/runtime/client/singletons.js +++ b/packages/kit/src/runtime/client/singletons.js @@ -21,7 +21,13 @@ export function init(opts) { */ export function client_method(key) { if (!BROWSER) { - if (key === 'before_navigate' || key === 'after_navigate' || key === 'on_navigate') { + if ( + key === 'before_navigate' || + key === 'after_navigate' || + key === 'on_navigate' || + key === 'push_state' || + key === 'replace_state' + ) { // @ts-expect-error doesn't recognize that both keys here return void so expects a async function return () => {}; } else { diff --git a/packages/kit/src/types/ambient.d.ts b/packages/kit/src/types/ambient.d.ts index c59ed2d3700a..73a08a202c02 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` and `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/types/index.d.ts b/packages/kit/types/index.d.ts index 59c18f3cf923..e198017fef79 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` and `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. */ @@ -1662,27 +1666,7 @@ declare module '@sveltejs/kit' { } type ValidatedConfig = RecursiveRequired; - /** - * Throws an error with a HTTP status code and an optional message. - * When called during request handling, this will cause SvelteKit to - * return an error response without invoking `handleError`. - * Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it. - * @param status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599. - * @param body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property. - * @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling. - * @throws {Error} If the provided status is invalid (not between 400 and 599). - */ export function error(status: NumericRange<400, 599>, body: App.Error): never; - /** - * Throws an error with a HTTP status code and an optional message. - * When called during request handling, this will cause SvelteKit to - * return an error response without invoking `handleError`. - * Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it. - * @param status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599. - * @param body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property. - * @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling. - * @throws {Error} If the provided status is invalid (not between 400 and 599). - */ export function error(status: NumericRange<400, 599>, body?: { message: string; } extends App.Error ? App.Error | string | undefined : never): never; @@ -1698,8 +1682,8 @@ declare module '@sveltejs/kit' { * Make sure you're not catching the thrown redirect, which would prevent SvelteKit from handling it. * @param status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). Must be in the range 300-308. * @param location The location to redirect to. - * @throws {Redirect} This error instructs SvelteKit to redirect to the specified location. - * @throws {Error} If the provided status is invalid. + * @throws This error instructs SvelteKit to redirect to the specified location. + * @throws If the provided status is invalid. * */ export function redirect(status: NumericRange<300, 308>, location: string | URL): never; /** @@ -1719,16 +1703,7 @@ declare module '@sveltejs/kit' { * @param init Options such as `status` and `headers` that will be added to the response. A `Content-Length` header will be added automatically. */ export function text(body: string, init?: ResponseInit | undefined): Response; - /** - * Create an `ActionFailure` object. - * @param status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599. - * */ export function fail(status: number): ActionFailure; - /** - * Create an `ActionFailure` object. - * @param status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599. - * @param data Data associated with the failure (e.g. validation errors) - * */ export function fail | undefined = undefined>(status: number, data: T): ActionFailure; export type LessThan = TNumber extends TArray['length'] ? TArray[number] : LessThan; export type NumericRange = Exclude, LessThan>; @@ -1745,7 +1720,7 @@ declare module '@sveltejs/kit' { class Redirect_1 { constructor(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308, location: string); - status: 301 | 302 | 303 | 307 | 308 | 300 | 304 | 305 | 306; + status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308; location: string; } } @@ -1962,7 +1937,7 @@ declare module '$app/navigation' { * * @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. @@ -1995,13 +1970,23 @@ declare module '$app/navigation' { * * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export const onNavigate: (callback: (navigation: import('@sveltejs/kit').OnNavigate) => MaybePromise<(() => void) | void>) => void; + export const onNavigate: (callback: (navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<(() => void) | void>) => void; /** * A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a new URL. * * `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`. + * + * */ + export const pushState: (state: App.PageState, url?: string | URL) => void; + /** + * Programmatically replace the current history entry with the given `$page.state`. + * + * */ + export const replaceState: (state: App.PageState, url?: string | URL) => void; type MaybePromise = T | Promise; } @@ -2064,6 +2049,7 @@ declare module '$app/stores' { * // interface Error {} * // interface Locals {} * // interface PageData {} + * // interface PageState {} * // interface Platform {} * } * } @@ -2096,6 +2082,11 @@ declare namespace App { */ export interface PageData {} + /** + * The shape of the `$page.state` object, which can be manipulated using the `pushState` and `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. */ From cbc33e95f0e09e849c9728fc7b2d59a289441ab1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 20:41:52 -0500 Subject: [PATCH 03/31] drive-by fix - bad merge --- packages/kit/src/runtime/client/client.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index e48d1768ca9f..9c08c63aedc6 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -96,7 +96,6 @@ function clear_onward_history(current_history_index, current_navigation_index) { } /** - * @param {import('./types.js').SvelteKitApp} app * Loads `href` the old-fashioned way, with a full page reload. * Returns a `Promise` that never resolves (to prevent any * subsequent work, e.g. history manipulation, from happening) From 48cec224ffaafb6ff4688015aa598a3c3b570c88 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 20:52:38 -0500 Subject: [PATCH 04/31] warn on use of history.pushState and history.replaceState --- packages/kit/src/runtime/client/client.js | 62 ++++++++++++++++------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 9c08c63aedc6..73d0e712856f 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -70,6 +70,32 @@ const states = storage.get(STATES_KEY, devalue.parse) ?? {}; */ const snapshots = storage.get(SNAPSHOT_KEY) ?? {}; +const original_push_state = history.pushState; +const original_replace_state = history.replaceState; + +if (DEV) { + let warned = false; + + const warn = () => { + if (warned) return; + warned = true; + + console.warn( + "Avoid using `history.pushState(...)` and `history.replaceState(...)` as these will conflict with SvelteKit's router. Use the `pushState` and `replaceState` imports from `$app/navigation` instead." + ); + }; + + history.pushState = (...args) => { + warn(); + return original_push_state.apply(history, args); + }; + + history.replaceState = (...args) => { + warn(); + return original_replace_state.apply(history, args); + }; +} + /** @param {number} index */ function update_scroll_positions(index) { scroll_positions[index] = scroll_state(); @@ -1180,7 +1206,9 @@ export function create_client(app, target) { const change = details.replaceState ? 0 : 1; details.state[HISTORY_INDEX] = current_history_index += change; details.state[NAVIGATION_INDEX] = current_navigation_index += change; - history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url); + + const fn = details.replaceState ? original_replace_state : original_push_state; + fn.call(history, details.state, '', url); if (!details.replaceState) { clear_onward_history(current_history_index, current_navigation_index); @@ -1561,15 +1589,13 @@ export function create_client(app, target) { } } - history.pushState( - { - [HISTORY_INDEX]: (current_history_index += 1), - [NAVIGATION_INDEX]: current_navigation_index, - [PAGE_URL_KEY]: page.url.href - }, - '', - new URL(url).href - ); + const opts = { + [HISTORY_INDEX]: (current_history_index += 1), + [NAVIGATION_INDEX]: current_navigation_index, + [PAGE_URL_KEY]: page.url.href + }; + + original_push_state.call(history, opts, '', new URL(url).href); page = { ...page, state }; root.$set({ page }); @@ -1588,15 +1614,13 @@ export function create_client(app, target) { } } - history.replaceState( - { - [HISTORY_INDEX]: current_history_index, - [NAVIGATION_INDEX]: current_navigation_index, - [PAGE_URL_KEY]: page.url.href - }, - '', - new URL(url).href - ); + const opts = { + [HISTORY_INDEX]: current_history_index, + [NAVIGATION_INDEX]: current_navigation_index, + [PAGE_URL_KEY]: page.url.href + }; + + original_replace_state.call(history, opts, '', new URL(url).href); page = { ...page, state }; root.$set({ page }); From eaa539b3625b8b01ba32c7e503c83c2f56e2ea67 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 21:46:16 -0500 Subject: [PATCH 05/31] regenerate types --- packages/kit/types/index.d.ts | 37 +++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index e198017fef79..81219cf7edc2 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1666,7 +1666,27 @@ declare module '@sveltejs/kit' { } type ValidatedConfig = RecursiveRequired; + /** + * Throws an error with a HTTP status code and an optional message. + * When called during request handling, this will cause SvelteKit to + * return an error response without invoking `handleError`. + * Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it. + * @param status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599. + * @param body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property. + * @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling. + * @throws {Error} If the provided status is invalid (not between 400 and 599). + */ export function error(status: NumericRange<400, 599>, body: App.Error): never; + /** + * Throws an error with a HTTP status code and an optional message. + * When called during request handling, this will cause SvelteKit to + * return an error response without invoking `handleError`. + * Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it. + * @param status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599. + * @param body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property. + * @throws {HttpError} This error instructs SvelteKit to initiate HTTP error handling. + * @throws {Error} If the provided status is invalid (not between 400 and 599). + */ export function error(status: NumericRange<400, 599>, body?: { message: string; } extends App.Error ? App.Error | string | undefined : never): never; @@ -1682,8 +1702,8 @@ declare module '@sveltejs/kit' { * Make sure you're not catching the thrown redirect, which would prevent SvelteKit from handling it. * @param status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). Must be in the range 300-308. * @param location The location to redirect to. - * @throws This error instructs SvelteKit to redirect to the specified location. - * @throws If the provided status is invalid. + * @throws {Redirect} This error instructs SvelteKit to redirect to the specified location. + * @throws {Error} If the provided status is invalid. * */ export function redirect(status: NumericRange<300, 308>, location: string | URL): never; /** @@ -1703,7 +1723,16 @@ declare module '@sveltejs/kit' { * @param init Options such as `status` and `headers` that will be added to the response. A `Content-Length` header will be added automatically. */ export function text(body: string, init?: ResponseInit | undefined): Response; + /** + * Create an `ActionFailure` object. + * @param status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599. + * */ export function fail(status: number): ActionFailure; + /** + * Create an `ActionFailure` object. + * @param status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599. + * @param data Data associated with the failure (e.g. validation errors) + * */ export function fail | undefined = undefined>(status: number, data: T): ActionFailure; export type LessThan = TNumber extends TArray['length'] ? TArray[number] : LessThan; export type NumericRange = Exclude, LessThan>; @@ -1720,7 +1749,7 @@ declare module '@sveltejs/kit' { class Redirect_1 { constructor(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308, location: string); - status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308; + status: 301 | 302 | 303 | 307 | 308 | 300 | 304 | 305 | 306; location: string; } } @@ -1970,7 +1999,7 @@ declare module '$app/navigation' { * * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted. * */ - export const onNavigate: (callback: (navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<(() => void) | void>) => void; + export const onNavigate: (callback: (navigation: import('@sveltejs/kit').OnNavigate) => MaybePromise<(() => void) | void>) => void; /** * A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a new URL. * From efe9f00140a75c19cf5d33ef8ed38fe5df1b2d09 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 22:13:37 -0500 Subject: [PATCH 06/31] =?UTF-8?q?make=20url=20the=20first=20argument,=20ev?= =?UTF-8?q?en=20though=20it's=20optional=20=E2=80=94=C2=A0this=20is=20more?= =?UTF-8?q?=20future-proof,=20as=20we=20may=20add=20options=20in=20future?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/kit/src/runtime/app/navigation.js | 12 ++++++------ packages/kit/src/runtime/client/client.js | 8 ++++---- packages/kit/types/index.d.ts | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index 1ff44bc4d049..2ababa1fb0b7 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -128,21 +128,21 @@ export const onNavigate = /* @__PURE__ */ client_method('on_navigate'); export const afterNavigate = /* @__PURE__ */ client_method('after_navigate'); /** - * Programmatically create a new history entry with the given `$page.state`. + * Programmatically create a new history entry with the given `$page.state`. To use the current URL, you can pass `''` as the first argument. * - * @type {(state: App.PageState, url?: string | URL) => void} + * @type {(url: string | URL, state: App.PageState) => void} + * @param {string | URL} url * @param {App.PageState} state - * @param {string | URL} [url] * @returns {void} */ export const pushState = /* @__PURE__ */ client_method('push_state'); /** - * Programmatically replace the current history entry with the given `$page.state`. + * Programmatically replace the current history entry with the given `$page.state`. To use the current URL, you can pass `''` as the first argument. * - * @type {(state: App.PageState, url?: string | URL) => void} + * @type {(url: string | URL, state: App.PageState) => void} + * @param {string | URL} url * @param {App.PageState} state - * @param {string | URL} [url] * @returns {void} */ export const replaceState = /* @__PURE__ */ client_method('replace_state'); diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 73d0e712856f..dfa7e47a41ea 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1579,7 +1579,7 @@ export function create_client(app, target) { return preload_code(pathname); }, - push_state: (state, url = current.url) => { + push_state: (url, state) => { if (DEV) { try { devalue.stringify(state); @@ -1595,7 +1595,7 @@ export function create_client(app, target) { [PAGE_URL_KEY]: page.url.href }; - original_push_state.call(history, opts, '', new URL(url).href); + original_push_state.call(history, opts, '', resolve_url(url)); page = { ...page, state }; root.$set({ page }); @@ -1604,7 +1604,7 @@ export function create_client(app, target) { clear_onward_history(current_history_index, current_navigation_index); }, - replace_state: (state, url = current.url) => { + replace_state: (url, state) => { if (DEV) { try { devalue.stringify(state); @@ -1620,7 +1620,7 @@ export function create_client(app, target) { [PAGE_URL_KEY]: page.url.href }; - original_replace_state.call(history, opts, '', new URL(url).href); + original_replace_state.call(history, opts, '', resolve_url(url)); page = { ...page, state }; root.$set({ page }); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 81219cf7edc2..22e7ca2d0677 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2007,15 +2007,15 @@ declare module '$app/navigation' { * */ export const afterNavigate: (callback: (navigation: import('@sveltejs/kit').AfterNavigate) => void) => void; /** - * Programmatically create a new history entry with the given `$page.state`. + * Programmatically create a new history entry with the given `$page.state`. To use the current URL, you can pass `''` as the first argument. * * */ - export const pushState: (state: App.PageState, url?: string | URL) => void; + export const pushState: (url: string | URL, state: App.PageState) => void; /** - * Programmatically replace the current history entry with the given `$page.state`. + * Programmatically replace the current history entry with the given `$page.state`. To use the current URL, you can pass `''` as the first argument. * * */ - export const replaceState: (state: App.PageState, url?: string | URL) => void; + export const replaceState: (url: string | URL, state: App.PageState) => void; type MaybePromise = T | Promise; } From b3abca5f351893a39c5837fa46bd718c054c06ce Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 22:21:37 -0500 Subject: [PATCH 07/31] tests --- packages/kit/test/apps/basics/src/app.d.ts | 4 ++ .../shallow-routing/push-state/+layout.svelte | 5 ++ .../shallow-routing/push-state/+page.svelte | 19 +++++ .../shallow-routing/push-state/a/+page.svelte | 7 ++ .../shallow-routing/push-state/b/+page.svelte | 7 ++ .../replace-state/+layout.svelte | 5 ++ .../replace-state/+page.svelte | 19 +++++ .../replace-state/a/+page.svelte | 7 ++ .../replace-state/b/+page.svelte | 7 ++ .../kit/test/apps/basics/test/client.test.js | 71 +++++++++++++++++++ 10 files changed, 151 insertions(+) create mode 100644 packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+layout.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/a/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/b/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/+layout.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/a/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/shallow-routing/replace-state/b/+page.svelte 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.svelte b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.svelte new file mode 100644 index 000000000000..6ec4a2760704 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/shallow-routing/push-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/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..5e7396bdc903 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -913,3 +913,74 @@ 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('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'); + }); +}); From 2cd0d8eee81ae17a605ad03625bef8e4c4eb9f11 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 22:31:34 -0500 Subject: [PATCH 08/31] remove state from goto --- packages/kit/src/runtime/app/navigation.js | 1 - packages/kit/src/runtime/client/client.js | 19 +++++++++++-------- packages/kit/src/runtime/client/types.d.ts | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index 2ababa1fb0b7..05e3c51d52dd 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -24,7 +24,6 @@ export const disableScrollHandling = /* @__PURE__ */ client_method('disable_scro * @param {boolean} [opts.noScroll] If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation * @param {boolean} [opts.keepFocus] If `true`, the currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body * @param {boolean} [opts.invalidateAll] If `true`, all `load` functions of the page will be rerun. See https://kit.svelte.dev/docs/load#rerunning-load-functions for more info on invalidation. - * @param {any} [opts.state] The state of the new/updated history entry * @returns {Promise} */ export const goto = /* @__PURE__ */ client_method('goto'); diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index dfa7e47a41ea..17c3e3ef6454 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -300,22 +300,18 @@ export function create_client(app, target) { /** * @param {string | URL} url - * @param {{ noScroll?: boolean; replaceState?: boolean; keepFocus?: boolean; state?: any; invalidateAll?: boolean }} opts + * @param {{ noScroll?: boolean; replaceState?: boolean; keepFocus?: boolean; invalidateAll?: boolean }} opts * @param {number} redirect_count * @param {{}} [nav_token] */ async function goto( url, - { - noScroll = false, - replaceState = false, - keepFocus = false, - state = {}, - invalidateAll = false - }, + { noScroll = false, replaceState = false, keepFocus = false, invalidateAll = false }, redirect_count, nav_token ) { + const state = {}; + return navigate({ url: resolve_url(url), scroll: noScroll ? scroll_state() : null, @@ -1514,6 +1510,13 @@ export function create_client(app, target) { goto: (url, opts = {}) => { url = resolve_url(url); + if (DEV && opts.state) { + // TOOD 3.0 remove + throw new Error( + 'Passing `state` to `goto` is no longer supported. Use `pushState` and `replaceState` from `$app/navigation` instead.' + ); + } + if (url.origin !== origin) { return Promise.reject( new Error( diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index 2d6937e79eba..e134817eab67 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -95,7 +95,7 @@ export type NavigationFinished = { state: NavigationState; props: { constructors: Array; - components: Array; + components?: Array; page: Page; form?: Record | null; [key: `data_${number}`]: Record; From 45d6e87622ca4f52f242f82b86f0106bb573b34d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 22:47:52 -0500 Subject: [PATCH 09/31] tidy up --- packages/kit/src/exports/public.d.ts | 7 +++++++ packages/kit/src/runtime/app/navigation.js | 8 +------- packages/kit/src/runtime/client/client.js | 20 +++++++++----------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index ab88d6031ed7..70ded5dd4b87 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -845,6 +845,13 @@ export interface NavigationTarget { */ export type NavigationType = 'enter' | 'form' | 'leave' | 'link' | 'goto' | 'popstate'; +export interface NavigationOptions { + replaceState?: boolean; + noScroll?: boolean; + keepFocus?: boolean; + invalidateAll?: boolean; +} + export interface Navigation { /** * Where navigation was triggered from diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index 05e3c51d52dd..8c114252b095 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -11,13 +11,7 @@ export const disableScrollHandling = /* @__PURE__ */ client_method('disable_scro * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`. * For external URLs, use `window.location = url` instead of calling `goto(url)`. * - * @type {(url: string | URL, opts?: { - * replaceState?: boolean; - * noScroll?: boolean; - * keepFocus?: boolean; - * invalidateAll?: boolean; - * state?: any - * }) => Promise} + * @type {(url: string | URL, opts?: import('@sveltejs/kit').NavigationOptions) => Promise} * @param {string | URL} url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://kit.svelte.dev/docs/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. * @param {Object} [opts] Options related to the navigation * @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState` diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 17c3e3ef6454..0cdd9444aa00 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -300,7 +300,7 @@ export function create_client(app, target) { /** * @param {string | URL} url - * @param {{ noScroll?: boolean; replaceState?: boolean; keepFocus?: boolean; invalidateAll?: boolean }} opts + * @param {import('@sveltejs/kit').NavigationOptions} opts * @param {number} redirect_count * @param {{}} [nav_token] */ @@ -310,16 +310,13 @@ export function create_client(app, target) { redirect_count, nav_token ) { - const state = {}; - return navigate({ url: resolve_url(url), scroll: noScroll ? scroll_state() : null, - state, + state: {}, keepfocus: keepFocus, redirect_count, details: { - state, replaceState }, nav_token, @@ -1088,7 +1085,6 @@ export function create_client(app, target) { * redirect_count: number; * details: { * replaceState: boolean; - * state: any; * } | null; * type: import('@sveltejs/kit').Navigation["type"]; * delta?: number; @@ -1199,12 +1195,16 @@ export function create_client(app, target) { } if (details) { + // this is a new navigation, rather than a popstate const change = details.replaceState ? 0 : 1; - details.state[HISTORY_INDEX] = current_history_index += change; - details.state[NAVIGATION_INDEX] = current_navigation_index += change; + + const entry = { + [HISTORY_INDEX]: (current_history_index += change), + [NAVIGATION_INDEX]: (current_navigation_index += change) + }; const fn = details.replaceState ? original_replace_state : original_push_state; - fn.call(history, details.state, '', url); + fn.call(history, entry, '', url); if (!details.replaceState) { clear_onward_history(current_history_index, current_navigation_index); @@ -1818,7 +1818,6 @@ export function create_client(app, target) { keepfocus: options.keep_focus ?? false, redirect_count: 0, details: { - state: {}, replaceState: options.replace_state ?? url.href === location.href }, accepted: () => event.preventDefault(), @@ -1873,7 +1872,6 @@ export function create_client(app, target) { keepfocus: keep_focus ?? false, redirect_count: 0, details: { - state: {}, replaceState: replace_state ?? url.href === location.href }, nav_token: {}, From 22489bff53556104987bdcaecfb1b72b6f0b879f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 23:29:07 -0500 Subject: [PATCH 10/31] tidy up internal navigate API a bit --- packages/kit/src/runtime/client/client.js | 126 ++++++++++------------ 1 file changed, 56 insertions(+), 70 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 0cdd9444aa00..9f65636fbd13 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -132,6 +132,8 @@ function native_navigation(url) { return new Promise(() => {}); } +function noop() {} + /** * @param {import('./types.js').SvelteKitApp} app * @param {HTMLElement} target @@ -300,33 +302,24 @@ export function create_client(app, target) { /** * @param {string | URL} url - * @param {import('@sveltejs/kit').NavigationOptions} opts + * @param {import('@sveltejs/kit').NavigationOptions} options * @param {number} redirect_count * @param {{}} [nav_token] */ - async function goto( - url, - { noScroll = false, replaceState = false, keepFocus = false, invalidateAll = false }, - redirect_count, - nav_token - ) { + async function goto(url, options, redirect_count, nav_token) { return navigate({ + type: 'goto', url: resolve_url(url), - scroll: noScroll ? scroll_state() : null, - state: {}, - keepfocus: keepFocus, + keep_focus: options.keepFocus, + no_scroll: options.noScroll, + replace_state: options.replaceState, redirect_count, - details: { - replaceState - }, nav_token, accepted: () => { - if (invalidateAll) { + if (options.invalidateAll) { force_invalidation = true; } - }, - blocked: () => {}, - type: 'goto' + } }); } @@ -1078,36 +1071,36 @@ export function create_client(app, target) { /** * @param {{ - * url: URL; - * scroll: { x: number, y: number } | null; - * state: Record; - * keepfocus: boolean; - * redirect_count: number; - * details: { - * replaceState: boolean; - * } | null; * type: import('@sveltejs/kit').Navigation["type"]; - * delta?: number; + * url: URL; + * popped?: { + * state: Record; + * scroll: { x: number, y: number }; + * delta: number; + * }; + * keep_focus?: boolean; + * no_scroll?: boolean; + * replace_state?: boolean; + * redirect_count?: number; * nav_token?: {}; - * accepted: () => void; - * blocked: () => void; + * accepted?: () => void; + * blocked?: () => void; * }} opts */ async function navigate({ - url, - scroll, - state, - keepfocus, - redirect_count, - details, type, - delta, + url, + popped, + keep_focus, + no_scroll, + replace_state, + redirect_count = 0, nav_token = {}, - accepted, - blocked + accepted = noop, + blocked = noop }) { const intent = get_navigation_intent(url, false); - const nav = before_navigate({ url, type, delta, intent }); + const nav = before_navigate({ url, type, delta: popped?.delta, intent }); if (!nav) { blocked(); @@ -1194,19 +1187,21 @@ export function create_client(app, target) { url.pathname = navigation_result.props.page?.url.pathname; } - if (details) { + const state = popped ? popped.state : {}; + + if (!popped) { // this is a new navigation, rather than a popstate - const change = details.replaceState ? 0 : 1; + const change = replace_state ? 0 : 1; const entry = { [HISTORY_INDEX]: (current_history_index += change), [NAVIGATION_INDEX]: (current_navigation_index += change) }; - const fn = details.replaceState ? original_replace_state : original_push_state; + const fn = replace_state ? original_replace_state : original_push_state; fn.call(history, entry, '', url); - if (!details.replaceState) { + if (!replace_state) { clear_onward_history(current_history_index, current_navigation_index); } } @@ -1259,6 +1254,8 @@ export function create_client(app, target) { await tick(); // we reset scroll before dealing with focus, to avoid a flash of unscrolled content + const scroll = popped ? popped.scroll : no_scroll ? scroll_state() : null; + if (autoscroll) { const deep_linked = url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1))); @@ -1281,7 +1278,7 @@ export function create_client(app, target) { // focus event might not have been fired on it yet document.activeElement !== document.body; - if (!keepfocus && !changed_focus) { + if (!keep_focus && !changed_focus) { reset_focus(); } @@ -1510,6 +1507,7 @@ export function create_client(app, target) { goto: (url, opts = {}) => { url = resolve_url(url); + // @ts-expect-error if (DEV && opts.state) { // TOOD 3.0 remove throw new Error( @@ -1812,17 +1810,13 @@ export function create_client(app, target) { } navigate({ + type: 'link', url, - scroll: options.noscroll ? scroll_state() : null, - state: {}, - keepfocus: options.keep_focus ?? false, - redirect_count: 0, - details: { - replaceState: options.replace_state ?? url.href === location.href - }, + keep_focus: options.keep_focus ?? false, + no_scroll: options.noscroll ?? false, + replace_state: options.replace_state ?? url.href === location.href, accepted: () => event.preventDefault(), - blocked: () => event.preventDefault(), - type: 'link' + blocked: () => event.preventDefault() }); }); @@ -1866,18 +1860,11 @@ export function create_client(app, target) { url.search = new URLSearchParams(data).toString(); navigate({ + type: 'form', url, - scroll: noscroll ? scroll_state() : null, - state: {}, - keepfocus: keep_focus ?? false, - redirect_count: 0, - details: { - replaceState: replace_state ?? url.href === location.href - }, - nav_token: {}, - accepted: () => {}, - blocked: () => {}, - type: 'form' + keep_focus: keep_focus ?? false, + no_scroll: noscroll ?? false, + replace_state: replace_state ?? url.href === location.href }); }); @@ -1922,12 +1909,13 @@ export function create_client(app, target) { const delta = history_index - current_history_index; await navigate({ + type: 'popstate', url, - scroll, - state, - keepfocus: false, - redirect_count: 0, - details: null, + popped: { + state, + scroll, + delta + }, accepted: () => { current_history_index = history_index; current_navigation_index = navigation_index; @@ -1935,8 +1923,6 @@ export function create_client(app, target) { blocked: () => { history.go(-delta); }, - type: 'popstate', - delta, nav_token: token }); } else { From 2900902f628c40a08ee1844f7548da355d82796a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 23:43:03 -0500 Subject: [PATCH 11/31] more --- .../kit/src/core/sync/write_non_ambient.js | 2 +- packages/kit/src/runtime/client/client.js | 54 +++++++++---------- packages/kit/src/runtime/client/utils.js | 8 +-- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/kit/src/core/sync/write_non_ambient.js b/packages/kit/src/core/sync/write_non_ambient.js index a191495ac6f2..5ee771ea90a7 100644 --- a/packages/kit/src/core/sync/write_non_ambient.js +++ b/packages/kit/src/core/sync/write_non_ambient.js @@ -26,7 +26,7 @@ declare module "svelte/elements" { | null; 'data-sveltekit-preload-data'?: true | '' | 'hover' | 'tap' | 'off' | undefined | null; 'data-sveltekit-reload'?: true | '' | 'off' | undefined | null; - 'data-sveltekit-replacestate'?: true | '' | 'off' | undefined | null; + 'data-sveltekit-replace_state'?: true | '' | 'off' | undefined | null; } } diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 9f65636fbd13..3248c40edced 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -310,12 +310,12 @@ export function create_client(app, target) { return navigate({ type: 'goto', url: resolve_url(url), - keep_focus: options.keepFocus, - no_scroll: options.noScroll, + keepfocus: options.keepFocus, + noscroll: options.noScroll, replace_state: options.replaceState, redirect_count, nav_token, - accepted: () => { + accept: () => { if (options.invalidateAll) { force_invalidation = true; } @@ -1078,40 +1078,40 @@ export function create_client(app, target) { * scroll: { x: number, y: number }; * delta: number; * }; - * keep_focus?: boolean; - * no_scroll?: boolean; + * keepfocus?: boolean; + * noscroll?: boolean; * replace_state?: boolean; * redirect_count?: number; * nav_token?: {}; - * accepted?: () => void; - * blocked?: () => void; + * accept?: () => void; + * block?: () => void; * }} opts */ async function navigate({ type, url, popped, - keep_focus, - no_scroll, + keepfocus, + noscroll, replace_state, redirect_count = 0, nav_token = {}, - accepted = noop, - blocked = noop + accept = noop, + block = noop }) { const intent = get_navigation_intent(url, false); const nav = before_navigate({ url, type, delta: popped?.delta, intent }); if (!nav) { - blocked(); + block(); return; } - // store this before calling `accepted()`, which may change the index + // store this before calling `accept()`, which may change the index const previous_history_index = current_history_index; const previous_navigation_index = current_navigation_index; - accepted(); + accept(); navigating = true; @@ -1254,7 +1254,7 @@ export function create_client(app, target) { await tick(); // we reset scroll before dealing with focus, to avoid a flash of unscrolled content - const scroll = popped ? popped.scroll : no_scroll ? scroll_state() : null; + const scroll = popped ? popped.scroll : noscroll ? scroll_state() : null; if (autoscroll) { const deep_linked = @@ -1278,7 +1278,7 @@ export function create_client(app, target) { // focus event might not have been fired on it yet document.activeElement !== document.body; - if (!keep_focus && !changed_focus) { + if (!keepfocus && !changed_focus) { reset_focus(); } @@ -1812,11 +1812,11 @@ export function create_client(app, target) { navigate({ type: 'link', url, - keep_focus: options.keep_focus ?? false, - no_scroll: options.noscroll ?? false, + keepfocus: options.keepfocus, + noscroll: options.noscroll, replace_state: options.replace_state ?? url.href === location.href, - accepted: () => event.preventDefault(), - blocked: () => event.preventDefault() + accept: () => event.preventDefault(), + block: () => event.preventDefault() }); }); @@ -1843,8 +1843,8 @@ export function create_client(app, target) { const event_form = /** @type {HTMLFormElement} */ (event.target); - const { keep_focus, noscroll, reload, replace_state } = get_router_options(event_form); - if (reload) return; + const options = get_router_options(event_form); + if (options.reload) return; event.preventDefault(); event.stopPropagation(); @@ -1862,9 +1862,9 @@ export function create_client(app, target) { navigate({ type: 'form', url, - keep_focus: keep_focus ?? false, - no_scroll: noscroll ?? false, - replace_state: replace_state ?? url.href === location.href + keepfocus: options.keepfocus, + noscroll: options.noscroll, + replace_state: options.replace_state ?? url.href === location.href }); }); @@ -1916,11 +1916,11 @@ export function create_client(app, target) { scroll, delta }, - accepted: () => { + accept: () => { current_history_index = history_index; current_navigation_index = navigation_index; }, - blocked: () => { + block: () => { history.go(-delta); }, nav_token: token diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js index 8432ac181ddd..6a4bc726f741 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) From 3fcabe2e4f66971c2794a0581314f3f227472a16 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 13 Dec 2023 23:50:40 -0500 Subject: [PATCH 12/31] use current.url for invalidation, not location.href --- packages/kit/src/runtime/client/client.js | 5 ++--- .../routes/shallow-routing/push-state/+page.js | 5 +++++ .../shallow-routing/push-state/+page.svelte | 6 +++++- .../kit/test/apps/basics/test/client.test.js | 17 +++++++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 packages/kit/test/apps/basics/src/routes/shallow-routing/push-state/+page.js diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 3248c40edced..3c3c076d11f8 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -249,8 +249,7 @@ export function create_client(app, target) { if (!pending_invalidate) return; pending_invalidate = null; - const url = new URL(location.href); - const intent = get_navigation_intent(url, true); + const intent = get_navigation_intent(current.url, true); // Clear preload, it might be affected by the invalidation. // Also solves an edge case where a preload is triggered, the navigation for it @@ -264,7 +263,7 @@ export function create_client(app, target) { if (navigation_result) { if (navigation_result.type === 'redirect') { - await goto(new URL(navigation_result.location, url).href, {}, 1, nav_token); + await goto(new URL(navigation_result.location, current.url).href, {}, 1, nav_token); } else { if (navigation_result.props.page !== undefined) { page = navigation_result.props.page; 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 index 6ec4a2760704..40c616c3b2a5 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,7 +1,9 @@ + +{#if $page.state.showModal} + history.back()} /> +{/if} +``` + +The modal can be dismissed by navigating back (unsetting `$page.state.showModal`) or by interacting with it in a way that causes the `close` callback to run, which will navigate back programmatically. + +## API + +The first argument to `pushState` is the URL, relative to the current URL. To stay on the current URL, use `''`. + +The second argument is the new page state, which can be accessed via the [page store](/docs/modules#$app-stores-page) as `$page.state`. You can make page state type-safe by declaring an [`App.PageState`](/docs/types#app) interface (usually in `src/app.d.ts`). + +To set page state without creating a new history entry, use `replaceState` instead of `pushState`. + +## 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, state will _not_ be applied until they navigate. diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index 8c114252b095..47f804ad6c05 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -121,7 +121,7 @@ export const onNavigate = /* @__PURE__ */ client_method('on_navigate'); export const afterNavigate = /* @__PURE__ */ client_method('after_navigate'); /** - * Programmatically create a new history entry with the given `$page.state`. To use the current URL, you can pass `''` as the first argument. + * 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). * * @type {(url: string | URL, state: App.PageState) => void} * @param {string | URL} url @@ -131,7 +131,7 @@ export const afterNavigate = /* @__PURE__ */ client_method('after_navigate'); export const pushState = /* @__PURE__ */ client_method('push_state'); /** - * Programmatically replace the current history entry with the given `$page.state`. To use the current URL, you can pass `''` as the first argument. + * 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). * * @type {(url: string | URL, state: App.PageState) => void} * @param {string | URL} url diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 22e7ca2d0677..d8ffc82487e2 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -827,6 +827,13 @@ declare module '@sveltejs/kit' { */ export type NavigationType = 'enter' | 'form' | 'leave' | 'link' | 'goto' | 'popstate'; + export interface NavigationOptions { + replaceState?: boolean; + noScroll?: boolean; + keepFocus?: boolean; + invalidateAll?: boolean; + } + export interface Navigation { /** * Where navigation was triggered from @@ -1926,13 +1933,7 @@ declare module '$app/navigation' { * @param url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://kit.svelte.dev/docs/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. * @param {Object} opts Options related to the navigation * */ - export const goto: (url: string | URL, opts?: { - replaceState?: boolean; - noScroll?: boolean; - keepFocus?: boolean; - invalidateAll?: boolean; - state?: any; - }) => Promise; + export const goto: (url: string | URL, opts?: import('@sveltejs/kit').NavigationOptions) => 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. * @@ -2007,12 +2008,12 @@ declare module '$app/navigation' { * */ 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. + * 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. + * 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; From 4135f2ac06d26ce90735e0322d34439be3b13b11 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 14 Dec 2023 00:17:55 -0500 Subject: [PATCH 14/31] copy-paste fail --- packages/kit/src/core/sync/write_non_ambient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/core/sync/write_non_ambient.js b/packages/kit/src/core/sync/write_non_ambient.js index 5ee771ea90a7..a191495ac6f2 100644 --- a/packages/kit/src/core/sync/write_non_ambient.js +++ b/packages/kit/src/core/sync/write_non_ambient.js @@ -26,7 +26,7 @@ declare module "svelte/elements" { | null; 'data-sveltekit-preload-data'?: true | '' | 'hover' | 'tap' | 'off' | undefined | null; 'data-sveltekit-reload'?: true | '' | 'off' | undefined | null; - 'data-sveltekit-replace_state'?: true | '' | 'off' | undefined | null; + 'data-sveltekit-replacestate'?: true | '' | 'off' | undefined | null; } } From f4f38f2c7a58abe1cb099e1e58f8d0a04a2a8da3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 14 Dec 2023 00:20:32 -0500 Subject: [PATCH 15/31] on second thoughts --- packages/kit/src/exports/public.d.ts | 7 ------- packages/kit/src/runtime/app/navigation.js | 2 +- packages/kit/src/runtime/client/client.js | 2 +- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 70ded5dd4b87..ab88d6031ed7 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -845,13 +845,6 @@ export interface NavigationTarget { */ export type NavigationType = 'enter' | 'form' | 'leave' | 'link' | 'goto' | 'popstate'; -export interface NavigationOptions { - replaceState?: boolean; - noScroll?: boolean; - keepFocus?: boolean; - invalidateAll?: boolean; -} - export interface Navigation { /** * Where navigation was triggered from diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index 47f804ad6c05..498ccad085a1 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -11,7 +11,7 @@ export const disableScrollHandling = /* @__PURE__ */ client_method('disable_scro * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`. * For external URLs, use `window.location = url` instead of calling `goto(url)`. * - * @type {(url: string | URL, opts?: import('@sveltejs/kit').NavigationOptions) => Promise} + * @type {(url: string | URL, opts?: { replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; }) => Promise} * @param {string | URL} url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://kit.svelte.dev/docs/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. * @param {Object} [opts] Options related to the navigation * @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState` diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 3c3c076d11f8..2ae2022bc426 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -301,7 +301,7 @@ export function create_client(app, target) { /** * @param {string | URL} url - * @param {import('@sveltejs/kit').NavigationOptions} options + * @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; }} options * @param {number} redirect_count * @param {{}} [nav_token] */ From dc6f16822664fc54121397685b732390eae05fe9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 14 Dec 2023 00:22:34 -0500 Subject: [PATCH 16/31] links --- packages/kit/src/exports/public.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index ab88d6031ed7..36f8a6cc9d39 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -962,7 +962,7 @@ export interface Page< */ data: App.PageData & Record; /** - * The page state, which can be manipulated using the `pushState` and `replaceState` functions from `$app/navigation`. + * 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; /** From 0ce07b994fea6294f8c2aa2bf2801895c5fc89b9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 14 Dec 2023 00:26:54 -0500 Subject: [PATCH 17/31] link --- packages/kit/src/types/ambient.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/types/ambient.d.ts b/packages/kit/src/types/ambient.d.ts index 73a08a202c02..fa32a8a0662b 100644 --- a/packages/kit/src/types/ambient.d.ts +++ b/packages/kit/src/types/ambient.d.ts @@ -41,7 +41,7 @@ declare namespace App { export interface PageData {} /** - * The shape of the `$page.state` object, which can be manipulated using the `pushState` and `replaceState` functions from `$app/navigation`. + * 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 {} From f6b42ba04725695624b59f4a62357ff9cec149dc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 14 Dec 2023 00:27:59 -0500 Subject: [PATCH 18/31] regenerate types --- packages/kit/types/index.d.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index d8ffc82487e2..2c9c2cd9aa6f 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -827,13 +827,6 @@ declare module '@sveltejs/kit' { */ export type NavigationType = 'enter' | 'form' | 'leave' | 'link' | 'goto' | 'popstate'; - export interface NavigationOptions { - replaceState?: boolean; - noScroll?: boolean; - keepFocus?: boolean; - invalidateAll?: boolean; - } - export interface Navigation { /** * Where navigation was triggered from @@ -951,7 +944,7 @@ declare module '@sveltejs/kit' { */ data: App.PageData & Record; /** - * The page state, which can be manipulated using the `pushState` and `replaceState` functions from `$app/navigation`. + * 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; /** @@ -1933,7 +1926,12 @@ declare module '$app/navigation' { * @param url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://kit.svelte.dev/docs/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app. * @param {Object} opts Options related to the navigation * */ - export const goto: (url: string | URL, opts?: import('@sveltejs/kit').NavigationOptions) => Promise; + export const goto: (url: string | URL, opts?: { + replaceState?: boolean; + noScroll?: boolean; + keepFocus?: boolean; + invalidateAll?: boolean; + }) => 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. * @@ -2113,7 +2111,7 @@ declare namespace App { export interface PageData {} /** - * The shape of the `$page.state` object, which can be manipulated using the `pushState` and `replaceState` functions from `$app/navigation`. + * 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 {} From 7af49f91f3ef4fb0a5bce922c2e971e9ad8827b8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 14 Dec 2023 00:30:05 -0500 Subject: [PATCH 19/31] update preloadData docs --- packages/kit/src/runtime/app/navigation.js | 2 +- packages/kit/types/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index 498ccad085a1..122323def7c7 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -57,7 +57,7 @@ export const invalidateAll = /* @__PURE__ */ client_method('invalidate_all'); * * 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 new `$page.data` when the preload is complete. * * @type {(href: string) => Promise>} * @param {string} href Page to preload diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 2c9c2cd9aa6f..df864ec0654f 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1961,7 +1961,7 @@ 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 new `$page.data` when the preload is complete. * * @param href Page to preload * */ From c76c15073fdd9cd4e79d6fc6d9d409f9ac356aca Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 14 Dec 2023 00:35:10 -0500 Subject: [PATCH 20/31] fix preloadData docs --- packages/kit/src/runtime/app/navigation.js | 4 ++-- packages/kit/types/index.d.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index 122323def7c7..db829db9b761 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -57,11 +57,11 @@ export const invalidateAll = /* @__PURE__ */ client_method('invalidate_all'); * * 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 with the new `$page.data` 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. * * @type {(href: string) => Promise>} * @param {string} href Page to preload - * @returns {Promise>} + * @returns {Promise<{ type: 'loaded', status: number, data: Record } | { type: 'redirect', location: string }>} */ export const preloadData = /* @__PURE__ */ client_method('preload_data'); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index df864ec0654f..07840d091f5c 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1961,7 +1961,7 @@ 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 with the new `$page.data` 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 * */ From dad37a70310c084b4a3e92d0f2493ef6e767d30c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 14 Dec 2023 00:39:38 -0500 Subject: [PATCH 21/31] drive-by fix --- packages/kit/src/runtime/client/client.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 2ae2022bc426..ccf22fd3c72b 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1805,17 +1805,16 @@ export function create_client(app, target) { // hashchange event shouldn't occur if the router is replacing state. hash_navigating = false; - event.preventDefault(); } + event.preventDefault(); + navigate({ type: 'link', url, keepfocus: options.keepfocus, noscroll: options.noscroll, - replace_state: options.replace_state ?? url.href === location.href, - accept: () => event.preventDefault(), - block: () => event.preventDefault() + replace_state: options.replace_state ?? url.href === location.href }); }); From 3f31983b39094c6025f23ae791892da80439da96 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 14 Dec 2023 09:22:36 +0100 Subject: [PATCH 22/31] Apply suggestions from code review Co-authored-by: Ignatius Bagus --- packages/kit/src/runtime/app/navigation.js | 6 +++--- packages/kit/src/runtime/client/client.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index db829db9b761..2e7c7bdaa2fe 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -61,7 +61,7 @@ export const invalidateAll = /* @__PURE__ */ client_method('invalidate_all'); * * @type {(href: string) => Promise>} * @param {string} href Page to preload - * @returns {Promise<{ type: 'loaded', status: number, data: Record } | { type: 'redirect', location: string }>} + * @returns {Promise<{ type: 'loaded'; status: number; data: Record } | { type: 'redirect'; location: string }>} */ export const preloadData = /* @__PURE__ */ client_method('preload_data'); @@ -123,7 +123,7 @@ export const afterNavigate = /* @__PURE__ */ client_method('after_navigate'); /** * 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). * - * @type {(url: string | URL, state: App.PageState) => void} + * @type {(url: string | URL; state: App.PageState) => void} * @param {string | URL} url * @param {App.PageState} state * @returns {void} @@ -133,7 +133,7 @@ export const pushState = /* @__PURE__ */ client_method('push_state'); /** * 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). * - * @type {(url: string | URL, state: App.PageState) => void} + * @type {(url: string | URL; state: App.PageState) => void} * @param {string | URL} url * @param {App.PageState} state * @returns {void} diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index ccf22fd3c72b..3dd390898f25 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -46,7 +46,7 @@ import { get_message, get_status } from '../../utils/error.js'; let errored = false; -/** @typedef {{ x: number, y: number }} ScrollPosition */ +/** @typedef {{ x: number; y: number }} ScrollPosition */ // We track the scroll position associated with each history entry in sessionStorage, // rather than on history.state itself, because when navigation is driven by From 89c31612722e54d6080d24c1a6a102f3a75453a6 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 14 Dec 2023 09:57:32 +0100 Subject: [PATCH 23/31] tweaks --- packages/kit/src/runtime/client/client.js | 9 +++------ packages/kit/src/runtime/client/types.d.ts | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 3dd390898f25..26cc63d88ebb 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -192,6 +192,8 @@ export function create_client(app, target) { let updating = false; let navigating = false; let hash_navigating = false; + /** True as soon as there happened one client-side navigation */ + let has_navigated = false; let force_invalidation = false; @@ -205,8 +207,6 @@ export function create_client(app, target) { /** @type {number} */ let current_navigation_index = history.state?.[NAVIGATION_INDEX]; - let has_navigated = false; - if (!current_history_index) { // we use Date.now() as an offset so that cross-document navigations // within the app don't result in data loss @@ -295,7 +295,6 @@ export function create_client(app, target) { capture_snapshot(current_navigation_index); storage.set(SNAPSHOT_KEY, snapshots); - storage.set(STATES_KEY, states, devalue.stringify); } @@ -1183,7 +1182,7 @@ export function create_client(app, target) { // ensure the url pathname matches the page's trailing slash option if (navigation_result.props.page.url.pathname !== url.pathname) { - url.pathname = navigation_result.props.page?.url.pathname; + url.pathname = navigation_result.props.page.url.pathname; } const state = popped ? popped.state : {}; @@ -1878,9 +1877,7 @@ export function create_client(app, target) { const scroll = scroll_positions[history_index]; const state = states[history_index] ?? {}; const url = new URL(event.state[PAGE_URL_KEY] ?? location.href); - const navigation_index = event.state[NAVIGATION_INDEX]; - const is_hash_change = location.href.split('#')[0] === current.url.href.split('#')[0]; const shallow = navigation_index === current_navigation_index && (has_navigated || is_hash_change); diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index e134817eab67..458c36ddddae 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -95,7 +95,7 @@ export type NavigationFinished = { state: NavigationState; props: { constructors: Array; - components?: Array; + components?: Array; page: Page; form?: Record | null; [key: `data_${number}`]: Record; From f93df9088b32ef1de97877df4b492a9eb2247dbe Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Dec 2023 10:01:34 +0100 Subject: [PATCH 24/31] changeset, breaking change docs --- .changeset/light-moons-dress.md | 5 +++++ .changeset/serious-months-happen.md | 5 +++++ .../docs/60-appendix/30-migrating-to-sveltekit-2.md | 4 ++-- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .changeset/light-moons-dress.md create mode 100644 .changeset/serious-months-happen.md diff --git a/.changeset/light-moons-dress.md b/.changeset/light-moons-dress.md new file mode 100644 index 000000000000..85afe61bcaaa --- /dev/null +++ b/.changeset/light-moons-dress.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": minor +--- + +feat: implement shallow routing diff --git a/.changeset/serious-months-happen.md b/.changeset/serious-months-happen.md new file mode 100644 index 000000000000..861d42d2959f --- /dev/null +++ b/.changeset/serious-months-happen.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": major +--- + +breaking: remove state option from goto in favor of shallow routing diff --git a/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md b/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md index 1ab03abdc277..b4efa640053c 100644 --- a/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md +++ b/documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md @@ -66,9 +66,9 @@ export function load({ fetch }) { } ``` -## goto(...) no longer accepts external URLs +## goto(...) changes -To navigate to an external URL, use `window.location = url`. +`goto(...)` no longer accepts external URLs. To navigate to an external URL, use `window.location = url`. The `state` option was removed in favor of [shallow routing](shallow-routing). ## paths are now relative by default From 54fdf2df731239d40f91c49d139fd6334b653dcb Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Dec 2023 10:10:25 +0100 Subject: [PATCH 25/31] code-golf --- packages/kit/src/runtime/app/navigation.js | 4 ++-- packages/kit/src/runtime/client/client.js | 5 +++-- packages/kit/src/utils/url.js | 8 ++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index 2e7c7bdaa2fe..4bbfae215f9c 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -123,7 +123,7 @@ export const afterNavigate = /* @__PURE__ */ client_method('after_navigate'); /** * 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). * - * @type {(url: string | URL; state: App.PageState) => void} + * @type {(url: string | URL, state: App.PageState) => void} * @param {string | URL} url * @param {App.PageState} state * @returns {void} @@ -133,7 +133,7 @@ export const pushState = /* @__PURE__ */ client_method('push_state'); /** * 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). * - * @type {(url: string | URL; state: App.PageState) => void} + * @type {(url: string | URL, state: App.PageState) => void} * @param {string | URL} url * @param {App.PageState} state * @returns {void} diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 26cc63d88ebb..4e09b0f57e42 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -4,6 +4,7 @@ import { add_data_suffix, decode_params, decode_pathname, + strip_hash, make_trackable, normalize_path } from '../../utils/url.js'; @@ -1782,7 +1783,7 @@ export function create_client(app, target) { // This will ensure the `hashchange` event is fired // Removing the hash does a full page navigation in the browser, so make sure a hash is present const [nonhash, hash] = url.href.split('#'); - if (hash !== undefined && nonhash === location.href.split('#')[0]) { + if (hash !== undefined && nonhash === strip_hash(location)) { // If we are trying to navigate to the same hash, we should only // attempt to scroll to that element and avoid any history changes. // Otherwise, this can cause Firefox to incorrectly assign a null @@ -1878,7 +1879,7 @@ export function create_client(app, target) { const state = states[history_index] ?? {}; const url = new URL(event.state[PAGE_URL_KEY] ?? location.href); const navigation_index = event.state[NAVIGATION_INDEX]; - const is_hash_change = location.href.split('#')[0] === current.url.href.split('#')[0]; + const is_hash_change = strip_hash(location) === strip_hash(current.url); const shallow = navigation_index === current_navigation_index && (has_navigated || is_hash_change); 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` From f9354c54041e0723c74440c97a7602099a6716ff Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Dec 2023 10:13:58 +0100 Subject: [PATCH 26/31] this seems unnecessary --- packages/kit/src/runtime/client/utils.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js index 6a4bc726f741..d205f48cdc42 100644 --- a/packages/kit/src/runtime/client/utils.js +++ b/packages/kit/src/runtime/client/utils.js @@ -220,12 +220,6 @@ export function notifiable_store(value) { store.set(new_value); } - /** @param {(value: any) => any} fn */ - function update(fn) { - ready = false; - store.update(fn); - } - /** @param {(value: any) => void} run */ function subscribe(run) { /** @type {any} */ @@ -237,7 +231,7 @@ export function notifiable_store(value) { }); } - return { notify, set, update, subscribe }; + return { notify, set, subscribe }; } export function create_updated_store() { From edecbe6ede4ff4860bdfcdd20794bba8bacd56cc Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Dec 2023 10:37:20 +0100 Subject: [PATCH 27/31] mention preloadData --- .../docs/30-advanced/67-shallow-routing.md | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/documentation/docs/30-advanced/67-shallow-routing.md b/documentation/docs/30-advanced/67-shallow-routing.md index 199d28482712..47a2c624a628 100644 --- a/documentation/docs/30-advanced/67-shallow-routing.md +++ b/documentation/docs/30-advanced/67-shallow-routing.md @@ -4,7 +4,7 @@ title: Shallow routing As you navigate around a SvelteKit app, you create _history entries_. Clicking the back and forward buttons traverses through this list of entries, re-running any `load` functions and replacing page components as necessary. -Sometimes, it's useful to create history entries _without_ navigating. For example, you might want to show a modal dialog that the user can dismiss by navigating back. (This is particularly valuable on mobile devices, where swipe gestures are often more natural than interacting directly with the UI. In these cases, a modal that is _not_ associated with a history entry can be a source of frustration, as a user may swipe backwards in an attempt to dismiss it and find themselves on the wrong page.) +Sometimes, it's useful to create history entries _without_ navigating. For example, you might want to show a modal dialog that the user can dismiss by navigating back. This is particularly valuable on mobile devices, where swipe gestures are often more natural than interacting directly with the UI. In these cases, a modal that is _not_ associated with a history entry can be a source of frustration, as a user may swipe backwards in an attempt to dismiss it and find themselves on the wrong page. SvelteKit makes this possible with the [`pushState`](/docs/modules#$app-navigation-pushstate) and [`replaceState`](/docs/modules#$app-navigation-replacestate) functions, which allow you to associate state with a history entry without navigating. For example, to implement a history-driven modal: @@ -37,6 +37,40 @@ The second argument is the new page state, which can be accessed via the [page s To set page state without creating a new history entry, use `replaceState` instead of `pushState`. +## Loading data from a route + +When doing a shallow navigation you might want to show a reduced version of a page in a modal, for which you need the data of that page. You can retrieve it by calling `preloadData` with the desired page URL and use its result to populate the component. This will call the load function(s) associated with that route and give you back the result. + +```svelte + + + + + {#await data} + + {:then } +

{data.title}

+

{data.content}

+ {/await} + +
+``` + ## 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, state will _not_ be applied until they navigate. + +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. From f4152ef0b2a2664076d588664356ce7878ac0a7a Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Dec 2023 11:00:27 +0100 Subject: [PATCH 28/31] use original replace state --- packages/kit/src/runtime/client/client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 4e09b0f57e42..c3c9c950a854 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -214,7 +214,7 @@ export function create_client(app, target) { current_history_index = current_navigation_index = Date.now(); // create initial history entry, so we can return here - history.replaceState( + original_replace_state( { ...history.state, [HISTORY_INDEX]: current_history_index, @@ -1937,7 +1937,7 @@ export function create_client(app, target) { // we need to update history, otherwise we have to leave it alone if (hash_navigating) { hash_navigating = false; - history.replaceState( + original_replace_state( { ...history.state, [HISTORY_INDEX]: ++current_history_index, From 82d67373ba4727787e4f8bfbf6ce54b2c0b00f5c Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Dec 2023 11:41:44 +0100 Subject: [PATCH 29/31] oops --- packages/kit/src/runtime/client/client.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index c3c9c950a854..f9f8a8dccd21 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -214,7 +214,8 @@ export function create_client(app, target) { current_history_index = current_navigation_index = Date.now(); // create initial history entry, so we can return here - original_replace_state( + original_replace_state.call( + history, { ...history.state, [HISTORY_INDEX]: current_history_index, @@ -1937,7 +1938,8 @@ export function create_client(app, target) { // we need to update history, otherwise we have to leave it alone if (hash_navigating) { hash_navigating = false; - original_replace_state( + original_replace_state.call( + history, { ...history.state, [HISTORY_INDEX]: ++current_history_index, From 3a2ff3b6e908e58c6015790bf1a81f55c718e6fd Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Dec 2023 13:19:09 +0100 Subject: [PATCH 30/31] handle SPA case --- packages/kit/src/runtime/client/client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index f9f8a8dccd21..c778c18d7932 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -193,7 +193,7 @@ export function create_client(app, target) { let updating = false; let navigating = false; let hash_navigating = false; - /** True as soon as there happened one client-side navigation */ + /** True as soon as there happened one client-side navigation (excluding the SvelteKit-initialized initial one when in SPA mode) */ let has_navigated = false; let force_invalidation = false; @@ -1244,6 +1244,7 @@ export function create_client(app, target) { } root.$set(navigation_result.props); + has_navigated = true; } else { initialize(navigation_result); } @@ -1302,7 +1303,6 @@ export function create_client(app, target) { stores.navigating.set(null); updating = false; - has_navigated = true; } /** From 5c6e6ae99797b5d10d40790300d0d38375cd5e2b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 14 Dec 2023 09:26:39 -0500 Subject: [PATCH 31/31] more involved example - show importing a +page.svelte and correctly handling a click event, and avoid --- .../docs/30-advanced/67-shallow-routing.md | 69 ++++++++++++------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/documentation/docs/30-advanced/67-shallow-routing.md b/documentation/docs/30-advanced/67-shallow-routing.md index 47a2c624a628..466f9ee2049e 100644 --- a/documentation/docs/30-advanced/67-shallow-routing.md +++ b/documentation/docs/30-advanced/67-shallow-routing.md @@ -37,36 +37,57 @@ The second argument is the new page state, which can be accessed via the [page s To set page state without creating a new history entry, use `replaceState` instead of `pushState`. -## Loading data from a route +## Loading data for a route -When doing a shallow navigation you might want to show a reduced version of a page in a modal, for which you need the data of that page. You can retrieve it by calling `preloadData` with the desired page URL and use its result to populate the component. This will call the load function(s) associated with that route and give you back the result. +When shallow routing, you may want to render another `+page.svelte` inside the current page. For example, clicking on a photo thumbnail could pop up the detail view without navigating to the photo page. + +For this to work, you need to load the data that the `+page.svelte` expects. A convenient way to do this is to use [`preloadData`](/docs/modules#$app-navigation-preloaddata) inside the `click` handler of an `
` element. If the element (or a parent) uses [`data-sveltekit-preload-data`](/docs/link-options#data-sveltekit-preload-data), the data will have already been requested, and `preloadData` will reuse that request. ```svelte - + - - {#await data} - - {:then } -

{data.title}

-

{data.content}

- {/await} - -
+{#each data.thumbnails as thumbnail} +
{ + // bail if opening a new tab, or we're on too small a screen + if (e.metaKey || innerWidth < 640) return; + + // prevent navigation + e.preventDefault(); + + const { href } = e.currentTarget; + + // run `load` functions (or rather, get the result of the `load` functions + // that are already running because of `data-sveltekit-preload-data`) + const result = await preloadData(href); + + if (result.type === 'loaded' && result.status === 200) { + pushState(href, { selected: result.data }); + } else { + // something bad happened! try navigating + goto(href); + } + }} + > + {thumbnail.alt} + +{/each} + +{#if $page.state.selected} + history.goBack()}> + + + +{/if} ``` ## Caveats