Skip to content

Commit 6796ec6

Browse files
committed
feat: enable snippets to fill slots
This allows people to use snippets to fill slots. It is implemented in the same way the default slot interop is already implemented, by passing a boolean to the hidden `$$slots` object, and using that at runtime to determine the correct outcome. The impact on bundle size is neglible. By enabling this, we can enhance our migration script to always transform slot usages (including `let:x` etc) to snippets. This wasn't possible before because we couldn't be sure if the other side was transformed to using render tags at the same time. This will be part of #13419. This is important because currently the migration script is transforming `<slot />` creations inside components, but since it's not touching its usage points the migration will make your app end up in a broken state which you have to finish by hand. This is a reduced alternative to, and closes #11619, which was also enabling the other way around, but that is a) not as necessary and b) more likely to confuse people / break, because it only works if your render function has 0-1 arguments.
1 parent d87bf17 commit 6796ec6

File tree

12 files changed

+77
-38
lines changed

12 files changed

+77
-38
lines changed

.changeset/neat-ways-allow.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+
feat: enable snippets to fill slots

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { BlockStatement, Expression, ExpressionStatement, Property } from 'estree' */
1+
/** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
import * as b from '../../../../utils/builders.js';
@@ -23,7 +23,7 @@ export function SlotElement(node, context) {
2323

2424
let is_default = true;
2525

26-
/** @type {Expression} */
26+
/** @type {Literal} */
2727
let name = b.literal('default');
2828

