Skip to content

Commit 749d3aa

Browse files
authored
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
1 parent a4a789d commit 749d3aa

File tree

8 files changed

+133
-2
lines changed

8 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/2-analyze/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,11 @@ export function analyze_component(root, options) {
357357
uses_component_bindings: false,
358358
custom_element: options.customElement,
359359
inject_styles: options.css === 'injected' || !!options.customElement,
360-
accessors: options.customElement ? true : !!options.accessors,
360+
accessors: options.customElement
361+
? true
362+
: !!options.accessors ||
363+
// because $set method needs accessors
364+
!!options.legacy?.componentApi,
361365
reactive_statements: new Map(),
362366
binding_groups: new Map(),
363367
slot_names: new Set(),

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,60 @@ export function client_component(source, analysis, options) {
258258
}
259259
}
260260

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

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)