Skip to content

Commit 22494be

Browse files
authored
feat: introduce $host rune, deprecate createEventDispatcher (#11059)
closes #11022
1 parent 8578857 commit 22494be

File tree

18 files changed

+189
-11
lines changed

18 files changed

+189
-11
lines changed

.changeset/chilly-rocks-hug.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
feat: introduce `$host` rune, deprecate `createEventDispatcher`

packages/svelte/src/ambient.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,24 @@ declare function $bindable<T>(t?: T): T;
211211
declare function $inspect<T extends any[]>(
212212
...values: T
213213
): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void };
214+
215+
/**
216+
* Retrieves the `this` reference of the custom element that contains this component. Example:
217+
*
218+
* ```svelte
219+
* <svelte:options customElement="my-element" />
220+
*
221+
* <script>
222+
* function greet(greeting) {
223+
* $host().dispatchEvent(new CustomEvent('greeting', { detail: greeting }))
224+
* }
225+
* </script>
226+
*
227+
* <button onclick={() => greet('hello')}>say hello</button>
228+
* ```
229+
*
230+
* Only available inside custom element components, and only on the client-side.
231+
*
232+
* https://svelte-5-preview.vercel.app/docs/runes#$host
233+
*/
234+
declare function $host<El extends HTMLElement = HTMLElement>(): El;

packages/svelte/src/compiler/errors.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ const runes = {
187187
'invalid-state-location': (rune) =>
188188
`${rune}(...) can only be used as a variable declaration initializer or a class field`,
189189
'invalid-effect-location': () => `$effect() can only be used as an expression statement`,
190+
'invalid-host-location': () =>
191+
`$host() can only be used inside custom element component instances`,
190192
/**
191193
* @param {boolean} is_binding
192194
* @param {boolean} show_details

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,9 @@ export const validation_runes_js = {
896896
}
897897
},
898898
CallExpression(node, { state, path }) {
899+
if (get_rune(node, state.scope) === '$host') {
900+
error(node, 'invalid-host-location');
901+
}
899902
validate_call_expression(node, state.scope, path);
900903
},
901904
VariableDeclarator(node, { state }) {
@@ -1063,9 +1066,17 @@ export const validation_runes = merge(validation, a11y_validators, {
10631066
}
10641067
},
10651068
CallExpression(node, { state, path }) {
1066-
if (get_rune(node, state.scope) === '$bindable' && node.arguments.length > 1) {
1069+
const rune = get_rune(node, state.scope);
1070+
if (rune === '$bindable' && node.arguments.length > 1) {
10671071
error(node, 'invalid-rune-args-length', '$bindable', [0, 1]);
1072+
} else if (rune === '$host') {
1073+
if (node.arguments.length > 0) {
1074+
error(node, 'invalid-rune-args-length', '$host', [0]);
1075+
} else if (state.ast_type === 'module' || !state.analysis.custom_element) {
1076+
error(node, 'invalid-host-location');
1077+
}
10681078
}
1079+
10691080
validate_call_expression(node, state.scope, path);
10701081
},
10711082
EachBlock(node, { next, state }) {

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -399,15 +399,12 @@ export function client_component(source, analysis, options) {
399399
}
400400

401401
if (analysis.uses_props || analysis.uses_rest_props) {
402+
const to_remove = [b.literal('children'), b.literal('$$slots'), b.literal('$$events')];
403+
if (analysis.custom_element) {
404+
to_remove.push(b.literal('$$host'));
405+
}
402406
component_block.body.unshift(
403-
b.const(
404-
'$$sanitized_props',
405-
b.call(
406-
'$.rest_props',
407-
b.id('$$props'),
408-
b.array([b.literal('children'), b.literal('$$slots'), b.literal('$$events')])
409-
)
410-
)
407+
b.const('$$sanitized_props', b.call('$.rest_props', b.id('$$props'), b.array(to_remove)))
411408
);
412409
}
413410

packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,10 @@ export const javascript_visitors_runes = {
380380
CallExpression(node, context) {
381381
const rune = get_rune(node, context.state.scope);
382382

383+
if (rune === '$host') {
384+
return b.id('$$props.$$host');
385+
}
386+
383387
if (rune === '$effect.active') {
384388
return b.call('$.effect_active');
385389
}

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,10 @@ const javascript_visitors_runes = {
785785
CallExpression(node, context) {
786786
const rune = get_rune(node, context.state.scope);
787787

788+
if (rune === '$host') {
789+
return b.id('undefined');
790+
}
791+
788792
if (rune === '$effect.active') {
789793
return b.literal(false);
790794
}

packages/svelte/src/compiler/phases/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export const Runes = /** @type {const} */ ([
4040
'$effect.active',
4141
'$effect.root',
4242
'$inspect',
43-
'$inspect().with'
43+
'$inspect().with',
44+
'$host'
4445
]);
4546

4647
/**

packages/svelte/src/index-client.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
8080
* ```
8181
*
8282
* https://svelte.dev/docs/svelte#createeventdispatcher
83+
* @deprecated Use callback props and/or the `$host()` rune instead — see https://svelte-5-preview.vercel.app/docs/deprecations#createeventdispatcher
8384
* @template {Record<string, any>} [EventMap = any]
8485
* @returns {import('./index.js').EventDispatcher<EventMap>}
8586
*/

packages/svelte/src/internal/client/dom/elements/custom-element.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ if (typeof HTMLElement === 'function') {
138138
target: this.shadowRoot || this,
139139
props: {
140140
...this.$$d,
141-
$$slots
141+
$$slots,
142+
$$host: this
142143
}
143144
});
144145

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
error: {
5+
code: 'invalid-host-location',
6+
message: '$host() can only be used inside custom element component instances'
7+
}
8+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<script>
2+
$host();
3+
</script>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
$host();
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { test } from '../../assert';
2+
const tick = () => Promise.resolve();
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
target.innerHTML = '<custom-element></custom-element>';
7+
/** @type {any} */
8+
const el = target.querySelector('custom-element');
9+
10+
/** @type {string[]} */
11+
const events = [];
12+
const handle_evt = (e) => events.push(e.type, e.detail);
13+
el.addEventListener('greeting', handle_evt);
14+
15+
await tick();
16+
17+
el.shadowRoot.querySelector('button').click();
18+
assert.deepEqual(events, ['greeting', 'hello']);
19+
20+
el.removeEventListener('greeting', handle_evt);
21+
el.shadowRoot.querySelector('button').click();
22+
assert.deepEqual(events, ['greeting', 'hello']);
23+
}
24+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<svelte:options customElement="custom-element" />
2+
3+
<script>
4+
function greet(greeting) {
5+
$host().dispatchEvent(new CustomEvent('greeting', { detail: greeting }))
6+
}
7+
</script>
8+
9+
<button onclick={() => greet('hello')}>say hello</button>

