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

feat: introduce $host rune, deprecate createEventDispatcher #11059

Merged
merged 3 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chilly-rocks-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

feat: introduce `$host` rune, deprecate `createEventDispatcher`
21 changes: 21 additions & 0 deletions packages/svelte/src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

takes a generic because people could add their own properties to the element when using the extend feature of the customElement option and then want type-safety using it

2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion packages/svelte/src/compiler/phases/2-analyze/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down Expand Up @@ -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 }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/compiler/phases/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export const Runes = /** @type {const} */ ([
'$effect.active',
'$effect.root',
'$inspect',
'$inspect().with'
'$inspect().with',
'$host'
]);

/**
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/index-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ if (typeof HTMLElement === 'function') {
target: this.shadowRoot || this,
props: {
...this.$$d,
$$slots
$$slots,
$$host: this
}
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script>
$host();
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$host();
Original file line number Diff line number Diff line change
@@ -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']);
}
});
Original file line number Diff line number Diff line change
@@ -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>
22 changes: 22 additions & 0 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EventMap extends Record<string, any> = any>(): EventDispatcher<EventMap>;
/**
Expand Down Expand Up @@ -2538,4 +2539,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
20 changes: 20 additions & 0 deletions sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only available within client-side custom element components.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This raises an interesting question — what even is a server-side custom element component? What is supposed to get rendered? Should we be emitting declarative shadow DOM?

Are there any situations where $host() could be reached in a server environment? Instead of undefined, should it be a function call that throws a useful error (perhaps only in dev?)

Copy link
Member Author

@dummdidumm dummdidumm Apr 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling $host() doesn't necessarily mean that you access one of its properties right away. You could save this to a variable. So an error could have false positives.

The question what we should be doing on the server is interesting indeed - I'm not even sure what would happen today if you do that. It's a larger/separate conversation IMO

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about a proxy that threw on property access? That way if someone did something like

const parent = $host().parentNode;

they would get a more useful error than Cannot read properties of undefined (reading 'parentNode') or whatever

Copy link
Member Author

@dummdidumm dummdidumm Apr 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could have unintended consequences in the case of checking if host is defined when writing code that is eagerly executed on the server


## 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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