Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply data-sveltekit-* attributes to children #6442

Merged
merged 14 commits into from
Aug 31, 2022
5 changes: 5 additions & 0 deletions .changeset/breezy-timers-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Apply `data-sveltekit-prefetch/noscroll/reload` to all child `<a>` elements
5 changes: 5 additions & 0 deletions .changeset/fresh-lemons-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-svelte': patch
---

Move `data-sveltekit-prefetch` to `<nav>` element
2 changes: 1 addition & 1 deletion documentation/docs/00-introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ title: Introduction

SvelteKit is a framework for building extremely high-performance web apps.

Building an app with all the modern best practices is fiendishly complicated. Those practices include [build optimizations](https://vitejs.dev/guide/features.html#build-optimizations), so that you load only the minimal required code; [offline support](/docs/service-workers); [prefetching](/docs/a-options#sveltekit-prefetch) pages before the user initiates navigation; and [configurable rendering](/docs/page-options) that allows you to render your app [on the server](/docs/appendix#ssr) or [in the browser](/docs/appendix#csr) at runtime or [at build-time](/docs/page-options#prerender). SvelteKit does all the boring stuff for you so that you can get on with the creative part.
Building an app with all the modern best practices is fiendishly complicated. Those practices include [build optimizations](https://vitejs.dev/guide/features.html#build-optimizations), so that you load only the minimal required code; [offline support](/docs/service-workers); [prefetching](/docs/link-options#data-sveltekit-prefetch) pages before the user initiates navigation; and [configurable rendering](/docs/page-options) that allows you to render your app [on the server](/docs/appendix#ssr) or [in the browser](/docs/appendix#csr) at runtime or [at build-time](/docs/page-options#prerender). SvelteKit does all the boring stuff for you so that you can get on with the creative part.

It uses [Vite](https://vitejs.dev/) with a [Svelte plugin](https://github.com/sveltejs/vite-plugin-svelte) to provide a lightning-fast and feature-rich development experience with [Hot Module Replacement (HMR)](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md#hot), where changes to your code are reflected in the browser instantly.

Expand Down
45 changes: 0 additions & 45 deletions documentation/docs/09-a-options.md

This file was deleted.

66 changes: 66 additions & 0 deletions documentation/docs/09-link-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Link options
---

In SvelteKit, `<a>` elements (rather than framework-specific `<Link>` components) are used to navigate between the routes of your app. If the user clicks on a link whose `href` is 'owned' by the app (as opposed to, say, a link to an external site) then SvelteKit will navigate to the new page by importing its code and then calling any `load` functions it needs to fetch data.

You can customise the behaviour of links with `data-sveltekit-*` attributes. These can be applied to the `<a>` itself, or to a parent element.

### data-sveltekit-prefetch

To get a head start on importing the code and fetching the page's data, use the `data-sveltekit-prefetch` attribute, which will start loading everything as soon as the user hovers over the link (on a desktop) or touches it (on mobile), rather than waiting for the `click` event to trigger navigation. Typically, this buys us an extra couple of hundred milliseconds, which is the difference between a user interface that feels laggy, and one that feels snappy.

To apply this behaviour across the board, add the attribute to a parent element (or even the `<body>` in your `src/app.html`):

```html
/// file: src/routes/+layout.svelte
<main data-sveltekit-prefetch>
<slot />
</main>
```

> You can also programmatically invoke `prefetch` from `$app/navigation`.

### data-sveltekit-reload

Occasionally, we need to tell SvelteKit not to handle a link, but allow the browser to handle it. Adding a `data-sveltekit-reload` attribute to a link...

```html
<a data-sveltekit-reload href="/path">Path</a>
```

...will cause a full-page navigation when the link is clicked.

Links with a `rel="external"` attribute will receive the same treatment. In addition, they will be ignored during [prerendering](/docs/page-options#prerender).

Conduitry marked this conversation as resolved.
Show resolved Hide resolved
### data-sveltekit-noscroll

When navigating to internal links, SvelteKit mirrors the browser's default navigation behaviour: it will change the scroll position to 0,0 so that the user is at the very top left of the page (unless the link includes a `#hash`, in which case it will scroll to the element with a matching ID).

In certain cases, you may wish to disable this behaviour. Adding a `data-sveltekit-noscroll` attribute to a link...

```html
<a href="path" data-sveltekit-noscroll>Path</a>
```

...will prevent scrolling after the link is clicked.

### Disabling options

To disable any of these options inside an element where they have been enabled, use the `"off"` value:

```html
<div data-sveltekit-prefetch>
<!-- these links will be prefetched -->
<a href="/a">a</a>
<a href="/b">b</a>
<a href="/c">c</a>

<div data-sveltekit-prefetch="off">
<!-- these links will NOT be prefetched -->
<a href="/d">d</a>
<a href="/e">e</a>
<a href="/f">f</a>
</div>
</div>
```
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@
</a>
</div>

<nav>
<nav data-sveltekit-prefetch>
<svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
</svg>
<ul>
<li class:active={$page.url.pathname === '/'}>
<a data-sveltekit-prefetch href="/">Home</a>
<a href="/">Home</a>
</li>
<li class:active={$page.url.pathname === '/about'}>
<a data-sveltekit-prefetch href="/about">About</a>
<a href="/about">About</a>
</li>
<li class:active={$page.url.pathname === '/todos'}>
<a data-sveltekit-prefetch href="/todos">Todos</a>
<a href="/todos">Todos</a>
</li>
</ul>
<svg viewBox="0 0 2 3" aria-hidden="true">
Expand Down
23 changes: 8 additions & 15 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { onMount, tick } from 'svelte';
import { normalize_error } from '../../utils/error.js';
import { make_trackable, decode_params, normalize_path } from '../../utils/url.js';
import { find_anchor, get_base_uri, get_href, scroll_state } from './utils.js';
import { find_anchor, get_base_uri, scroll_state } from './utils.js';
import { lock_fetch, unlock_fetch, initial_fetch, native_fetch } from './fetcher.js';
import { parse } from './parse.js';
import { error } from '../../exports/index.js';
Expand Down Expand Up @@ -1139,9 +1139,9 @@ export function create_client({ target, base, trailing_slash }) {

/** @param {Event} event */
const trigger_prefetch = (event) => {
const a = find_anchor(event);
if (a && a.href && a.hasAttribute('data-sveltekit-prefetch')) {
prefetch(get_href(a));
const { url, options } = find_anchor(event);
if (url && options.prefetch === '') {
prefetch(url);
}
};

Expand Down Expand Up @@ -1172,13 +1172,10 @@ export function create_client({ target, base, trailing_slash }) {
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
if (event.defaultPrevented) return;

const a = find_anchor(event);
if (!a) return;

if (!a.href) return;
const { a, url, options } = find_anchor(event);
if (!a || !url) return;

const is_svg_a_element = a instanceof SVGAElement;
const url = get_href(a);

// Ignore non-HTTP URL protocols (e.g. `mailto:`, `tel:`, `myapp:`, etc.)
// MEMO: Without this condition, firefox will open mailer twice.
Expand All @@ -1192,11 +1189,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') ||
a.hasAttribute('data-sveltekit-reload')
) {
if (a.hasAttribute('download') || rel.includes('external') || options.reload === '') {
return;
}

Expand All @@ -1222,7 +1215,7 @@ export function create_client({ target, base, trailing_slash }) {

navigate({
url,
scroll: a.hasAttribute('data-sveltekit-noscroll') ? scroll_state() : null,
scroll: options.noscroll === '' ? scroll_state() : null,
keepfocus: false,
redirect_chain: [],
details: {
Expand Down
47 changes: 37 additions & 10 deletions packages/kit/src/runtime/client/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,44 @@ export function scroll_state() {

/** @param {Event} event */
export function find_anchor(event) {
const node = event
.composedPath()
.find((e) => e instanceof Node && e.nodeName.toUpperCase() === 'A'); // SVG <a> elements have a lowercase name
return /** @type {HTMLAnchorElement | SVGAElement | undefined} */ (node);
}
/** @type {HTMLAnchorElement | SVGAElement | undefined} */
let a;

const options = {
/** @type {string | null} */
noscroll: null,

/** @type {string | null} */
prefetch: null,

/** @type {string | null} */
reload: null
};

for (const element of event.composedPath()) {
if (!(element instanceof Element)) continue;

if (!a && element.nodeName.toUpperCase() === 'A') {
// SVG <a> elements have a lowercase name
a = /** @type {HTMLAnchorElement | SVGAElement} */ (element);
}

if (options.noscroll === null) {
options.noscroll = element.getAttribute('data-sveltekit-noscroll');
}

if (options.prefetch === null) {
options.prefetch = element.getAttribute('data-sveltekit-prefetch');
}

if (options.reload === null) {
options.reload = element.getAttribute('data-sveltekit-reload');
}
}

const url = a && new URL(a instanceof SVGAElement ? a.href.baseVal : a.href, document.baseURI);

/** @param {HTMLAnchorElement | SVGAElement} node */
export function get_href(node) {
return node instanceof SVGAElement
? new URL(node.href.baseVal, document.baseURI)
: new URL(node.href);
return { a, url, options };
}

/** @param {any} value */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div style="height: 2000px; background: palegoldenrod" />

<a id="one" href="/data-sveltekit/noscroll/target" data-sveltekit-noscroll>one</a>

<div data-sveltekit-noscroll>
<a id="two" href="/data-sveltekit/noscroll/target">two</a>
<a id="three" href="/data-sveltekit/noscroll/target" data-sveltekit-noscroll="off">three</a>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div style="height: 2000px; background: palegoldenrod" />

<h1>target</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<a id="one" href="/data-sveltekit/prefetch/target" data-sveltekit-prefetch>one</a>

<div data-sveltekit-prefetch>
<a id="two" href="/data-sveltekit/prefetch/target">two</a>
<a id="three" href="/data-sveltekit/prefetch/target" data-sveltekit-prefetch="off">three</a>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>target</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<a id="one" href="/data-sveltekit/reload/target" data-sveltekit-reload>one</a>

<div data-sveltekit-reload>
<a id="two" href="/data-sveltekit/reload/target">two</a>
<a id="three" href="/data-sveltekit/reload/target" data-sveltekit-reload="off">three</a>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>target</h1>
72 changes: 72 additions & 0 deletions packages/kit/test/apps/basics/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -697,3 +697,75 @@ test.describe.serial('Invalidation', () => {
expect(await page.textContent('h1')).toBe('a: 4, b: 5');
});
});

test.describe('data-sveltekit attributes', () => {
test('data-sveltekit-prefetch', async ({ baseURL, page }) => {
const requests = [];
page.on('request', (r) => requests.push(r.url()));

const module = process.env.DEV
? `${baseURL}/src/routes/data-sveltekit/prefetch/target/+page.svelte`
: `${baseURL}/_app/immutable/components/pages/data-sveltekit/prefetch/target/_page`;

await page.goto('/data-sveltekit/prefetch');
await page.locator('#one').dispatchEvent('mousemove');
await Promise.all([
page.waitForTimeout(100), // wait for prefetching to start
page.waitForLoadState('networkidle') // wait for prefetching to finish
]);
expect(requests.find((r) => r.startsWith(module))).toBeDefined();

requests.length = 0;
await page.goto('/data-sveltekit/prefetch');
await page.locator('#two').dispatchEvent('mousemove');
await Promise.all([
page.waitForTimeout(100), // wait for prefetching to start
page.waitForLoadState('networkidle') // wait for prefetching to finish
]);
expect(requests.find((r) => r.startsWith(module))).toBeDefined();

requests.length = 0;
await page.goto('/data-sveltekit/prefetch');
await page.locator('#three').dispatchEvent('mousemove');
await Promise.all([
page.waitForTimeout(100), // wait for prefetching to start
page.waitForLoadState('networkidle') // wait for prefetching to finish
]);
expect(requests.find((r) => r.startsWith(module))).toBeUndefined();
});

test('data-sveltekit-reload', async ({ baseURL, page, clicknav }) => {
const requests = [];
page.on('request', (r) => requests.push(r.url()));

await page.goto('/data-sveltekit/reload');
await page.click('#one');
expect(requests).toContain(`${baseURL}/data-sveltekit/reload/target`);

requests.length = 0;
await page.goto('/data-sveltekit/reload');
await page.click('#two');
expect(requests).toContain(`${baseURL}/data-sveltekit/reload/target`);

requests.length = 0;
await page.goto('/data-sveltekit/reload');
await clicknav('#three');
expect(requests).not.toContain(`${baseURL}/data-sveltekit/reload/target`);
});

test('data-sveltekit-noscroll', async ({ page, clicknav }) => {
await page.goto('/data-sveltekit/noscroll');
// await page.evaluate(() => window.scrollTo(0, 1000));
await clicknav('#one');
expect(await page.evaluate(() => window.scrollY)).toBeGreaterThan(1000);

await page.goto('/data-sveltekit/noscroll');
await clicknav('#two');
expect(await page.evaluate(() => window.scrollY)).toBeGreaterThan(1000);

await page.goto('/data-sveltekit/noscroll');
// await page.evaluate(() => window.scrollTo(0, 1000));
await clicknav('#three');
expect(await page.evaluate(() => window.scrollY)).toBe(0);
});
});