Skip to content

Commit

Permalink
feat: Add ability to invalidate a custom identifier on goto() (#13256)
Browse files Browse the repository at this point in the history
closes #10659

---------

Co-authored-by: Tee Ming <[email protected]>
Co-authored-by: Simon H <[email protected]>
  • Loading branch information
3 people authored Jan 16, 2025
1 parent a91ba1f commit 04958cc
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/nasty-spoons-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: add ability to invalidate a custom identifier on `goto()`
18 changes: 15 additions & 3 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ function persist_state() {

/**
* @param {string | URL} url
* @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; state?: Record<string, any> }} options
* @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any> }} options
* @param {number} redirect_count
* @param {{}} [nav_token]
*/
Expand All @@ -392,6 +392,10 @@ async function _goto(url, options, redirect_count, nav_token) {
if (options.invalidateAll) {
force_invalidation = true;
}

if (options.invalidate) {
options.invalidate.forEach(push_invalidated);
}
}
});
}
Expand Down Expand Up @@ -1805,6 +1809,7 @@ export function disableScrollHandling() {
* @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://svelte.dev/docs/kit/load#rerunning-load-functions for more info on invalidation.
* @param {Array<string | URL | ((url: URL) => boolean)>} [opts.invalidate] Causes any load functions to re-run if they depend on one of the urls
* @param {App.PageState} [opts.state] An optional object that will be available as `page.state`
* @returns {Promise<void>}
*/
Expand Down Expand Up @@ -1851,14 +1856,21 @@ export function invalidate(resource) {
throw new Error('Cannot call invalidate(...) on the server');
}

push_invalidated(resource);

return _invalidate();
}

/**
* @param {string | URL | ((url: URL) => boolean)} resource The invalidated URL
*/
function push_invalidated(resource) {
if (typeof resource === 'function') {
invalidated.push(resource);
} else {
const { href } = new URL(resource, location.href);
invalidated.push((url) => url.href === href);
}

return _invalidate();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** @type {import('./$types').LayoutLoad} */
export function load({ depends }) {
depends('invalidate-depends-goto:layout');
return {
layout: new Date().getTime()
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('./$types').PageLoad} */
export function load({ data, depends }) {
depends('invalidate-depends-goto:shared');
return {
shared: new Date().getTime(),
...data
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** @type {import('./$types').PageServerLoad} */
export function load({ depends }) {
depends('invalidate-depends-goto:server');
return {
server: new Date().getTime()
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script>
import { goto } from '$app/navigation';
import { page } from '$app/state';
/** @type {import('./$types').PageData} */
export let data;
</script>

<p class="layout">{data.layout}</p>
<button
type="button"
class="specified"
on:click={() =>
(window.promise = goto(page.url.pathname, {
invalidate: ['invalidate-depends-goto:layout', 'invalidate-depends-goto:shared']
}))}
>
invalidate specified
</button>

<p class="server">{data.server}</p>
<button
type="button"
class="server"
on:click={() =>
(window.promise = goto(page.url.pathname, { invalidate: ['invalidate-depends-goto:server'] }))}
>
invalidate server
</button>

<p class="shared">{data.shared}</p>
<button
type="button"
class="shared"
on:click={() =>
(window.promise = goto(page.url.pathname, { invalidate: ['invalidate-depends-goto:shared'] }))}
>
invalidate shared
</button>

<p class="neither">neither</p>
<button
type="button"
class="neither"
on:click={() =>
(window.promise = goto(page.url.pathname, { invalidate: ['invalidate-depends-goto:neither'] }))}
>
invalidate neither
</button>
77 changes: 77 additions & 0 deletions packages/kit/test/apps/basics/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,83 @@ test.describe('Invalidation', () => {
expect(await page.textContent('p.shared')).toBe(next_shared);
});

test('+page(.server).js is re-run when server dep is invalidated following goto', async ({
page
}) => {
await page.goto('/load/invalidation/depends-goto');
const layout = await page.textContent('p.layout');
const server = await page.textContent('p.server');
const shared = await page.textContent('p.shared');
expect(layout).toBeDefined();
expect(server).toBeDefined();
expect(shared).toBeDefined();

await page.click('button.server');
await page.evaluate(() => window.promise);
const next_layout = await page.textContent('p.layout');
const next_server = await page.textContent('p.server');
const next_shared = await page.textContent('p.shared');
expect(layout).toBe(next_layout);
expect(server).not.toBe(next_server);
expect(shared).not.toBe(next_shared);

await page.click('button.neither');
await page.evaluate(() => window.promise);
expect(await page.textContent('p.layout')).toBe(next_layout);
expect(await page.textContent('p.server')).toBe(next_server);
expect(await page.textContent('p.shared')).toBe(next_shared);
});

test('+page.js is re-run when shared dep is invalidated following goto', async ({ page }) => {
await page.goto('/load/invalidation/depends-goto');
const layout = await page.textContent('p.layout');
const server = await page.textContent('p.server');
const shared = await page.textContent('p.shared');
expect(layout).toBeDefined();
expect(server).toBeDefined();
expect(shared).toBeDefined();

await page.click('button.shared');
await page.evaluate(() => window.promise);
const next_layout = await page.textContent('p.layout');
const next_server = await page.textContent('p.server');
const next_shared = await page.textContent('p.shared');
expect(layout).toBe(next_layout);
expect(server).toBe(next_server);
expect(shared).not.toBe(next_shared);

await page.click('button.neither');
await page.evaluate(() => window.promise);
expect(await page.textContent('p.layout')).toBe(next_layout);
expect(await page.textContent('p.server')).toBe(next_server);
expect(await page.textContent('p.shared')).toBe(next_shared);
});

test('Specified dependencies are re-run following goto', async ({ page }) => {
await page.goto('/load/invalidation/depends-goto');
const layout = await page.textContent('p.layout');
const server = await page.textContent('p.server');
const shared = await page.textContent('p.shared');
expect(layout).toBeDefined();
expect(server).toBeDefined();
expect(shared).toBeDefined();

await page.click('button.specified');
await page.evaluate(() => window.promise);
const next_layout = await page.textContent('p.layout');
const next_server = await page.textContent('p.server');
const next_shared = await page.textContent('p.shared');
expect(layout).not.toBe(next_layout);
expect(server).toBe(next_server);
expect(shared).not.toBe(next_shared);

await page.click('button.neither');
await page.evaluate(() => window.promise);
expect(await page.textContent('p.layout')).toBe(next_layout);
expect(await page.textContent('p.server')).toBe(next_server);
expect(await page.textContent('p.shared')).toBe(next_shared);
});

test('Parameter use is tracked even for routes that do not use the parameters', async ({
page,
clicknav
Expand Down
1 change: 1 addition & 0 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2210,6 +2210,7 @@ declare module '$app/navigation' {
noScroll?: boolean | undefined;
keepFocus?: boolean | undefined;
invalidateAll?: boolean | undefined;
invalidate?: (string | URL | ((url: URL) => boolean))[] | undefined;
state?: App.PageState | undefined;
} | undefined): Promise<void>;
/**
Expand Down

0 comments on commit 04958cc

Please sign in to comment.