From c8b99dd5c19ad72d45668d8ba0d39c9e3630ce42 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 25 Jan 2023 14:36:18 +0100 Subject: [PATCH] feat: implement shallow routing closes #2673 --- packages/kit/src/runtime/client/client.js | 61 ++++++++++++++----- .../src/routes/load/skip-load/+layout.js | 7 +++ .../src/routes/load/skip-load/+layout.svelte | 11 ++++ .../src/routes/load/skip-load/+page.server.js | 7 +++ .../src/routes/load/skip-load/+page.svelte | 5 ++ .../routes/load/skip-load/inner/+page.svelte | 1 + .../kit/test/apps/basics/test/client.test.js | 26 ++++++++ packages/kit/types/ambient.d.ts | 4 ++ 8 files changed, 107 insertions(+), 15 deletions(-) create mode 100644 packages/kit/test/apps/basics/src/routes/load/skip-load/+layout.js create mode 100644 packages/kit/test/apps/basics/src/routes/load/skip-load/+layout.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/load/skip-load/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/load/skip-load/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/load/skip-load/inner/+page.svelte diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index c3fb0bbc22b2..2b128bd730e9 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -160,7 +160,7 @@ export function create_client({ target, base }) { /** * @param {string | URL} url - * @param {{ noScroll?: boolean; replaceState?: boolean; keepFocus?: boolean; state?: any; invalidateAll?: boolean }} opts + * @param {{ noScroll?: boolean; replaceState?: boolean; keepFocus?: boolean; state?: any; invalidateAll?: boolean; skipLoad?: boolean }} opts * @param {string[]} redirect_chain * @param {{}} [nav_token] */ @@ -171,7 +171,8 @@ export function create_client({ target, base }) { replaceState = false, keepFocus = false, state = {}, - invalidateAll = false + invalidateAll = false, + skipLoad = false }, redirect_chain, nav_token @@ -184,6 +185,7 @@ export function create_client({ target, base }) { url, scroll: noScroll ? scroll_state() : null, keepfocus: keepFocus, + skip_load: skipLoad, redirect_chain, details: { state, @@ -238,13 +240,13 @@ export function create_client({ target, base }) { * @param {import('./types').NavigationIntent | undefined} intent * @param {URL} url * @param {string[]} redirect_chain - * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, details: { replaceState: boolean, state: any } | null}} [opts] + * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, skip_load: boolean; details: { replaceState: boolean, state: any } | null}} [opts] * @param {{}} [nav_token] To distinguish between different navigation events and determine the latest. Needed for example for redirects to keep the original token * @param {() => void} [callback] */ async function update(intent, url, redirect_chain, opts, nav_token = {}, callback) { token = nav_token; - let navigation_result = intent && (await load_route(intent)); + let navigation_result = intent && (await load_route(intent, opts?.skip_load)); if (!navigation_result) { navigation_result = await server_fallback( @@ -474,6 +476,9 @@ export function create_client({ target, base }) { p += 1; } + // data_changed could be true if more nodes were added, but the data stayed the same + data_changed = Object.keys(result.props).some((key) => key.startsWith('data_')); + const page_changed = !current.url || url.href !== current.url.href || @@ -510,10 +515,11 @@ export function create_client({ target, base }) { * params: Record; * route: { id: string | null }; * server_data_node: import('./types').DataNode | null; + * skip_load?: boolean; * }} options * @returns {Promise} */ - async function load_node({ loader, parent, url, params, route, server_data_node }) { + async function load_node({ loader, parent, url, params, route, server_data_node, skip_load }) { /** @type {Record | null} */ let data = null; @@ -533,6 +539,11 @@ export function create_client({ target, base }) { } if (node.universal?.load) { + if (DEV && skip_load) { + // TODO warning instead? + throw new Error('Cannot skip load when some data needs to be loaded'); + } + /** @param {string[]} deps */ function depends(...deps) { for (const dep of deps) { @@ -699,9 +710,10 @@ export function create_client({ target, base }) { /** * @param {import('./types').NavigationIntent} intent + * @param {boolean} [skip_load] * @returns {Promise} */ - async function load_route({ id, invalidating, url, params, route }) { + async function load_route({ id, invalidating, url, params, route }, skip_load) { if (load_cache?.id === id) { return load_cache.promise; } @@ -728,19 +740,24 @@ export function create_client({ target, base }) { const invalid = !!loader?.[0] && (previous?.loader !== loader[1] || - has_changed( - acc.some(Boolean), - route_changed, - url_changed, - previous.server?.uses, - params - )); + (!skip_load && + has_changed( + acc.some(Boolean), + route_changed, + url_changed, + previous.server?.uses, + params + ))); acc.push(invalid); return acc; }, /** @type {boolean[]} */ ([])); if (invalid_server_nodes.some(Boolean)) { + if (DEV && skip_load) { + // TODO warning instead? + throw new Error('Cannot skip load when some data needs to be loaded'); + } try { server_data = await load_data(url, invalid_server_nodes); } catch (error) { @@ -773,7 +790,14 @@ export function create_client({ target, base }) { const valid = (!server_data_node || server_data_node.type === 'skip') && loader[1] === previous?.loader && - !has_changed(parent_changed, route_changed, url_changed, previous.universal?.uses, params); + (skip_load || + !has_changed( + parent_changed, + route_changed, + url_changed, + previous.universal?.uses, + params + )); if (valid) return previous; parent_changed = true; @@ -800,7 +824,8 @@ export function create_client({ target, base }) { // and if current loader uses server data, we want to reuse previous data. server_data_node === undefined && loader[0] ? { type: 'skip' } : server_data_node ?? null, previous?.server - ) + ), + skip_load }); }); @@ -1049,6 +1074,7 @@ export function create_client({ target, base }) { * url: URL; * scroll: { x: number, y: number } | null; * keepfocus: boolean; + * skip_load: boolean; * redirect_chain: string[]; * details: { * replaceState: boolean; @@ -1065,6 +1091,7 @@ export function create_client({ target, base }) { url, scroll, keepfocus, + skip_load, redirect_chain, details, type, @@ -1097,6 +1124,7 @@ export function create_client({ target, base }) { redirect_chain, { scroll, + skip_load, keepfocus, details }, @@ -1459,6 +1487,7 @@ export function create_client({ target, base }) { url, scroll: options.noscroll ? scroll_state() : null, keepfocus: false, + skip_load: false, // TODO data-sveltekit-skipload? redirect_chain: [], details: { state: {}, @@ -1513,6 +1542,7 @@ export function create_client({ target, base }) { url, scroll: noscroll ? scroll_state() : null, keepfocus: false, + skip_load: false, redirect_chain: [], details: { state: {}, @@ -1537,6 +1567,7 @@ export function create_client({ target, base }) { url: new URL(location.href), scroll: scroll_positions[event.state[INDEX_KEY]], keepfocus: false, + skip_load: false, // TODO should popstate skip load if navigation to it was with skipLoad? redirect_chain: [], details: null, accepted: () => { diff --git a/packages/kit/test/apps/basics/src/routes/load/skip-load/+layout.js b/packages/kit/test/apps/basics/src/routes/load/skip-load/+layout.js new file mode 100644 index 000000000000..c5aa954c24e0 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/skip-load/+layout.js @@ -0,0 +1,7 @@ +/** @type {import('./$types').LayoutLoad} */ +export async function load({ url }) { + url.pathname; // force rerun on every page change + return { + random: Math.random() + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/load/skip-load/+layout.svelte b/packages/kit/test/apps/basics/src/routes/load/skip-load/+layout.svelte new file mode 100644 index 000000000000..8ea8bd92336c --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/skip-load/+layout.svelte @@ -0,0 +1,11 @@ + + +/load/skip-load +/load/skip-load/inner +

