feat: introduce `$host` rune, deprecate `createEventDispatcher` (#11059)

closes #11022
pull/11093/head
Simon H 9 months ago committed by GitHub
parent 8578857332
commit 22494be9ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: introduce `$host` rune, deprecate `createEventDispatcher`

@ -211,3 +211,24 @@ declare function $bindable<T>(t?: T): T;
declare function $inspect<T extends any[]>(
...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
* <svelte:options customElement="my-element" />
*
* <script>
* function greet(greeting) {
* $host().dispatchEvent(new CustomEvent('greeting', { detail: greeting }))
* }
* </script>
*
* <button onclick={() => greet('hello')}>say hello</button>
* ```
*
* 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 extends HTMLElement = HTMLElement>(): El;

@ -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

@ -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 }) {

@ -399,15 +399,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)))
);
}

@ -380,6 +380,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');
}

@ -785,6 +785,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);
}

@ -40,7 +40,8 @@ export const Runes = /** @type {const} */ ([
'$effect.active',
'$effect.root',
'$inspect',
'$inspect().with'
'$inspect().with',
'$host'
]);
/**

@ -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<string, any>} [EventMap = any]
* @returns {import('./index.js').EventDispatcher<EventMap>}
*/

@ -138,7 +138,8 @@ if (typeof HTMLElement === 'function') {
target: this.shadowRoot || this,
props: {
...this.$$d,
$$slots
$$slots,
$$host: this
}
});

@ -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'
}
});

@ -0,0 +1,24 @@
import { test } from '../../assert';
const tick = () => Promise.resolve();
export default test({
async test({ assert, target }) {
target.innerHTML = '<custom-element></custom-element>';
/** @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']);
}
});

@ -0,0 +1,9 @@
<svelte:options customElement="custom-element" />
<script>
function greet(greeting) {
$host().dispatchEvent(new CustomEvent('greeting', { detail: greeting }))
}
</script>
<button onclick={() => greet('hello')}>say hello</button>

@ -266,6 +266,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<EventMap extends Record<string, any> = any>(): EventDispatcher<EventMap>;
/**
@ -2690,4 +2691,25 @@ declare function $inspect<T extends any[]>(
...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
* <svelte:options customElement="my-element" />
*
* <script>
* function greet(greeting) {
* $host().dispatchEvent(new CustomEvent('greeting', { detail: greeting }))
* }
* </script>
*
* <button onclick={() => greet('hello')}>say hello</button>
* ```
*
* 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 extends HTMLElement = HTMLElement>(): El;
//# sourceMappingURL=index.d.ts.map

@ -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
<svelte:options customElement="my-element" />
<script>
function greet(greeting) {
$host().dispatchEvent(
new CustomEvent('greeting', { detail: greeting })
);
}
</script>
<button onclick={() => greet('hello')}>say hello</button>
```
> 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.

@ -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
<script>
- import { createEventDispatcher } from 'svelte';
- const dispatch = createEventDispatcher();
+ let { greet } = $props();
- function greet() {
- dispatch('greet');
- }
</script>
<button
- on:click={greet}
+ onclick={greet}
>greet</button>
```
When authoring custom elements, use the new [host rune](/docs/runes#$host) to dispatch events (among other things):
```diff
<script>
- import { createEventDispatcher } from 'svelte';
- const dispatch = createEventDispatcher();
function greet() {
- dispatch('greet');
+ $host().dispatchEvent(new CustomEvent('greet'));
}
</script>
<button
- on:click={greet}
+ onclick={greet}
>greet</button>
```
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).

Loading…
Cancel
Save