Repository: sveltejs/kit
Pull request #6546 (2022-09-03 13:05:54)

Warn about incorrect `data-sveltekit-x` values
#6532. This updates the docs to clarify that `data-sveltekit-prefetch` and `data-sveltekit-prefetch=""` are equivalent, and also adds console errors at dev time if you pass an incorrect value.

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it solves.
- [ ] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: 7e0d2a534f6081a69eb92cd33e6804a2b5a4276b

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name          | Type  |
| ------------- | ----- |
| @sveltejs/kit | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/sveltejs/kit/new/gh-6532?filename=.changeset/short-spiders-pretend.md&value=---%0A%22%40sveltejs%2Fkit%22%3A%20patch%0A---%0A%0AWarn%20about%20incorrect%20%60data-sveltekit-x%60%20values%0A)


netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *kit-demo* processing.


|  Name | Link |
|---------------------------------|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 7e0d2a534f6081a69eb92cd33e6804a2b5a4276b |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/kit-demo/deploys/631351349b67110008811915 |

Patch:
diff --git a/.changeset/breezy-ladybugs-dance.md b/.changeset/breezy-ladybugs-dance.md
new file mode 100644
index 000000000..c15821bb5
--- /dev/null
+++ b/.changeset/breezy-ladybugs-dance.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] change Navigation type to include from/to.params and from/to.routeId
diff --git a/.changeset/spicy-games-trade.md b/.changeset/spicy-games-trade.md
new file mode 100644
index 000000000..bf1225a0a
--- /dev/null
+++ b/.changeset/spicy-games-trade.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Warn about incorrect data-sveltekit-x values
diff --git a/documentation/docs/09-link-options.md b/documentation/docs/09-link-options.md
index b77ab71f7..59ba993b0 100644
--- a/documentation/docs/09-link-options.md
+++ b/documentation/docs/09-link-options.md
@@ -64,3 +64,11 @@ To disable any of these options inside an element where they have been enabled,
 	</div>
 </div>
 ```
+
+To apply an attribute to an element conditionally, do this:
+
+```html
+<div data-sveltekit-reload={shouldReload ? '' : 'off'}>
+```
+
+> This works because in HTML, `<element attribute>` is equivalent to `<element attribute="">`
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 1e61ce2cc..f866a04c1 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -145,7 +145,8 @@ export function create_client({ target, base, trailing_slash }) {
 			const url = new URL(location.href);
 
 			invalidating = Promise.resolve().then(async () => {
-				await update(url, []);
+				const intent = get_navigation_intent(url);
+				await update(intent, url, []);
 
 				invalidating = null;
 				force_invalidation = false;
@@ -200,14 +201,13 @@ export function create_client({ target, base, trailing_slash }) {
 
 	/**
 	 * Returns `true` if update completes, `false` if it is aborted
+	 * @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 {() => void} [callback]
 	 */
-	async function update(url, redirect_chain, opts, callback) {
-		const intent = get_navigation_intent(url);
-
+	async function update(intent, url, redirect_chain, opts, callback) {
 		const current_token = (token = {});
 		let navigation_result = intent && (await load_route(intent));
 
@@ -399,7 +399,15 @@ export function create_client({ target, base, trailing_slash }) {
 		}
 
 		/** @type {import('types').Navigation} */
-		const navigation = { from: null, to: new URL(location.href), type: 'load' };
+		const navigation = {
+			from: null,
+			to: add_url_properties('to', {
+				params: current.params,
+				routeId: current.route?.id ?? null,
+				url: new URL(location.href)
+			}),
+			type: 'load'
+		};
 		callbacks.after_navigate.forEach((fn) => fn(navigation));
 
 		started = true;
@@ -413,7 +421,7 @@ export function create_client({ target, base, trailing_slash }) {
 	 *   branch: Array<import('./types').BranchNode | undefined>;
 	 *   status: number;
 	 *   error: HttpError | Error | null;
-	 *   routeId: string | null;
+	 *   route: import('types').CSRRoute | null;
 	 *   validation_errors?: Record<string, any> | null;
 	 * }} opts
 	 */
@@ -423,7 +431,7 @@ export function create_client({ target, base, trailing_slash }) {
 		branch,
 		status,
 		error,
-		routeId,
+		route,
 		validation_errors
 	}) {
 		const filtered = /** @type {import('./types').BranchNode[] } */ (branch.filter(Boolean));
@@ -436,6 +444,7 @@ export function create_client({ target, base, trailing_slash }) {
 				params,
 				branch,
 				error,
+				route,
 				session_id
 			},
 			props: {
@@ -470,7 +479,7 @@ export function create_client({ target, base, trailing_slash }) {
 			result.props.page = {
 				error,
 				params,
-				routeId,
+				routeId: route && route.id,
 				status,
 				url,
 				// The whole page store is updated, but this way the object reference stays the same
@@ -846,7 +855,7 @@ export function create_client({ target, base, trailing_slash }) {
 									branch: branch.slice(0, j + 1).concat(error_loaded),
 									status,
 									error,
-									routeId: route.id
+									route
 								});
 							} catch (e) {
 								continue;
@@ -872,7 +881,7 @@ export function create_client({ target, base, trailing_slash }) {
 			branch,
 			status: 200,
 			error: null,
-			routeId: route.id
+			route
 		});
 	}
 
@@ -941,7 +950,7 @@ export function create_client({ target, base, trailing_slash }) {
 			branch: [root_layout, root_error],
 			status,
 			error,
-			routeId
+			route: null
 		});
 	}
 
@@ -1000,10 +1009,20 @@ export function create_client({ target, base, trailing_slash }) {
 	}) {
 		let should_block = false;
 
+		const intent = get_navigation_intent(url);
+
 		/** @type {import('types').Navigation} */
 		const navigation = {
-			from: current.url,
-			to: url,
+			from: add_url_properties('from', {
+				params: current.params,
+				routeId: current.route?.id ?? null,
+				url: current.url
+			}),
+			to: add_url_properties('to', {
+				params: intent?.params ?? null,
+				routeId: intent?.route.id ?? null,
+				url
+			}),
 			type
 		};
 
@@ -1034,6 +1053,7 @@ export function create_client({ target, base, trailing_slash }) {
 		}
 
 		await update(
+			intent,
 			url,
 			redirect_chain,
 			{
@@ -1153,7 +1173,11 @@ export function create_client({ target, base, trailing_slash }) {
 
 				/** @type {import('types').Navigation & { cancel: () => void }} */
 				const navigation = {
-					from: current.url,
+					from: add_url_properties('from', {
+						params: current.params,
+						routeId: current.route?.id ?? null,
+						url: current.url
+					}),
 					to: null,
 					type: 'unload',
 					cancel: () => (should_block = true)
@@ -1184,7 +1208,7 @@ export function create_client({ target, base, trailing_slash }) {
 			/** @param {Event} event */
 			const trigger_prefetch = (event) => {
 				const { url, options } = find_anchor(event);
-				if (url && options.prefetch === '') {
+				if (url && options.prefetch) {
 					if (is_external_url(url)) return;
 					prefetch(url);
 				}
@@ -1234,7 +1258,7 @@ export function create_client({ target, base, trailing_slash }) {
 				// 2. 'rel' attribute includes external
 				const rel = (a.getAttribute('rel') || '').split(/\s+/);
 
-				if (a.hasAttribute('download') || rel.includes('external') || options.reload === '') {
+				if (a.hasAttribute('download') || rel.includes('external') || options.reload) {
 					return;
 				}
 
@@ -1260,7 +1284,7 @@ export function create_client({ target, base, trailing_slash }) {
 
 				navigate({
 					url,
-					scroll: options.noscroll === '' ? scroll_state() : null,
+					scroll: options.noscroll ? scroll_state() : null,
 					keepfocus: false,
 					redirect_chain: [],
 					details: {
@@ -1379,7 +1403,7 @@ export function create_client({ target, base, trailing_slash }) {
 						  )
 						: original_error,
 					validation_errors,
-					routeId
+					route: routes.find((route) => route.id === routeId) ?? null
 				});
 			} catch (e) {
 				const error = normalize_error(e);
@@ -1434,3 +1458,37 @@ async function load_data(url, invalid) {
 
 	return server_data;
 }
+
+// TODO remove for 1.0
+const properties = [
+	'hash',
+	'href',
+	'host',
+	'hostname',
+	'origin',
+	'pathname',
+	'port',
+	'protocol',
+	'search',
+	'searchParams',
+	'toString',
+	'toJSON'
+];
+
+/**
+ * @param {'from' | 'to'} type
+ * @param {import('types').NavigationTarget} target
+ */
+function add_url_properties(type, target) {
+	for (const prop of properties) {
+		Object.defineProperty(target, prop, {
+			get() {
+				throw new Error(
+					`The navigation shape changed - ${type}.${prop} should now be ${type}.url.${prop}`
+				);
+			}
+		});
+	}
+
+	return target;
+}
diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts
index f2f5a3617..ad59df44a 100644
--- a/packages/kit/src/runtime/client/types.d.ts
+++ b/packages/kit/src/runtime/client/types.d.ts
@@ -81,10 +81,11 @@ export interface DataNode {
 	uses: Uses;
 }
 
-export type NavigationState = {
+export interface NavigationState {
 	branch: Array<BranchNode | undefined>;
 	error: HttpError | Error | null;
 	params: Record<string, string>;
+	route: CSRRoute | null;
 	session_id: number;
 	url: URL;
-};
+}
diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js
index 1f6a659b1..25d183dd3 100644
--- a/packages/kit/src/runtime/client/utils.js
+++ b/packages/kit/src/runtime/client/utils.js
@@ -27,16 +27,14 @@ export function find_anchor(event) {
 	/** @type {HTMLAnchorElement | SVGAElement | undefined} */
 	let a;
 
-	const options = {
-		/** @type {string | null} */
-		noscroll: null,
+	/** @type {boolean | null} */
+	let noscroll = null;
 
-		/** @type {string | null} */
-		prefetch: null,
+	/** @type {boolean | null} */
+	let prefetch = null;
 
-		/** @type {string | null} */
-		reload: null
-	};
+	/** @type {boolean | null} */
+	let reload = null;
 
 	for (const element of event.composedPath()) {
 		if (!(element instanceof Element)) continue;
@@ -46,22 +44,43 @@ export function find_anchor(event) {
 			a = /** @type {HTMLAnchorElement | SVGAElement} */ (element);
 		}
 
-		if (options.noscroll === null) {
-			options.noscroll = element.getAttribute('data-sveltekit-noscroll');
-		}
+		if (noscroll === null) noscroll = get_link_option(element, 'data-sveltekit-noscroll');
+		if (prefetch === null) prefetch = get_link_option(element, 'data-sveltekit-prefetch');
+		if (reload === null) reload = get_link_option(element, 'data-sveltekit-reload');
+	}
 
-		if (options.prefetch === null) {
-			options.prefetch = element.getAttribute('data-sveltekit-prefetch');
-		}
+	const url = a && new URL(a instanceof SVGAElement ? a.href.baseVal : a.href, document.baseURI);
 
-		if (options.reload === null) {
-			options.reload = element.getAttribute('data-sveltekit-reload');
+	return {
+		a,
+		url,
+		options: {
+			noscroll,
+			prefetch,
+			reload
 		}
-	}
+	};
+}
 
-	const url = a && new URL(a instanceof SVGAElement ? a.href.baseVal : a.href, document.baseURI);
+const warned = new WeakSet();
+
+/**
+ * @param {Element} element
+ * @param {string} attribute
+ */
+function get_link_option(element, attribute) {
+	const value = element.getAttribute(attribute);
+	if (value === null) return value;
+
+	if (value === '') return true;
+	if (value === 'off') return false;
+
+	if (__SVELTEKIT_DEV__ && !warned.has(element)) {
+		console.error(`Unexpected value for ${attribute} — should be "" or "off"`, element);
+		warned.add(element);
+	}
 
-	return { a, url, options };
+	return false;
 }
 
 /** @param {any} value */
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index 7eb746c2d..c09238532 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -219,11 +219,17 @@ export interface LoadEvent<
 	depends: (...deps: string[]) => void;
 }
 
+export interface NavigationTarget {
+	params: Record<string, string> | null;
+	routeId: string | null;
+	url: URL;
+}
+
 export type NavigationType = 'load' | 'unload' | 'link' | 'goto' | 'popstate';
 
 export interface Navigation {
-	from: URL | null;
-	to: URL | null;
+	from: NavigationTarget | null;
+	to: NavigationTarget | null;
 	type: NavigationType;
 	delta?: number;
 }


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte b/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte
index 36560e6ef..70209b704 100644
--- a/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte
@@ -1,10 +1,10 @@
 <script>
 	import { afterNavigate } from '$app/navigation';
 
-	/** @type {URL | null} */
+	/** @type {import('@sveltejs/kit').NavigationTarget | null} */
 	let from;
 
-	/** @type {URL} */
+	/** @type {import('@sveltejs/kit').NavigationTarget} */
 	let to;
 
 	afterNavigate((navigation) => {
@@ -13,5 +13,5 @@
 	});
 </script>
 
-<h1>{from?.pathname} -> {to?.pathname}</h1>
+<h1>{from?.url.pathname} -> {to?.url.pathname}</h1>
 <a href="/after-navigate/b">/b</a>
diff --git a/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte b/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte
index c1e43e302..3db6a4022 100644
--- a/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte
@@ -1,10 +1,10 @@
 <script>
 	import { afterNavigate } from '$app/navigation';
 
-	/** @type {URL | null} */
+	/** @type {import('@sveltejs/kit').NavigationTarget | null} */
 	let from;
 
-	/** @type {URL} */
+	/** @type {import('@sveltejs/kit').NavigationTarget} */
 	let to;
 
 	afterNavigate((navigation) => {
@@ -13,5 +13,5 @@
 	});
 </script>
 
-<h1>{from?.pathname} -> {to?.pathname}</h1>
+<h1>{from?.url.pathname} -> {to?.url.pathname}</h1>
 <a href="/after-navigate/a">/a</a>
diff --git a/packages/kit/test/apps/basics/src/routes/store/navigating/+layout.svelte b/packages/kit/test/apps/basics/src/routes/store/navigating/+layout.svelte
index 706de9b32..c15660dbf 100644
--- a/packages/kit/test/apps/basics/src/routes/store/navigating/+layout.svelte
+++ b/packages/kit/test/apps/basics/src/routes/store/navigating/+layout.svelte
@@ -11,7 +11,7 @@
 <div id="nav-status">
 	{#if $navigating}
 		<p id="navigating">
-			navigating from {$navigating.from.pathname} to {$navigating.to.pathname} ({$navigating.type})
+			navigating from {$navigating.from.url.pathname} to {$navigating.to.url.pathname} ({$navigating.type})
 		</p>
 	{:else}
 		<p id="not-navigating">not currently navigating</p>
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 56f873e66..1c1237820 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -11,12 +11,8 @@ test.describe('beforeNavigate', () => {
 	test('prevents navigation triggered by link click', async ({ clicknav, page, baseURL }) => {
 		await page.goto('/before-navigate/prevent-navigation');
 
-		try {
-			await clicknav('[href="/before-navigate/a"]', { timeout: 1000 });
-			expect(false).toBe(true);
-		} catch (/** @type {any} */ e) {
-			expect(e.message).toMatch('page.waitForNavigation: Timeout 1000ms exceeded');
-		}
+		await page.click('[href="/before-navigate/a"]');
+		await page.waitForLoadState('networkidle');
 
 		expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
 		expect(await page.innerHTML('pre')).toBe('true');
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 789ffd4e9..4455f6423 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -1153,11 +1153,12 @@ test.describe('$app/stores', () => {
 			await page.waitForSelector('#not-navigating');
 			expect(await page.textContent('#nav-status')).toBe('not currently navigating');
 
-			page.goBack();
-			await page.waitForSelector('#navigating');
-			expect(await page.textContent('#navigating')).toBe(
-				'navigating from /store/navigating/b to /store/navigating/a (popstate)'
-			);
+			await Promise.all([
+				expect(page.locator('#navigating')).toHaveText(
+					'navigating from /store/navigating/b to /store/navigating/a (popstate)'
+				),
+				page.goBack()
+			]);
 		}
 	});