diff --git a/.changeset/chilly-rocks-hug.md b/.changeset/chilly-rocks-hug.md new file mode 100644 index 000000000000..cccc87da3289 --- /dev/null +++ b/.changeset/chilly-rocks-hug.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +feat: introduce `$host` rune, deprecate `createEventDispatcher` diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index a2ad6d63af9a..f923aace5ab1 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -211,3 +211,24 @@ declare function $bindable(t?: T): T; declare function $inspect( ...values: T ): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void }; + +/** + * Retrieves the `this` reference of the custom element that contains this component. Example: + * + * ```svelte + * + * + * + * + * + * ``` + * + * Only available inside custom element components, and only on the client-side. + * + * https://svelte-5-preview.vercel.app/docs/runes#$host + */ +declare function $host(): El; diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 318225be95ae..c72a3bd0080c 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -187,6 +187,8 @@ const runes = { 'invalid-state-location': (rune) => `${rune}(...) can only be used as a variable declaration initializer or a class field`, 'invalid-effect-location': () => `$effect() can only be used as an expression statement`, + 'invalid-host-location': () => + `$host() can only be used inside custom element component instances`, /** * @param {boolean} is_binding * @param {boolean} show_details diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 69d879d021a9..ed840f530315 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -896,6 +896,9 @@ export const validation_runes_js = { } }, CallExpression(node, { state, path }) { + if (get_rune(node, state.scope) === '$host') { + error(node, 'invalid-host-location'); + } validate_call_expression(node, state.scope, path); }, VariableDeclarator(node, { state }) { @@ -1063,9 +1066,17 @@ export const validation_runes = merge(validation, a11y_validators, { } }, CallExpression(node, { state, path }) { - if (get_rune(node, state.scope) === '$bindable' && node.arguments.length > 1) { + const rune = get_rune(node, state.scope); + if (rune === '$bindable' && node.arguments.length > 1) { error(node, 'invalid-rune-args-length', '$bindable', [0, 1]); + } else if (rune === '$host') { + if (node.arguments.length > 0) { + error(node, 'invalid-rune-args-length', '$host', [0]); + } else if (state.ast_type === 'module' || !state.analysis.custom_element) { + error(node, 'invalid-host-location'); + } } + validate_call_expression(node, state.scope, path); }, EachBlock(node, { next, state }) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 8c88a405f114..83c32b01de20 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -401,15 +401,12 @@ export function client_component(source, analysis, options) { } if (analysis.uses_props || analysis.uses_rest_props) { + const to_remove = [b.literal('children'), b.literal('$$slots'), b.literal('$$events')]; + if (analysis.custom_element) { + to_remove.push(b.literal('$$host')); + } component_block.body.unshift( - b.const( - '$$sanitized_props', - b.call( - '$.rest_props', - b.id('$$props'), - b.array([b.literal('children'), b.literal('$$slots'), b.literal('$$events')]) - ) - ) + b.const('$$sanitized_props', b.call('$.rest_props', b.id('$$props'), b.array(to_remove))) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index 25a2564453e0..7fcb9a92c690 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -381,6 +381,10 @@ export const javascript_visitors_runes = { CallExpression(node, context) { const rune = get_rune(node, context.state.scope); + if (rune === '$host') { + return b.id('$$props.$$host'); + } + if (rune === '$effect.active') { return b.call('$.effect_active'); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 19c626ae58a9..e9638ea17daf 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -767,6 +767,10 @@ const javascript_visitors_runes = { CallExpression(node, context) { const rune = get_rune(node, context.state.scope); + if (rune === '$host') { + return b.id('undefined'); + } + if (rune === '$effect.active') { return b.literal(false); } diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index 822174d4dd1a..c61015e92a40 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -40,7 +40,8 @@ export const Runes = /** @type {const} */ ([ '$effect.active', '$effect.root', '$inspect', - '$inspect().with' + '$inspect().with', + '$host' ]); /** diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 2b2203745712..215a3379ff90 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -80,6 +80,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false * ``` * * https://svelte.dev/docs/svelte#createeventdispatcher + * @deprecated Use callback props and/or the `$host()` rune instead — see https://svelte-5-preview.vercel.app/docs/deprecations#createeventdispatcher * @template {Record} [EventMap = any] * @returns {import('./index.js').EventDispatcher} */ diff --git a/packages/svelte/src/internal/client/dom/elements/custom-element.js b/packages/svelte/src/internal/client/dom/elements/custom-element.js index 5ea42a962827..eb47d674c21d 100644 --- a/packages/svelte/src/internal/client/dom/elements/custom-element.js +++ b/packages/svelte/src/internal/client/dom/elements/custom-element.js @@ -138,7 +138,8 @@ if (typeof HTMLElement === 'function') { target: this.shadowRoot || this, props: { ...this.$$d, - $$slots + $$slots, + $$host: this } }); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/_config.js new file mode 100644 index 000000000000..f848528c6c47 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'invalid-host-location', + message: '$host() can only be used inside custom element component instances' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/main.svelte new file mode 100644 index 000000000000..d4ac5ced65a1 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/main.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/main.svelte.js new file mode 100644 index 000000000000..b7e4b4aee3cd --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-host-placement/main.svelte.js @@ -0,0 +1 @@ +$host(); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/_config.js new file mode 100644 index 000000000000..0c2625dc204e --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/_config.js @@ -0,0 +1,24 @@ +import { test } from '../../assert'; +const tick = () => Promise.resolve(); + +export default test({ + async test({ assert, target }) { + target.innerHTML = ''; + /** @type {any} */ + const el = target.querySelector('custom-element'); + + /** @type {string[]} */ + const events = []; + const handle_evt = (e) => events.push(e.type, e.detail); + el.addEventListener('greeting', handle_evt); + + await tick(); + + el.shadowRoot.querySelector('button').click(); + assert.deepEqual(events, ['greeting', 'hello']); + + el.removeEventListener('greeting', handle_evt); + el.shadowRoot.querySelector('button').click(); + assert.deepEqual(events, ['greeting', 'hello']); + } +}); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/main.svelte new file mode 100644 index 000000000000..080e7a2740ce --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/main.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 22406fe281b9..3abbe35d6951 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -263,6 +263,7 @@ declare module 'svelte' { * ``` * * https://svelte.dev/docs/svelte#createeventdispatcher + * @deprecated Use callback props and/or the `$host()` rune instead — see https://svelte-5-preview.vercel.app/docs/deprecations#createeventdispatcher * */ export function createEventDispatcher = any>(): EventDispatcher; /** @@ -2538,4 +2539,25 @@ declare function $inspect( ...values: T ): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void }; +/** + * Retrieves the `this` reference of the custom element that contains this component. Example: + * + * ```svelte + * + * + * + * + * + * ``` + * + * Only available inside custom element components, and only on the client-side. + * + * https://svelte-5-preview.vercel.app/docs/runes#$host + */ +declare function $host(): El; + //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index 3b24ad004e60..51ba3e2155fe 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -561,6 +561,26 @@ $inspect(stuff).with(console.trace); > `$inspect` only works during development. +## `$host` + +Retrieves the `this` reference of the custom element that contains this component. Example: + +```svelte + + + + + +``` + +> Only available inside custom element components, and only on the client-side + ## How to opt in Current Svelte code will continue to work without any adjustments. Components using the Svelte 4 syntax can use components using runes and vice versa. diff --git a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/03-deprecations.md b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/03-deprecations.md index b498862c4737..b4a170ccb76e 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/03-deprecations.md +++ b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/03-deprecations.md @@ -40,6 +40,50 @@ These functions run indiscriminately when _anything_ changes. By using `$effect. Note that using `$effect` and `$effect.pre` will put you in [runes mode](/docs/runes) — be sure to update your props and state accordingly. +## `createEventDispatcher` + +`createEventDispatcher` returns a function from which you can dispatch custom events. The usage is somewhat boilerplate-y, but it was encouraged in Svelte 4 due to consistency with how you listen to dom events (via `on:click` for example). + +Svelte 5 introduces [event attributes](/docs/event-handlers) which deprecate event directives (`onclick` instead of `on:click`), and as such we also encourage you to use callback properties for events instead: + +```diff + + + +``` + +When authoring custom elements, use the new [host rune](/docs/runes#$host) to dispatch events (among other things): + +```diff + + + +``` + +Note that using `$props` and `$host` will put you in [runes mode](/docs/runes) — be sure to update your props and state accordingly. + ## `immutable` The `immutable` compiler option is deprecated. Use runes mode instead, where all state is immutable (which means that assigning to `object.property` won't cause updates for anything that is observing `object` itself, or a different property of it).