Skip to content

Commit e850852

Browse files
feat: enable snippets to fill slots (#13427)
* 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. * unused * ditto - annotation is redundant * couple of drive-by consistency tweaks --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 59b608c commit e850852

File tree

12 files changed

+79
-41
lines changed

12 files changed

+79
-41
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: 10 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,6 @@ export function SlotElement(node, context) {
2323

2424
let is_default = true;
2525

26-
/** @type {Expression} */
2726
let name = b.literal('default');
2827

2928
for (const attribute of node.attributes) {
@@ -33,7 +32,7 @@ export function SlotElement(node, context) {
3332
const { value } = build_attribute_value(attribute.value, context);
3433

3534
if (attribute.name === 'name') {
36-
name = value;
35+
name = /** @type {Literal} */ (value);
3736
is_default = false;
3837
} else if (attribute.name !== 'slot') {
3938
if (attribute.metadata.expression.has_state) {
@@ -58,10 +57,14 @@ export function SlotElement(node, context) {
5857
? b.literal(null)
5958
: b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)));
6059

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);
60+
const slot = b.call(
61+
'$.slot',
62+
context.state.node,
63+
b.id('$$props'),
64+
name,
65+
props_expression,
66+
fallback
67+
);
6468

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

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ export function build_component(node, component_name, context, anchor = context.
4545
/** @type {Identifier | MemberExpression | null} */
4646
let bind_this = null;
4747

48-
/**
49-
* @type {ExpressionStatement[]}
50-
*/
48+
/** @type {ExpressionStatement[]} */
5149
const binding_initializers = [];
5250

5351
/**
@@ -216,6 +214,9 @@ export function build_component(node, component_name, context, anchor = context.
216214
/** @type {Statement[]} */
217215
const snippet_declarations = [];
218216

217+
/** @type {import('estree').Property[]} */
218+
const serialized_slots = [];
219+
219220
// Group children by slot
220221
for (const child of node.fragment.nodes) {
221222
if (child.type === 'SnippetBlock') {
@@ -229,6 +230,9 @@ export function build_component(node, component_name, context, anchor = context.
229230

230231
push_prop(b.prop('init', child.expression, child.expression));
231232

233+
// Interop: allows people to pass snippets when component still uses slots
234+
serialized_slots.push(b.init(child.expression.name, b.true));
235+
232236
continue;
233237
}
234238

@@ -238,8 +242,6 @@ export function build_component(node, component_name, context, anchor = context.
238242
}
239243

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

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

Lines changed: 11 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,7 @@ 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+
let name = b.literal('default');
2019

2120
for (const attribute of node.attributes) {
2221
if (attribute.type === 'SpreadAttribute') {
@@ -25,7 +24,7 @@ export function SlotElement(node, context) {
2524
const value = build_attribute_value(attribute.value, context, false, true);
2625

2726
if (attribute.name === 'name') {
28-
expression = b.member(b.member_id('$$props.$$slots'), value, true, true);
27+
name = /** @type {Literal} */ (value);
2928
} else if (attribute.name !== 'slot') {
3029
props.push(b.init(attribute.name, value));
3130
}
@@ -42,7 +41,14 @@ export function SlotElement(node, context) {
4241
? b.literal(null)
4342
: b.thunk(/** @type {BlockStatement} */ (context.visit(node.fragment)));
4443

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

4753
context.state.template.push(empty_comment, b.stmt(slot), empty_comment);
4854
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export function build_inline_component(node, expression, context) {
5959
props_and_spreads.push(props);
6060
}
6161
}
62+
6263
for (const attribute of node.attributes) {
6364
if (attribute.type === 'LetDirective') {
6465
if (!slot_scope_applies_to_itself) {
@@ -102,6 +103,9 @@ export function build_inline_component(node, expression, context) {
102103
/** @type {Statement[]} */
103104
const snippet_declarations = [];
104105

106+
/** @type {Property[]} */
107+
const serialized_slots = [];
108+
105109
// Group children by slot
106110
for (const child of node.fragment.nodes) {
107111
if (child.type === 'SnippetBlock') {
@@ -115,6 +119,9 @@ export function build_inline_component(node, expression, context) {
115119

116120
push_prop(b.prop('init', child.expression, child.expression));
117121

122+
// Interop: allows people to pass snippets when component still uses slots
123+
serialized_slots.push(b.init(child.expression.name, b.true));
124+
118125
continue;
119126
}
120127

@@ -142,9 +149,6 @@ export function build_inline_component(node, expression, context) {
142149
}
143150

144151
// Serialize each slot
145-
/** @type {Property[]} */
146-
const serialized_slots = [];
147-
148152
for (const slot_name of Object.keys(children)) {
149153
const block = /** @type {BlockStatement} */ (
150154
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+
});

0 commit comments

Comments
 (0)