Skip to content

Commit 957c21e

Browse files
committed
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
1 parent ee4b1f2 commit 957c21e

File tree

7 files changed

+133
-2
lines changed

7 files changed

+133
-2
lines changed

.changeset/tough-radios-punch.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+
fix: add `$set` and `$on` methods in legacy compat mode

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

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,11 @@ export function client_component(source, analysis, options) {
245245
return b.get(alias ?? name, [b.return(expression)]);
246246
});
247247

248-
if (analysis.accessors) {
248+
if (
249+
analysis.accessors ||
250+
// because $set method needs accessors
251+
options.legacy.componentApi
252+
) {
249253
for (const [name, binding] of analysis.instance.scope.declarations) {
250254
if (binding.kind !== 'prop' || name.startsWith('$$')) continue;
251255

@@ -258,6 +262,60 @@ export function client_component(source, analysis, options) {
258262
}
259263
}
260264

265+
if (options.legacy.componentApi) {
266+
properties.push(
267+
b.init('$set', b.id('$.update_legacy_props')),
268+
b.init(
269+
'$on',
270+
b.arrow(
271+
[b.id('$$event_name'), b.id('$$event_cb')],
272+
b.call(
273+
'$.add_legacy_event_listener',
274+
b.id('$$props'),
275+
b.id('$$event_name'),
276+
b.id('$$event_cb')
277+
)
278+
)
279+
)
280+
);
281+
} else if (options.dev) {
282+
properties.push(
283+
b.init(
284+
'$set',
285+
b.thunk(
286+
b.block([
287+
b.throw_error(
288+
`The component shape you get when doing bind:this changed. Updating its properties via $set is no longer valid in Svelte 5. ` +
289+
'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information'
290+
)
291+
])
292+
)
293+
),
294+
b.init(
295+
'$on',
296+
b.thunk(
297+
b.block([
298+
b.throw_error(
299+
`The component shape you get when doing bind:this changed. Listening to events via $on is no longer valid in Svelte 5. ` +
300+
'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information'
301+
)
302+
])
303+
)
304+
),
305+
b.init(
306+
'$destroy',
307+
b.thunk(
308+
b.block([
309+
b.throw_error(
310+
`The component shape you get when doing bind:this changed. Destroying such a component via $destroy is no longer valid in Svelte 5. ` +
311+
'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information'
312+
)
313+
])
314+
)
315+
)
316+
);
317+
}
318+
261319
const push_args = [b.id('$$props'), b.literal(analysis.runes)];
262320
if (options.dev) push_args.push(b.id(analysis.name));
263321

packages/svelte/src/internal/client/render.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2929,3 +2929,30 @@ export function bubble_event($$props, event) {
29292929
fn.call(this, event);
29302930
}
29312931
}
2932+
2933+
/**
2934+
* Used to simulate `$on` on a component instance when `legacy.componentApi` is `true`
2935+
* @param {Record<string, any>} $$props
2936+
* @param {string} event_name
2937+
* @param {Function} event_callback
2938+
*/
2939+
export function add_legacy_event_listener($$props, event_name, event_callback) {
2940+
$$props.$$events ||= {};
2941+
$$props.$$events[event_name] ||= [];
2942+
$$props.$$events[event_name].push(event_callback);
2943+
}
2944+
2945+
/**
2946+
* Used to simulate `$set` on a component instance when `legacy.componentApi` is `true`.
2947+
* Needs component accessors so that it can call the setter of the prop. Therefore doesn't
2948+
* work for updating props in `$$props` or `$$restProps`.
2949+
* @this {Record<string, any>}
2950+
* @param {Record<string, any>} $$new_props
2951+
*/
2952+
export function update_legacy_props($$new_props) {
2953+
for (const key in $$new_props) {
2954+
if (key in this) {
2955+
this[key] = $$new_props[key];
2956+
}
2957+
}
2958+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
compileOptions: {
6+
legacy: {
7+
componentApi: true
8+
}
9+
},
10+
html: '<button>0</button>',
11+
async test({ assert, target }) {
12+
const button = target.querySelector('button');
13+
await button?.click();
14+
await tick();
15+
assert.htmlEqual(target.innerHTML, '<button>1</button>');
16+
}
17+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script>
2+
import Sub from './sub.svelte';
3+
import { onMount } from 'svelte';
4+
5+
let count = 0;
6+
let component;
7+
8+
onMount(() => {
9+
component.$on('increment', (e) => {
10+
count += e.detail;
11+
component.$set({ count });
12+
});
13+
});
14+
</script>
15+
16+
<Sub bind:this={component} />
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
import { createEventDispatcher } from 'svelte';
3+
4+
export let count = 0;
5+
const dispatch = createEventDispatcher();
6+
</script>
7+
8+
<button on:click={() => dispatch('increment', 1)}>{count}</button>

sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ import App from './App.svelte'
7070
export default app;
7171
```
7272

73-
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).
73+
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`.
7474

7575
### Server API changes
7676

0 commit comments

Comments
 (0)