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>
pull/11617/head
Simon H 7 months ago committed by GitHub
parent cac8630de6
commit 4365562228
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: deduplicate children prop and default slot

@ -660,6 +660,13 @@ function serialize_inline_component(node, component_name, context) {
*/ */
let slot_scope_applies_to_itself = false; let slot_scope_applies_to_itself = false;
/**
* Components may have a children prop and also have child nodes. In this case, we assume
* that the child component isn't using render tags yet and pass the slot as $$slots.default.
* We're not doing it for spread attributes, as this would result in too many false positives.
*/
let has_children_prop = false;
/** /**
* @param {import('estree').Property} prop * @param {import('estree').Property} prop
*/ */
@ -709,6 +716,10 @@ function serialize_inline_component(node, component_name, context) {
slot_scope_applies_to_itself = true; slot_scope_applies_to_itself = true;
} }
if (attribute.name === 'children') {
has_children_prop = true;
}
const [, value] = serialize_attribute_value(attribute.value, context); const [, value] = serialize_attribute_value(attribute.value, context);
if (attribute.metadata.dynamic) { if (attribute.metadata.dynamic) {
@ -825,10 +836,13 @@ function serialize_inline_component(node, component_name, context) {
b.block([...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), ...body]) b.block([...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), ...body])
); );
if (slot_name === 'default') { if (slot_name === 'default' && !has_children_prop) {
push_prop( push_prop(
b.init('children', context.state.options.dev ? b.call('$.wrap_snippet', slot_fn) : slot_fn) b.init('children', context.state.options.dev ? b.call('$.wrap_snippet', slot_fn) : slot_fn)
); );
// We additionally add the default slot as a boolean, so that the slot render function on the other
// side knows it should get the content to render from $$props.children
serialized_slots.push(b.init(slot_name, b.true));
} else { } else {
serialized_slots.push(b.init(slot_name, slot_fn)); serialized_slots.push(b.init(slot_name, slot_fn));
} }
@ -3057,7 +3071,7 @@ export const template_visitors = {
); );
const expression = is_default const expression = is_default
? b.member(b.id('$$props'), b.id('children')) ? b.call('$.default_slot', b.id('$$props'))
: b.member(b.member(b.id('$$props'), b.id('$$slots')), name, true, true); : b.member(b.member(b.id('$$props'), b.id('$$slots')), name, true, true);
const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback); const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback);

