Skip to content

Commit 33108cc

Browse files
[breaking] add embedded option, turned off by default (#7969)
* [breaking] add embedded option, turned off by default Closes #7940 Adds a new option which defaults to false. If true, events related to navigation etc are listened to on the target element rather the html root * fix test * support #4935 * tests * docs * filter start options Co-authored-by: Rich Harris <[email protected]>
1 parent b082905 commit 33108cc

File tree

18 files changed

+106
-26
lines changed

18 files changed

+106
-26
lines changed

.changeset/popular-crabs-shop.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
[breaking] add embedded option, turned off by default

packages/kit/src/core/config/index.spec.js

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const get_defaults = (prefix = '') => ({
7676
checkOrigin: true
7777
},
7878
endpointExtensions: undefined,
79+
embedded: false,
7980
env: {
8081
dir: process.cwd(),
8182
publicPrefix: 'PUBLIC_'

packages/kit/src/core/config/options.js

+2
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ const options = object(
129129
checkOrigin: boolean(true)
130130
}),
131131

132+
embedded: boolean(false),
133+
132134
// TODO: remove this for the 1.0 release
133135
endpointExtensions: error(
134136
(keypath) => `${keypath} has been renamed to config.kit.moduleExtensions`

packages/kit/src/exports/vite/build/build_server.js

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export class Server {
6666
check_origin: ${s(config.kit.csrf.checkOrigin)},
6767
},
6868
dev: false,
69+
embedded: ${config.kit.embedded},
6970
handle_error: (error, event) => {
7071
return this.options.hooks.handleError({
7172
error,

packages/kit/src/exports/vite/build/utils.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ export function get_default_build_config({ config, input, ssr, outDir }) {
147147
__SVELTEKIT_APP_VERSION_FILE__: JSON.stringify(`${config.kit.appDir}/version.json`),
148148
__SVELTEKIT_APP_VERSION_POLL_INTERVAL__: JSON.stringify(config.kit.version.pollInterval),
149149
__SVELTEKIT_BROWSER__: ssr ? 'false' : 'true',
150-
__SVELTEKIT_DEV__: 'false'
150+
__SVELTEKIT_DEV__: 'false',
151+
__SVELTEKIT_EMBEDDED__: config.kit.embedded ? 'true' : 'false'
151152
},
152153
publicDir: ssr ? false : config.kit.files.assets,
153154
resolve: {

packages/kit/src/exports/vite/dev/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ export async function dev(vite, vite_config, svelte_config) {
449449
check_origin: svelte_config.kit.csrf.checkOrigin
450450
},
451451
dev: true,
452+
embedded: svelte_config.kit.embedded,
452453
handle_error: async (error, event) => {
453454
const error_object = await hooks.handleError({
454455
error: new Proxy(error, {

packages/kit/src/exports/vite/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,8 @@ function kit({ svelte_config }) {
262262
define: {
263263
__SVELTEKIT_APP_VERSION_POLL_INTERVAL__: '0',
264264
__SVELTEKIT_BROWSER__: config_env.ssrBuild ? 'false' : 'true',
265-
__SVELTEKIT_DEV__: 'true'
265+
__SVELTEKIT_DEV__: 'true',
266+
__SVELTEKIT_EMBEDDED__: svelte_config.kit.embedded ? 'true' : 'false'
266267
},
267268
publicDir: svelte_config.kit.files.assets,
268269
resolve: {

packages/kit/src/runtime/client/client.js

+24-9
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ function check_for_removed_attributes() {
8989
* @returns {import('./types').Client}
9090
*/
9191
export function create_client({ target, base }) {
92+
const container = __SVELTEKIT_EMBEDDED__ ? target : document.documentElement;
9293
/** @type {Array<((url: URL) => boolean)>} */
9394
const invalidated = [];
9495

@@ -1199,7 +1200,7 @@ export function create_client({ target, base }) {
11991200
/** @type {NodeJS.Timeout} */
12001201
let mousemove_timeout;
12011202

1202-
target.addEventListener('mousemove', (event) => {
1203+
container.addEventListener('mousemove', (event) => {
12031204
const target = /** @type {Element} */ (event.target);
12041205

12051206
clearTimeout(mousemove_timeout);
@@ -1213,8 +1214,8 @@ export function create_client({ target, base }) {
12131214
preload(/** @type {Element} */ (event.composedPath()[0]), 1);
12141215
}
12151216

1216-
target.addEventListener('mousedown', tap);
1217-
target.addEventListener('touchstart', tap, { passive: true });
1217+
container.addEventListener('mousedown', tap);
1218+
container.addEventListener('touchstart', tap, { passive: true });
12181219

12191220
const observer = new IntersectionObserver(
12201221
(entries) => {
@@ -1233,7 +1234,7 @@ export function create_client({ target, base }) {
12331234
* @param {number} priority
12341235
*/
12351236
function preload(element, priority) {
1236-
const a = find_anchor(element, target);
1237+
const a = find_anchor(element, container);
12371238
if (!a) return;
12381239

12391240
const { url, external } = get_link_info(a, base);
@@ -1253,7 +1254,7 @@ export function create_client({ target, base }) {
12531254
function after_navigate() {
12541255
observer.disconnect();
12551256

1256-
for (const a of target.querySelectorAll('a')) {
1257+
for (const a of container.querySelectorAll('a')) {
12571258
const { url, external } = get_link_info(a, base);
12581259
if (external) continue;
12591260

@@ -1457,14 +1458,14 @@ export function create_client({ target, base }) {
14571458
}
14581459

14591460
/** @param {MouseEvent} event */
1460-
target.addEventListener('click', (event) => {
1461+
container.addEventListener('click', (event) => {
14611462
// Adapted from https://github.com/visionmedia/page.js
14621463
// MIT license https://github.com/visionmedia/page.js#license
14631464
if (event.button || event.which !== 1) return;
14641465
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
14651466
if (event.defaultPrevented) return;
14661467

1467-
const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), target);
1468+
const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), container);
14681469
if (!a) return;
14691470

14701471
const { url, external, has } = get_link_info(a, base);
@@ -1534,7 +1535,7 @@ export function create_client({ target, base }) {
15341535
});
15351536
});
15361537

1537-
target.addEventListener('submit', (event) => {
1538+
container.addEventListener('submit', (event) => {
15381539
if (event.defaultPrevented) return;
15391540

15401541
const form = /** @type {HTMLFormElement} */ (
@@ -1639,11 +1640,25 @@ export function create_client({ target, base }) {
16391640
});
16401641
},
16411642

1642-
_hydrate: async ({ status, error, node_ids, params, route, data: server_data_nodes, form }) => {
1643+
_hydrate: async ({
1644+
status = 200,
1645+
error,
1646+
node_ids,
1647+
params,
1648+
route,
1649+
data: server_data_nodes,
1650+
form
1651+
}) => {
16431652
hydrated = true;
16441653

16451654
const url = new URL(location.href);
16461655

1656+
if (!__SVELTEKIT_EMBEDDED__) {
1657+
// See https://github.com/sveltejs/kit/pull/4935#issuecomment-1328093358 for one motivation
1658+
// of determining the params on the client side.
1659+
({ params = {}, route = { id: null } } = get_navigation_intent(url, false) || {});
1660+
}
1661+
16471662
/** @type {import('./types').NavigationFinished | undefined} */
16481663
let result;
16491664

packages/kit/src/runtime/server/page/render.js

+30-13
Original file line numberDiff line numberDiff line change
@@ -266,24 +266,41 @@ export async function render_response({
266266
}
267267

268268
if (page_config.csr) {
269+
const opts = [
270+
`env: ${s(options.public_env)}`,
271+
`paths: ${s(options.paths)}`,
272+
`target: document.querySelector('[data-sveltekit-hydrate="${target}"]').parentNode`,
273+
`version: ${s(options.version)}`
274+
];
275+
276+
if (page_config.ssr) {
277+
const hydrate = [
278+
`node_ids: [${branch.map(({ node }) => node.index).join(', ')}]`,
279+
`data: ${serialized.data}`,
280+
`form: ${serialized.form}`
281+
];
282+
283+
if (status !== 200) {
284+
hydrate.push(`status: ${status}`);
285+
}
286+
287+
if (error) {
288+
hydrate.push(`error: ${devalue.uneval(error)}`);
289+
}
290+
291+
if (options.embedded) {
292+
hydrate.push(`params: ${devalue.uneval(event.params)}`, `route: ${s(event.route)}`);
293+
}
294+
295+
opts.push(`hydrate: {\n\t\t\t\t\t${hydrate.join(',\n\t\t\t\t\t')}\n\t\t\t\t}`);
296+
}
297+
269298
// prettier-ignore
270299
const init_app = `
271300
import { start } from ${s(prefixed(entry.file))};
272301
273302
start({
274-
env: ${s(options.public_env)},
275-
hydrate: ${page_config.ssr ? `{
276-
status: ${status},
277-
error: ${devalue.uneval(error)},
278-
node_ids: [${branch.map(({ node }) => node.index).join(', ')}],
279-
params: ${devalue.uneval(event.params)},
280-
route: ${s(event.route)},
281-
data: ${serialized.data},
282-
form: ${serialized.form}
283-
}` : 'null'},
284-
paths: ${s(options.paths)},
285-
target: document.querySelector('[data-sveltekit-hydrate="${target}"]').parentNode,
286-
version: ${s(options.version)}
303+
${opts.join(',\n\t\t\t\t')}
287304
});
288305
`;
289306

packages/kit/test/apps/basics/test/client.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -809,11 +809,11 @@ test.describe('Routing', () => {
809809
expect(await page.textContent('h1')).toBe('hello');
810810
});
811811

812-
test('ignores clicks outside the app target', async ({ page }) => {
812+
test('recognizes clicks outside the app target', async ({ page }) => {
813813
await page.goto('/routing/link-outside-app-target/source');
814814

815815
await page.click('[href="/routing/link-outside-app-target/target"]');
816-
expect(await page.textContent('h1')).toBe('target: 0');
816+
await expect(page.locator('h1')).toHaveText('target: 1');
817817
});
818818

819819
test('responds to <form method="GET"> submission without reload', async ({ page }) => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
import { onMount } from 'svelte';
3+
import { increment, count } from '../state.js';
4+
5+
onMount(increment);
6+
</script>
7+
8+
<h2>source: {count}</h2>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export let count = 0;
2+
3+
export function increment() {
4+
count += 1;
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
import { count } from '../state.js';
3+
</script>
4+
5+
<h2>target: {count}</h2>

packages/kit/test/apps/options/source/template.html

+1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
<body>
99
<h1>I am in the template</h1>
1010
<div>%sveltekit.body%</div>
11+
<a href="/path-base/routing/link-outside-app-target/target">outside app target</a>
1112
</body>
1213
</html>

packages/kit/test/apps/options/svelte.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const config = {
33
extensions: ['.jesuslivesineveryone', '.whokilledthemuffinman', '.svelte.md', '.svelte'],
44
kit: {
5+
embedded: true,
56
csp: {
67
directives: {
78
'script-src': ['self']

packages/kit/test/apps/options/test/test.js

+9
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,12 @@ test.describe('Vite options', () => {
229229
expect(await page.textContent('h2')).toBe(`${mode} === ${mode} === ${mode}`);
230230
});
231231
});
232+
233+
test.describe('Routing', () => {
234+
test('ignores clicks outside the app target', async ({ page }) => {
235+
await page.goto('/path-base/routing/link-outside-app-target/source');
236+
237+
await page.click('[href="/path-base/routing/link-outside-app-target/target"]');
238+
await expect(page.locator('h2')).toHaveText('target: 0');
239+
});
240+
});

packages/kit/types/index.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,11 @@ export interface KitConfig {
325325
*/
326326
checkOrigin?: boolean;
327327
};
328+
/**
329+
* Whether or not the app is embedded inside a larger app. If `true`, SvelteKit will add its event listeners related to navigation etc on the parent of `%sveltekit.body%` instead of `window`, and will pass `params` from the server rather than inferring them from `location.pathname`.
330+
* @default false
331+
*/
332+
embedded?: boolean;
328333
/**
329334
* Environment variable configuration
330335
*/

packages/kit/types/internal.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ export interface SSROptions {
299299
check_origin: boolean;
300300
};
301301
dev: boolean;
302+
embedded: boolean;
302303
handle_error(error: Error & { frame?: string }, event: RequestEvent): MaybePromise<App.Error>;
303304
hooks: ServerHooks;
304305
manifest: SSRManifest;
@@ -387,4 +388,5 @@ declare global {
387388
const __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: number;
388389
const __SVELTEKIT_BROWSER__: boolean;
389390
const __SVELTEKIT_DEV__: boolean;
391+
const __SVELTEKIT_EMBEDDED__: boolean;
390392
}

0 commit comments

Comments
 (0)