{data.random}

+
{JSON.stringify($page.data)}
+ diff --git a/packages/kit/test/apps/basics/src/routes/load/skip-load/+page.server.js b/packages/kit/test/apps/basics/src/routes/load/skip-load/+page.server.js new file mode 100644 index 000000000000..4fb71d5d2157 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/skip-load/+page.server.js @@ -0,0 +1,7 @@ +/** @type {import('./$types').PageServerLoad} */ +export async function load({ url }) { + url.search; // force rerun on every query change + return { + pageRandom: Math.random() + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/load/skip-load/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/skip-load/+page.svelte new file mode 100644 index 000000000000..dee86950fd07 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/skip-load/+page.svelte @@ -0,0 +1,5 @@ + + +

Skip load: {data.pageRandom}

diff --git a/packages/kit/test/apps/basics/src/routes/load/skip-load/inner/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/skip-load/inner/+page.svelte new file mode 100644 index 000000000000..97c91fae21e8 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/skip-load/inner/+page.svelte @@ -0,0 +1 @@ +

Skip load inner

diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 91b4beec802a..cf63856addbf 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -193,6 +193,32 @@ test.describe('Load', () => { expect(did_request_data).toBe(false); }); + test('skipLoad skips loading data', async ({ page, app }) => { + await page.goto('/load/skip-load'); + + const page_content = await page.textContent('p.skip-page'); + const layout_content = await page.textContent('p.skip-layout'); + const store_content = await page.textContent('pre'); + + expect(page_content).toMatch(/Skip load: 0\.\d+/); + expect(layout_content).toMatch(/0\.\d+/); + expect(store_content).toMatch( + /{"foo":{"bar":"Custom layout"},"random":0\.\d+,"pageRandom":0\.\d+}/ + ); + + await app.goto('/load/skip-load?foo', { skipLoad: true }); + expect(await page.textContent('p.skip-page')).toBe(page_content); + expect(await page.textContent('p.skip-layout')).toBe(layout_content); + expect(await page.textContent('pre')).toBe(store_content); + + await app.goto('/load/skip-load/inner', { skipLoad: true }); + expect(await page.textContent('p.skip-page')).toBe('Skip load inner'); + expect(await page.textContent('p.skip-layout')).toBe(layout_content); + expect(await page.textContent('pre')).toBe( + store_content.slice(0, store_content.indexOf(',"pageRandom"')) + '}' + ); + }); + if (process.env.DEV) { test('using window.fetch causes a warning', async ({ page, baseURL }) => { await Promise.all([ diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts index 8a81037954c4..1b50c5ec4a3b 100644 --- a/packages/kit/types/ambient.d.ts +++ b/packages/kit/types/ambient.d.ts @@ -197,6 +197,10 @@ declare module '$app/navigation' { * If `true`, all `load` functions of the page will be rerun. See https://kit.svelte.dev/docs/load#invalidation for more info on invalidation. */ invalidateAll?: boolean; + /** + * If `true`, will not run any `load` functions of the page. This only makes sense if you're navigating to a page where all `load` function results are already available. + */ + skipLoad?: boolean; } ): Promise; /**