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 <rich.harris@vercel.com>
pull/13486/head
Simon H 12 months ago committed by GitHub
parent 59b608c16e
commit e8508526e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: enable snippets to fill slots

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Property } from 'estree' */ /** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
@ -23,7 +23,6 @@ export function SlotElement(node, context) {
let is_default = true; let is_default = true;
/** @type {Expression} */
let name = b.literal('default'); let name = b.literal('default');
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
@ -33,7 +32,7 @@ export function SlotElement(node, context) {
const { value } = build_attribute_value(attribute.value, context); const { value } = build_attribute_value(attribute.value, context);
if (attribute.name === 'name') { if (attribute.name === 'name') {
name = value; name = /** @type {Literal} */ (value);
is_default = false; is_default = false;
} else if (attribute.name !== 'slot') { } else if (attribute.name !== 'slot') {
if (attribute.metadata.expression.has_state) { if (attribute.metadata.expression.has_state) {
@ -58,10 +57,14 @@ export function SlotElement(node, context) {
? b.literal(null) ? b.literal(null)
: b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment))); : b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)));
const expression = is_default const slot = b.call(
? b.call('$.default_slot', b.id('$$props')) '$.slot',
: b.member(b.member(b.id('$$props'), '$$slots'), name, true, true); context.state.node,
b.id('$$props'),
name,
props_expression,
fallback
);
const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback);
context.state.init.push(b.stmt(slot)); context.state.init.push(b.stmt(slot));
} }

