Skip to content

Commit 4365562

Browse files
fix: deduplicate children prop from default slot (#10800)
* feat: provide isSnippet type, deduplicate children prop from default slot fixes #10790 part of #9774 * fix ce bug * remove isSnippet type, adjust test * fix types * revert unrelated changes * remove changeset * enhance test * fix * fix * fix * fix, different approach without needing symbol --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>
1 parent cac8630 commit 4365562

File tree

12 files changed

+82
-11
lines changed

12 files changed

+82
-11
lines changed

.changeset/cold-cheetahs-judge.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: deduplicate children prop and default slot

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,13 @@ function serialize_inline_component(node, component_name, context) {
660660
*/
661661
let slot_scope_applies_to_itself = false;
662662

663+
/**
664+
* Components may have a children prop and also have child nodes. In this case, we assume
665+
* that the child component isn't using render tags yet and pass the slot as $$slots.default.
666+
* We're not doing it for spread attributes, as this would result in too many false positives.
667+
*/
668+
let has_children_prop = false;
669+
663670
/**
664671
* @param {import('estree').Property} prop
665672
*/
@@ -709,6 +716,10 @@ function serialize_inline_component(node, component_name, context) {
709716
slot_scope_applies_to_itself = true;
710717
}
711718

719+
if (attribute.name === 'children') {
720+
has_children_prop = true;
721+
}
722+
712723
const [, value] = serialize_attribute_value(attribute.value, context);
713724

714725
if (attribute.metadata.dynamic) {
@@ -825,10 +836,13 @@ function serialize_inline_component(node, component_name, context) {
825836
b.block([...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), ...body])
826837
);
827838

828-
if (slot_name === 'default') {
839+
if (slot_name === 'default' && !has_children_prop) {
829840
push_prop(
830841
b.init('children', context.state.options.dev ? b.call('$.wrap_snippet', slot_fn) : slot_fn)
831842
);
843+
// We additionally add the default slot as a boolean, so that the slot render function on the other
844+
// side knows it should get the content to render from $$props.children
845+
serialized_slots.push(b.init(slot_name, b.true));
832846
} else {
833847
serialized_slots.push(b.init(slot_name, slot_fn));
834848
}
@@ -3057,7 +3071,7 @@ export const template_visitors = {
30573071
);
30583072

30593073
const expression = is_default
3060-
? b.member(b.id('$$props'), b.id('children'))
3074+
? b.call('$.default_slot', b.id('$$props'))
30613075
: b.member(b.member(b.id('$$props'), b.id('$$slots')), name, true, true);
30623076

30633077
const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback);

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,13 @@ function serialize_inline_component(node, component_name, context) {
978978
*/
979979
let slot_scope_applies_to_itself = false;
980980

981+
/**
982+
* Components may have a children prop and also have child nodes. In this case, we assume
983+
* that the child component isn't using render tags yet and pass the slot as $$slots.default.
984+
* We're not doing it for spread attributes, as this would result in too many false positives.
985+
*/
986+
let has_children_prop = false;
987+
981988
/**
982989
* @param {import('estree').Property} prop
983990
*/
@@ -1006,6 +1013,10 @@ function serialize_inline_component(node, component_name, context) {
10061013
slot_scope_applies_to_itself = true;
10071014
}
10081015

1016+
if (attribute.name === 'children') {
1017+
has_children_prop = true;
1018+
}
1019+
10091020
const value = serialize_attribute_value(attribute.value, context, false, true);
10101021
push_prop(b.prop('init', b.key(attribute.name), value));
10111022
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
@@ -1080,14 +1091,17 @@ function serialize_inline_component(node, component_name, context) {
10801091
b.block([...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), ...body])
10811092
);
10821093

1083-
if (slot_name === 'default') {
1094+
if (slot_name === 'default' && !has_children_prop) {
10841095
push_prop(
10851096
b.prop(
10861097
'init',
10871098
b.id('children'),
10881099
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
10891100
)
10901101
);
1102+
// We additionally add the default slot as a boolean, so that the slot render function on the other
1103+
// side knows it should get the content to render from $$props.children
1104+
serialized_slots.push(b.init('default', b.true));
10911105
} else {
10921106
const slot = b.prop('init', b.literal(slot_name), slot_fn);
10931107
serialized_slots.push(slot);
@@ -1755,7 +1769,7 @@ const template_visitors = {
17551769
const lets = [];
17561770

17571771
/** @type {import('estree').Expression} */
1758-
let expression = b.member_id('$$props.children');
1772+
let expression = b.call('$.default_slot', b.id('$$props'));
17591773

17601774
for (const attribute of node.attributes) {
17611775
if (attribute.type === 'SpreadAttribute') {

packages/svelte/src/internal/client/dom/elements/custom-element.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ if (typeof HTMLElement === 'function') {
109109
const existing_slots = get_custom_elements_slots(this);
110110
for (const name of this.$$s) {
111111
if (name in existing_slots) {
112-
if (name === 'default') {
112+
if (name === 'default' && !this.$$d.children) {
113113
this.$$d.children = create_slot(name);
114+
$$slots.default = true;
114115
} else {
115116
$$slots[name] = create_slot(name);
116117
}

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

+12
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,15 @@ 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

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ export {
7272
add_legacy_event_listener,
7373
bubble_event,
7474
reactive_import,
75-
update_legacy_props
75+
update_legacy_props,
76+
default_slot
7677
} from './dom/legacy/misc.js';
7778
export {
7879
append,
@@ -154,7 +155,6 @@ export {
154155
} from './dom/operations.js';
155156
export { noop } from '../shared/utils.js';
156157
export {
157-
add_snippet_symbol,
158158
validate_component,
159159
validate_dynamic_element_tag,
160160
validate_snippet,

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,9 @@ export function spread_attributes(attrs, lowercase_attributes, is_html, class_ha
193193
for (let i = 0; i < attrs.length; i++) {
194194
const obj = attrs[i];
195195
for (key in obj) {
196-
// omit functions
197-
if (typeof obj[key] !== 'function') {
196+
// omit functions and internal svelte properties
197+
const prefix = key[0] + key[1]; // this is faster than key.slice(0, 2)
198+
if (typeof obj[key] !== 'function' && prefix !== '$$') {
198199
merged_attrs[key] = obj[key];
199200
}
200201
}
@@ -549,3 +550,5 @@ export {
549550
} from '../shared/validate.js';
550551

551552
export { escape_html as escape };
553+
554+
export { default_slot } from '../client/dom/legacy/misc.js';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
export let children;
3+
</script>
4+
5+
{children}
6+
<slot />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `foo bar foo`
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
import A from "./A.svelte";
3+
</script>
4+
5+
<A children="foo">
6+
bar
7+
</A>
8+
9+
<A children="foo" />

packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export default function Function_prop_no_getter($$anchor) {
2121

2222
$.template_effect(() => $.set_text(text, `clicks: ${$.stringify($.get(count))}`));
2323
$.append($$anchor, text);
24-
}
24+
},
25+
$$slots: { default: true }
2526
});
2627

2728
$.append($$anchor, fragment);

packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export default function Function_prop_no_getter($$payload, $$props) {
1919
onmouseenter: () => count = plusOne(count),
2020
children: ($$payload, $$slotProps) => {
2121
$$payload.out += `clicks: ${$.escape(count)}`;
22-
}
22+
},
23+
$$slots: { default: true }
2324
});
2425

2526
$$payload.out += `<!--]-->`;

0 commit comments

Comments
 (0)