fix: add `$set` and `$on` methods in legacy compat mode (#10642)

People could've done bind:this and called instance methods on the instance - a rare case, but not impossible. This shims $set and $on when in legacy compat mode. $destroy is never shimmed because you shouldn't manually destroy a component, ever, and there's no way to make that work in the new world.
closes #10420
pull/10654/head
Simon H 10 months ago committed by GitHub
parent a4a789db4d
commit 749d3aa413
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: add `$set` and `$on` methods in legacy compat mode

@ -357,7 +357,11 @@ export function analyze_component(root, options) {
uses_component_bindings: false, uses_component_bindings: false,
custom_element: options.customElement, custom_element: options.customElement,
inject_styles: options.css === 'injected' || !!options.customElement, inject_styles: options.css === 'injected' || !!options.customElement,
accessors: options.customElement ? true : !!options.accessors, accessors: options.customElement
? true
: !!options.accessors ||
// because $set method needs accessors
!!options.legacy?.componentApi,
reactive_statements: new Map(), reactive_statements: new Map(),
binding_groups: new Map(), binding_groups: new Map(),
slot_names: new Set(), slot_names: new Set(),

@ -258,6 +258,60 @@ export function client_component(source, analysis, options) {
} }
} }
if (options.legacy.componentApi) {
properties.push(
b.init('$set', b.id('$.update_legacy_props')),
b.init(
'$on',
b.arrow(
[b.id('$$event_name'), b.id('$$event_cb')],
b.call(
'$.add_legacy_event_listener',
b.id('$$props'),
b.id('$$event_name'),
b.id('$$event_cb')
)
)
)
);
} else if (options.dev) {
properties.push(
b.init(
'$set',
b.thunk(
b.block([
b.throw_error(
`The component shape you get when doing bind:this changed. Updating its properties via $set is no longer valid in Svelte 5. ` +
'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information'
)
])
)
),
b.init(
'$on',
b.thunk(
b.block([
b.throw_error(
`The component shape you get when doing bind:this changed. Listening to events via $on is no longer valid in Svelte 5. ` +
'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information'
)
])
)
),
b.init(
'$destroy',
b.thunk(
b.block([
b.throw_error(
`The component shape you get when doing bind:this changed. Destroying such a component via $destroy is no longer valid in Svelte 5. ` +
'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information'
)
])
)
)
);
}
const push_args = [b.id('$$props'), b.literal(analysis.runes)]; const push_args = [b.id('$$props'), b.literal(analysis.runes)];
if (options.dev) push_args.push(b.id(analysis.name)); if (options.dev) push_args.push(b.id(analysis.name));

@ -2929,3 +2929,30 @@ export function bubble_event($$props, event) {
fn.call(this, event); fn.call(this, event);
} }
} }
/**
* Used to simulate `$on` on a component instance when `legacy.componentApi` is `true`
* @param {Record<string, any>} $$props
* @param {string} event_name
* @param {Function} event_callback
*/
export function add_legacy_event_listener($$props, event_name, event_callback) {
$$props.$$events ||= {};
$$props.$$events[event_name] ||= [];
$$props.$$events[event_name].push(event_callback);
}
/**
* Used to simulate `$set` on a component instance when `legacy.componentApi` is `true`.
* Needs component accessors so that it can call the setter of the prop. Therefore doesn't
* work for updating props in `$$props` or `$$restProps`.
* @this {Record<string, any>}
* @param {Record<string, any>} $$new_props
*/
export function update_legacy_props($$new_props) {
for (const key in $$new_props) {
if (key in this) {
this[key] = $$new_props[key];
}
}
}

@ -0,0 +1,17 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
legacy: {
componentApi: true
}
},
html: '<button>0</button>',
async test({ assert, target }) {
const button = target.querySelector('button');
await button?.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>1</button>');
}
});

@ -0,0 +1,16 @@
<script>
import Sub from './sub.svelte';
import { onMount } from 'svelte';
let count = 0;
let component;
onMount(() => {
component.$on('increment', (e) => {
count += e.detail;
component.$set({ count });
});
});
</script>
<Sub bind:this={component} />

@ -0,0 +1,8 @@
<script>
import { createEventDispatcher } from 'svelte';
export let count = 0;
const dispatch = createEventDispatcher();
</script>
<button on:click={() => dispatch('increment', 1)}>{count}</button>

@ -70,7 +70,7 @@ import App from './App.svelte'
export default app; export default app;
``` ```
If this component is not under your control, you can use the `legacy.componentApi` compiler option for auto-applied backwards compatibility (note that this adds a bit of overhead to each component). If this component is not under your control, you can use the `legacy.componentApi` compiler option for auto-applied backwards compatibility (note that this adds a bit of overhead to each component). This will also add `$set` and `$on` methods for all component instances you get through `bind:this`.
### Server API changes ### Server API changes

Loading…
Cancel
Save