@ -45,9 +45,7 @@ export function build_component(node, component_name, context, anchor = context.
/** @type {Identifier | MemberExpression | null} */ /** @type {Identifier | MemberExpression | null} */
let bind_this = null; let bind_this = null;
/** /** @type {ExpressionStatement[]} */
* @type {ExpressionStatement[]}
*/
const binding_initializers = []; const binding_initializers = [];
/** /**
@ -216,6 +214,9 @@ export function build_component(node, component_name, context, anchor = context.
/** @type {Statement[]} */ /** @type {Statement[]} */
const snippet_declarations = []; const snippet_declarations = [];
/** @type {import('estree').Property[]} */
const serialized_slots = [];
// Group children by slot // Group children by slot
for (const child of node.fragment.nodes) { for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock') { if (child.type === 'SnippetBlock') {
@ -229,6 +230,9 @@ export function build_component(node, component_name, context, anchor = context.
push_prop(b.prop('init', child.expression, child.expression)); push_prop(b.prop('init', child.expression, child.expression));
// Interop: allows people to pass snippets when component still uses slots
serialized_slots.push(b.init(child.expression.name, b.true));
continue; continue;
} }
@ -238,8 +242,6 @@ export function build_component(node, component_name, context, anchor = context.
} }
// Serialize each slot // Serialize each slot
/** @type {Property[]} */
const serialized_slots = [];
for (const slot_name of Object.keys(children)) { for (const slot_name of Object.keys(children)) {
const block = /** @type {BlockStatement} */ ( const block = /** @type {BlockStatement} */ (
context.visit( context.visit(

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, Property } from 'estree' */ /** @import { BlockStatement, Expression, Literal, Property } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
@ -15,8 +15,7 @@ export function SlotElement(node, context) {
/** @type {Expression[]} */ /** @type {Expression[]} */
const spreads = []; const spreads = [];
/** @type {Expression} */ let name = b.literal('default');
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') {
@ -25,7 +24,7 @@ export function SlotElement(node, context) {
const value = build_attribute_value(attribute.value, context, false, true); const value = build_attribute_value(attribute.value, context, false, true);
if (attribute.name === 'name') { if (attribute.name === 'name') {
expression = b.member(b.member_id('$$props.$$slots'), value, true, true); name = /** @type {Literal} */ (value);
} else if (attribute.name !== 'slot') { } else if (attribute.name !== 'slot') {
props.push(b.init(attribute.name, value)); props.push(b.init(attribute.name, value));
} }
@ -42,7 +41,14 @@ export function SlotElement(node, context) {
? b.literal(null) ? b.literal(null)
: b.thunk(/** @type {BlockStatement} */ (context.visit(node.fragment))); : b.thunk(/** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback); const slot = b.call(
'$.slot',
b.id('$$payload'),
b.id('$$props'),
name,
props_expression,
fallback
);
context.state.template.push(empty_comment, b.stmt(slot), empty_comment); context.state.template.push(empty_comment, b.stmt(slot), empty_comment);
} }

@ -59,6 +59,7 @@ export function build_inline_component(node, expression, context) {
props_and_spreads.push(props); props_and_spreads.push(props);
} }
} }
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') { if (attribute.type === 'LetDirective') {
if (!slot_scope_applies_to_itself) { if (!slot_scope_applies_to_itself) {
@ -102,6 +103,9 @@ export function build_inline_component(node, expression, context) {
/** @type {Statement[]} */ /** @type {Statement[]} */
const snippet_declarations = []; const snippet_declarations = [];
/** @type {Property[]} */
const serialized_slots = [];
// Group children by slot // Group children by slot
for (const child of node.fragment.nodes) { for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock') { if (child.type === 'SnippetBlock') {
@ -115,6 +119,9 @@ export function build_inline_component(node, expression, context) {
push_prop(b.prop('init', child.expression, child.expression)); push_prop(b.prop('init', child.expression, child.expression));
// Interop: allows people to pass snippets when component still uses slots
serialized_slots.push(b.init(child.expression.name, b.true));
continue; continue;
} }
@ -142,9 +149,6 @@ export function build_inline_component(node, expression, context) {
} }
// Serialize each slot // Serialize each slot
/** @type {Property[]} */
const serialized_slots = [];
for (const slot_name of Object.keys(children)) { for (const slot_name of Object.keys(children)) {
const block = /** @type {BlockStatement} */ ( const block = /** @type {BlockStatement} */ (
context.visit( context.visit(

@ -2,21 +2,30 @@ import { hydrate_next, hydrating } from '../hydration.js';
/** /**
* @param {Comment} anchor * @param {Comment} anchor
* @param {void | ((anchor: Comment, slot_props: Record<string, unknown>) => void)} slot_fn * @param {Record<string, any>} $$props
* @param {string} name
* @param {Record<string, unknown>} slot_props * @param {Record<string, unknown>} slot_props
* @param {null | ((anchor: Comment) => void)} fallback_fn * @param {null | ((anchor: Comment) => void)} fallback_fn
*/ */
export function slot(anchor, slot_fn, slot_props, fallback_fn) { export function slot(anchor, $$props, name, slot_props, fallback_fn) {
if (hydrating) { if (hydrating) {
hydrate_next(); hydrate_next();
} }
var slot_fn = $$props.$$slots?.[name];
// Interop: Can use snippets to fill slots
var is_interop = false;
if (slot_fn === true) {
slot_fn = $$props[name === 'default' ? 'children' : name];
is_interop = true;
}
if (slot_fn === undefined) { if (slot_fn === undefined) {
if (fallback_fn !== null) { if (fallback_fn !== null) {
fallback_fn(anchor); fallback_fn(anchor);
} }
} else { } else {
slot_fn(anchor, slot_props); slot_fn(anchor, is_interop ? () => slot_props : slot_props);
} }
} }

@ -66,15 +66,3 @@ 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;
}
}

@ -80,8 +80,7 @@ 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,

@ -405,12 +405,19 @@ export function unsubscribe_stores(store_values) {
/** /**
* @param {Payload} payload * @param {Payload} payload
* @param {void | ((payload: Payload, props: Record<string, unknown>) => void)} slot_fn * @param {Record<string, any>} $$props
* @param {string} name
* @param {Record<string, unknown>} slot_props * @param {Record<string, unknown>} slot_props
* @param {null | (() => void)} fallback_fn * @param {null | (() => void)} fallback_fn
* @returns {void} * @returns {void}
*/ */
export function slot(payload, slot_fn, slot_props, fallback_fn) { export function slot(payload, $$props, name, slot_props, fallback_fn) {
var slot_fn = $$props.$$slots?.[name];
// Interop: Can use snippets to fill slots
if (slot_fn === true) {
slot_fn = $$props[name === 'default' ? 'children' : name];
}
if (slot_fn !== undefined) { if (slot_fn !== undefined) {
slot_fn(payload, slot_props); slot_fn(payload, slot_props);
} else { } else {
@ -545,5 +552,3 @@ 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,5 @@
import { test } from '../../test';
export default test({
html: `<p>Default</p> <p>Named foo</p>`
});

@ -0,0 +1,2 @@
<p><slot /></p>
<p><slot name="named" foo="foo" /></p>

@ -0,0 +1,10 @@
<script>
import Child from './child.svelte';
</script>
<Child>
Default
{#snippet named({ foo })}
Named {foo}
{/snippet}
</Child>
Loading…
Cancel
Save