packages/svelte/types/index.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ declare module 'svelte' {
266266
* ```
267267
*
268268
* https://svelte.dev/docs/svelte#createeventdispatcher
269+
* @deprecated Use callback props and/or the `$host()` rune instead — see https://svelte-5-preview.vercel.app/docs/deprecations#createeventdispatcher
269270
* */
270271
export function createEventDispatcher<EventMap extends Record<string, any> = any>(): EventDispatcher<EventMap>;
271272
/**
@@ -2690,4 +2691,25 @@ declare function $inspect<T extends any[]>(
26902691
...values: T
26912692
): { with: (fn: (type: 'init' | 'update', ...values: T) => void) => void };
26922693

2694+
/**
2695+
* Retrieves the `this` reference of the custom element that contains this component. Example:
2696+
*
2697+
* ```svelte
2698+
* <svelte:options customElement="my-element" />
2699+
*
2700+
* <script>
2701+
* function greet(greeting) {
2702+
* $host().dispatchEvent(new CustomEvent('greeting', { detail: greeting }))
2703+
* }
2704+
* </script>
2705+
*
2706+
* <button onclick={() => greet('hello')}>say hello</button>
2707+
* ```
2708+
*
2709+
* Only available inside custom element components, and only on the client-side.
2710+
*
2711+
* https://svelte-5-preview.vercel.app/docs/runes#$host
2712+
*/
2713+
declare function $host<El extends HTMLElement = HTMLElement>(): El;
2714+
26932715
//# sourceMappingURL=index.d.ts.map

sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,26 @@ $inspect(stuff).with(console.trace);
561561
562562
> `$inspect` only works during development.
563563
564+
## `$host`
565+
566+
Retrieves the `this` reference of the custom element that contains this component. Example:
567+
568+
```svelte
569+
<svelte:options customElement="my-element" />
570+
571+
<script>
572+
function greet(greeting) {
573+
$host().dispatchEvent(
574+
new CustomEvent('greeting', { detail: greeting })
575+
);
576+
}
577+
</script>
578+
579+
<button onclick={() => greet('hello')}>say hello</button>
580+
```
581+
582+
> Only available inside custom element components, and only on the client-side
583+
564584
## How to opt in
565585
566586
Current Svelte code will continue to work without any adjustments. Components using the Svelte 4 syntax can use components using runes and vice versa.

sites/svelte-5-preview/src/routes/docs/content/03-appendix/03-deprecations.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,50 @@ These functions run indiscriminately when _anything_ changes. By using `$effect.
4040

4141
Note that using `$effect` and `$effect.pre` will put you in [runes mode](/docs/runes) — be sure to update your props and state accordingly.
4242

43+
## `createEventDispatcher`
44+
45+
`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).
46+
47+
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:
48+
49+
```diff
50+
<script>
51+
- import { createEventDispatcher } from 'svelte';
52+
- const dispatch = createEventDispatcher();
53+
+ let { greet } = $props();
54+
55+
- function greet() {
56+
- dispatch('greet');
57+
- }
58+
</script>
59+
60+
<button
61+
- on:click={greet}
62+
+ onclick={greet}
63+
>greet</button>
64+
```
65+
66+
When authoring custom elements, use the new [host rune](/docs/runes#$host) to dispatch events (among other things):
67+
68+
```diff
69+
<script>
70+
- import { createEventDispatcher } from 'svelte';
71+
- const dispatch = createEventDispatcher();
72+
73+
function greet() {
74+
- dispatch('greet');
75+
+ $host().dispatchEvent(new CustomEvent('greet'));
76+
}
77+
</script>
78+
79+
<button
80+
- on:click={greet}
81+
+ onclick={greet}
82+
>greet</button>
83+
```
84+
85+
Note that using `$props` and `$host` will put you in [runes mode](/docs/runes) — be sure to update your props and state accordingly.
86+
4387
## `immutable`
4488

4589
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).

0 commit comments

Comments
 (0)