@ -978,6 +978,13 @@ function serialize_inline_component(node, component_name, context) {
*/ */
let slot_scope_applies_to_itself = false; let slot_scope_applies_to_itself = false;
/**
* Components may have a children prop and also have child nodes. In this case, we assume
* that the child component isn't using render tags yet and pass the slot as $$slots.default.
* We're not doing it for spread attributes, as this would result in too many false positives.
*/
let has_children_prop = false;
/** /**
* @param {import('estree').Property} prop * @param {import('estree').Property} prop
*/ */
@ -1006,6 +1013,10 @@ function serialize_inline_component(node, component_name, context) {
slot_scope_applies_to_itself = true; slot_scope_applies_to_itself = true;
} }
if (attribute.name === 'children') {
has_children_prop = true;
}
const value = serialize_attribute_value(attribute.value, context, false, true); const value = serialize_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value)); push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
@ -1080,7 +1091,7 @@ function serialize_inline_component(node, component_name, context) {
b.block([...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), ...body]) b.block([...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), ...body])
); );
if (slot_name === 'default') { if (slot_name === 'default' && !has_children_prop) {
push_prop( push_prop(
b.prop( b.prop(
'init', 'init',
@ -1088,6 +1099,9 @@ function serialize_inline_component(node, component_name, context) {
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
) )
); );
// We additionally add the default slot as a boolean, so that the slot render function on the other
// side knows it should get the content to render from $$props.children
serialized_slots.push(b.init('default', b.true));
} else { } else {
const slot = b.prop('init', b.literal(slot_name), slot_fn); const slot = b.prop('init', b.literal(slot_name), slot_fn);
serialized_slots.push(slot); serialized_slots.push(slot);
@ -1755,7 +1769,7 @@ const template_visitors = {
const lets = []; const lets = [];
/** @type {import('estree').Expression} */ /** @type {import('estree').Expression} */
let expression = b.member_id('$$props.children'); let expression = b.call('$.default_slot', b.id('$$props'));
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') { if (attribute.type === 'SpreadAttribute') {

@ -109,8 +109,9 @@ if (typeof HTMLElement === 'function') {
const existing_slots = get_custom_elements_slots(this); const existing_slots = get_custom_elements_slots(this);
for (const name of this.$$s) { for (const name of this.$$s) {
if (name in existing_slots) { if (name in existing_slots) {
if (name === 'default') { if (name === 'default' && !this.$$d.children) {
this.$$d.children = create_slot(name); this.$$d.children = create_slot(name);
$$slots.default = true;
} else { } else {
$$slots[name] = create_slot(name); $$slots[name] = create_slot(name);
} }

@ -66,3 +66,15 @@ export function update_legacy_props($$new_props) {
} }
} }
} }
/**
* @param {Record<string, any>} $$props
*/
export function default_slot($$props) {
var children = $$props.$$slots?.default;
if (children === true) {
return $$props.children;
} else {
return children;
}
}

@ -72,7 +72,8 @@ export {
add_legacy_event_listener, add_legacy_event_listener,
bubble_event, bubble_event,
reactive_import, reactive_import,
update_legacy_props update_legacy_props,
default_slot
} from './dom/legacy/misc.js'; } from './dom/legacy/misc.js';
export { export {
append, append,
@ -154,7 +155,6 @@ export {
} from './dom/operations.js'; } from './dom/operations.js';
export { noop } from '../shared/utils.js'; export { noop } from '../shared/utils.js';
export { export {
add_snippet_symbol,
validate_component, validate_component,
validate_dynamic_element_tag, validate_dynamic_element_tag,
validate_snippet, validate_snippet,

@ -193,8 +193,9 @@ export function spread_attributes(attrs, lowercase_attributes, is_html, class_ha
for (let i = 0; i < attrs.length; i++) { for (let i = 0; i < attrs.length; i++) {
const obj = attrs[i]; const obj = attrs[i];
for (key in obj) { for (key in obj) {
// omit functions // omit functions and internal svelte properties
if (typeof obj[key] !== 'function') { const prefix = key[0] + key[1]; // this is faster than key.slice(0, 2)
if (typeof obj[key] !== 'function' && prefix !== '$$') {
merged_attrs[key] = obj[key]; merged_attrs[key] = obj[key];
} }
} }
@ -549,3 +550,5 @@ export {
} from '../shared/validate.js'; } from '../shared/validate.js';
export { escape_html as escape }; export { escape_html as escape };
export { default_slot } from '../client/dom/legacy/misc.js';

@ -0,0 +1,6 @@
<script>
export let children;
</script>
{children}
<slot />

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `foo bar foo`
});

@ -0,0 +1,9 @@
<script>
import A from "./A.svelte";
</script>
<A children="foo">
bar
</A>
<A children="foo" />

@ -21,7 +21,8 @@ export default function Function_prop_no_getter($$anchor) {
$.template_effect(() => $.set_text(text, `clicks: ${$.stringify($.get(count))}`)); $.template_effect(() => $.set_text(text, `clicks: ${$.stringify($.get(count))}`));
$.append($$anchor, text); $.append($$anchor, text);
} },
$$slots: { default: true }
}); });
$.append($$anchor, fragment); $.append($$anchor, fragment);

@ -19,7 +19,8 @@ export default function Function_prop_no_getter($$payload, $$props) {
onmouseenter: () => count = plusOne(count), onmouseenter: () => count = plusOne(count),
children: ($$payload, $$slotProps) => { children: ($$payload, $$slotProps) => {
$$payload.out += `clicks: ${$.escape(count)}`; $$payload.out += `clicks: ${$.escape(count)}`;
} },
$$slots: { default: true }
}); });
$$payload.out += `<!--]-->`; $$payload.out += `<!--]-->`;

Loading…
Cancel
Save