2929
for (const attribute of node.attributes) {
@@ -33,7 +33,7 @@ export function SlotElement(node, context) {
3333
const { value } = build_attribute_value(attribute.value, context);
3434

3535
if (attribute.name === 'name') {
36-
name = value;
36+
name = /** @type {Literal} */ (value);
3737
is_default = false;
3838
} else if (attribute.name !== 'slot') {
3939
if (attribute.metadata.expression.has_state) {
@@ -58,10 +58,14 @@ export function SlotElement(node, context) {
5858
? b.literal(null)
5959
: b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)));
6060

61-
const expression = is_default
62-
? b.call('$.default_slot', b.id('$$props'))
63-
: b.member(b.member(b.id('$$props'), '$$slots'), name, true, true);
61+
const slot = b.call(
62+
'$.slot',
63+
context.state.node,
64+
b.id('$$props'),
65+
name,
66+
props_expression,
67+
fallback
68+
);
6469

65-
const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback);
6670
context.state.init.push(b.stmt(slot));
6771
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ export function build_component(node, component_name, context, anchor = context.
215215

216216
/** @type {Statement[]} */
217217
const snippet_declarations = [];
218+
/** @type {import('estree').Property[]} */
219+
const serialized_slots = [];
218220

219221
// Group children by slot
220222
for (const child of node.fragment.nodes) {
@@ -229,6 +231,9 @@ export function build_component(node, component_name, context, anchor = context.
229231

230232
push_prop(b.prop('init', child.expression, child.expression));
231233

234+
// Interop: allows people to pass snippets when component still uses slots
235+
serialized_slots.push(b.init(child.expression.name, b.true));
236+
232237
continue;
233238
}
234239

@@ -238,8 +243,6 @@ export function build_component(node, component_name, context, anchor = context.
238243
}
239244

240245
// Serialize each slot
241-
/** @type {Property[]} */
242-
const serialized_slots = [];
243246
for (const slot_name of Object.keys(children)) {
244247
const block = /** @type {BlockStatement} */ (
245248
context.visit(

packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { BlockStatement, Expression, Property } from 'estree' */
1+
/** @import { BlockStatement, Expression, Literal, Property } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types.js' */
44
import * as b from '../../../../utils/builders.js';
@@ -15,8 +15,8 @@ export function SlotElement(node, context) {
1515
/** @type {Expression[]} */
1616
const spreads = [];
1717

18-
/** @type {Expression} */
19-
let expression = b.call('$.default_slot', b.id('$$props'));
18+
/** @type {Literal} */
19+
let name = b.literal('default');
2020

2121
for (const attribute of node.attributes) {
2222
if (attribute.type === 'SpreadAttribute') {
@@ -25,7 +25,7 @@ export function SlotElement(node, context) {
2525
const value = build_attribute_value(attribute.value, context, false, true);
2626

2727
if (attribute.name === 'name') {
28-
expression = b.member(b.member_id('$$props.$$slots'), value, true, true);
28+
name = /** @type {Literal} */ (value);
2929
} else if (attribute.name !== 'slot') {
3030
props.push(b.init(attribute.name, value));
3131
}
@@ -42,7 +42,14 @@ export function SlotElement(node, context) {
4242
? b.literal(null)
4343
: b.thunk(/** @type {BlockStatement} */ (context.visit(node.fragment)));
4444

45-
const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback);
45+
const slot = b.call(
46+
'$.slot',
47+
b.id('$$payload'),
48+
b.id('$$props'),
49+
name,
50+
props_expression,
51+
fallback
52+
);
4653

4754
context.state.template.push(empty_comment, b.stmt(slot), empty_comment);
4855
}

packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ export function build_inline_component(node, expression, context) {
101101

102102
/** @type {Statement[]} */
103103
const snippet_declarations = [];
104+
/** @type {Property[]} */
105+
const serialized_slots = [];
104106

105107
// Group children by slot
106108
for (const child of node.fragment.nodes) {
@@ -115,6 +117,9 @@ export function build_inline_component(node, expression, context) {
115117

116118
push_prop(b.prop('init', child.expression, child.expression));
117119

120+
// Interop: allows people to pass snippets when component still uses slots
121+
serialized_slots.push(b.init(child.expression.name, b.true));
122+
118123
continue;
119124
}
120125

@@ -142,9 +147,6 @@ export function build_inline_component(node, expression, context) {
142147
}
143148

144149
// Serialize each slot
145-
/** @type {Property[]} */
146-
const serialized_slots = [];
147-
148150
for (const slot_name of Object.keys(children)) {
149151
const block = /** @type {BlockStatement} */ (
150152
context.visit(

packages/svelte/src/internal/client/dom/blocks/slot.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,30 @@ import { hydrate_next, hydrating } from '../hydration.js';
22

33
/**
44
* @param {Comment} anchor
5-
* @param {void | ((anchor: Comment, slot_props: Record<string, unknown>) => void)} slot_fn
5+
* @param {Record<string, any>} $$props
6+
* @param {string} name
67
* @param {Record<string, unknown>} slot_props
78
* @param {null | ((anchor: Comment) => void)} fallback_fn
89
*/
9-
export function slot(anchor, slot_fn, slot_props, fallback_fn) {
10+
export function slot(anchor, $$props, name, slot_props, fallback_fn) {
1011
if (hydrating) {
1112
hydrate_next();
1213
}
1314

15+
var slot_fn = $$props.$$slots?.[name];
16+
// Interop: Can use snippets to fill slots
17+
var is_interop = false;
18+
if (slot_fn === true) {
19+
slot_fn = $$props[name === 'default' ? 'children' : name];
20+
is_interop = true;
21+
}
22+
1423
if (slot_fn === undefined) {
1524
if (fallback_fn !== null) {
1625
fallback_fn(anchor);
1726
}
1827
} else {
19-
slot_fn(anchor, slot_props);
28+
slot_fn(anchor, is_interop ? () => slot_props : slot_props);
2029
}
2130
}
2231

packages/svelte/src/internal/client/dom/legacy/misc.js

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,3 @@ export function update_legacy_props($$new_props) {
6666
}
6767
}
6868
}
69-
70-
/**
71-
* @param {Record<string, any>} $$props
72-
*/
73-
export function default_slot($$props) {
74-
var children = $$props.$$slots?.default;
75-
if (children === true) {
76-
return $$props.children;
77-
} else {
78-
return children;
79-
}
80-
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ export {
8080
add_legacy_event_listener,
8181
bubble_event,
8282
reactive_import,
83-
update_legacy_props,
84-
default_slot
83+
update_legacy_props
8584
} from './dom/legacy/misc.js';
8685
export {
8786
append,

packages/svelte/src/internal/server/index.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -405,12 +405,19 @@ export function unsubscribe_stores(store_values) {
405405

406406
/**
407407
* @param {Payload} payload
408-
* @param {void | ((payload: Payload, props: Record<string, unknown>) => void)} slot_fn
408+
* @param {Record<string, any>} $$props
409+
* @param {string} name
409410
* @param {Record<string, unknown>} slot_props
410411
* @param {null | (() => void)} fallback_fn
411412
* @returns {void}
412413
*/
413-
export function slot(payload, slot_fn, slot_props, fallback_fn) {
414+
export function slot(payload, $$props, name, slot_props, fallback_fn) {
415+
var slot_fn = $$props.$$slots?.[name];
416+
// Interop: Can use snippets to fill slots
417+
if (slot_fn === true) {
418+
slot_fn = $$props[name === 'default' ? 'children' : name];
419+
}
420+
414421
if (slot_fn !== undefined) {
415422
slot_fn(payload, slot_props);
416423
} else {
@@ -545,5 +552,3 @@ export {
545552
} from '../shared/validate.js';
546553

547554
export { escape_html as escape };
548-
549-
export { default_slot } from '../client/dom/legacy/misc.js';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `<p>Default</p> <p>Named foo</p>`
5+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<p><slot /></p>
2+
<p><slot name="named" foo="foo" /></p>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
import Child from './child.svelte';
3+
</script>
4+
5+
<Child>
6+
Default
7+
{#snippet named({ foo })}
8+
Named {foo}
9+
{/snippet}
10+
</Child>

0 commit comments

Comments
 (0)