fix: add `$set` and `$on` methods in legacy compat mode

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/10642/head
Simon Holthausen 3 years ago
parent ee4b1f2c75
commit 957c21ec37

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

@ -245,7 +245,11 @@ export function client_component(source, analysis, options) {
return b.get(alias ?? name, [b.return(expression)]); return b.get(alias ?? name, [b.return(expression)]);
}); });
if (analysis.accessors) { if (
analysis.accessors ||
// because $set method needs accessors
options.legacy.componentApi
) {
for (const [name, binding] of analysis.instance.scope.declarations) { for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind !== 'prop' || name.startsWith('$$')) continue; if (binding.kind !== 'prop' || name.startsWith('$$')) continue;
@ -258,6 +262,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