Skip to content

Commit 762a6e2

Browse files
committed
feat: allow arbitrary call expressions for render tags
closes #9582
1 parent 3fe4940 commit 762a6e2

File tree

12 files changed

+122
-24
lines changed

12 files changed

+122
-24
lines changed

.changeset/ten-jokes-divide.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: allow arbitrary call expressions and optional chaining for snippets

packages/svelte/src/compiler/phases/1-parse/state/tag.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,12 @@ function special(parser) {
577577

578578
const expression = read_expression(parser);
579579

580-
if (expression.type !== 'CallExpression' || expression.callee.type !== 'Identifier') {
580+
if (
581+
expression.type !== 'CallExpression' &&
582+
(expression.type !== 'ChainExpression' ||
583+
expression.expression.type !== 'CallExpression' ||
584+
!expression.expression.optional)
585+
) {
581586
error(expression, 'invalid-render-expression');
582587
}
583588

@@ -589,8 +594,11 @@ function special(parser) {
589594
type: 'RenderTag',
590595
start,
591596
end: parser.index,
592-
expression: expression.callee,
593-
arguments: expression.arguments
597+
expression: expression,
598+
arguments:
599+
expression.type === 'ChainExpression'
600+
? /** @type {import('estree').CallExpression} */ (expression.expression).arguments
601+
: expression.arguments
594602
});
595603
}
596604
}

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -604,11 +604,16 @@ const validation = {
604604
});
605605
},
606606
RenderTag(node, context) {
607-
for (const arg of node.arguments) {
607+
const raw_args =
608+
node.expression.type === 'CallExpression'
609+
? node.expression.arguments
610+
: node.expression.expression.arguments;
611+
for (const arg of raw_args) {
608612
if (arg.type === 'SpreadElement') {
609613
error(arg, 'invalid-render-spread-argument');
610614
}
611615
}
616+
612617
const is_inside_textarea = context.path.find((n) => {
613618
return (
614619
n.type === 'SvelteElement' &&
@@ -622,7 +627,7 @@ const validation = {
622627
node,
623628
'invalid-tag-placement',
624629
'inside <textarea> or <svelte:element this="textarea">',
625-
node.expression.name
630+
'render'
626631
);
627632
}
628633
},

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1864,18 +1864,24 @@ export const template_visitors = {
18641864
},
18651865
RenderTag(node, context) {
18661866
context.state.template.push('<!>');
1867-
const binding = context.state.scope.get(node.expression.name);
1868-
const is_reactive = binding?.kind !== 'normal' || node.expression.type !== 'Identifier';
1867+
const callee =
1868+
node.expression.type === 'CallExpression'
1869+
? node.expression.callee
1870+
: node.expression.expression.callee;
1871+
const raw_args =
1872+
node.expression.type === 'CallExpression'
1873+
? node.expression.arguments
1874+
: node.expression.expression.arguments;
1875+
const is_reactive =
1876+
callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal';
18691877

18701878
/** @type {import('estree').Expression[]} */
18711879
const args = [context.state.node];
1872-
for (const arg of node.arguments) {
1880+
for (const arg of raw_args) {
18731881
args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg))));
18741882
}
18751883

1876-
let snippet_function = /** @type {import('estree').Expression} */ (
1877-
context.visit(node.expression)
1878-
);
1884+
let snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee));
18791885
if (context.state.options.dev) {
18801886
snippet_function = b.call('$.validate_snippet', snippet_function);
18811887
}
@@ -1885,7 +1891,14 @@ export const template_visitors = {
18851891
b.stmt(b.call('$.snippet_effect', b.thunk(snippet_function), ...args))
18861892
);
18871893
} else {
1888-
context.state.after_update.push(b.stmt(b.call(snippet_function, ...args)));
1894+
context.state.after_update.push(
1895+
b.stmt(
1896+
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
1897+
snippet_function,
1898+
...args
1899+
)
1900+
)
1901+
);
18891902
}
18901903
},
18911904
AnimateDirective(node, { state, visit }) {

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,17 +1141,34 @@ const template_visitors = {
11411141
state.init.push(anchor);
11421142
state.template.push(t_expression(anchor_id));
11431143

1144-
const expression = /** @type {import('estree').Expression} */ (context.visit(node.expression));
1144+
const callee =
1145+
node.expression.type === 'CallExpression'
1146+
? node.expression.callee
1147+
: node.expression.expression.callee;
1148+
const raw_args =
1149+
node.expression.type === 'CallExpression'
1150+
? node.expression.arguments
1151+
: node.expression.expression.arguments;
1152+
1153+
const expression = /** @type {import('estree').Expression} */ (context.visit(callee));
11451154
const snippet_function = state.options.dev
11461155
? b.call('$.validate_snippet', expression)
11471156
: expression;
11481157

1149-
const snippet_args = node.arguments.map((arg) => {
1158+
const snippet_args = raw_args.map((arg) => {
11501159
return /** @type {import('estree').Expression} */ (context.visit(arg));
11511160
});
11521161

11531162
state.template.push(
1154-
t_statement(b.stmt(b.call(snippet_function, b.id('$$payload'), ...snippet_args)))
1163+
t_statement(
1164+
b.stmt(
1165+
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
1166+
snippet_function,
1167+
b.id('$$payload'),
1168+
...snippet_args
1169+
)
1170+
)
1171+
)
11551172
);
11561173

11571174
state.template.push(t_expression(anchor_id));

packages/svelte/src/compiler/types/template.d.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import type {
1313
ObjectExpression,
1414
Pattern,
1515
Program,
16-
SpreadElement
16+
SpreadElement,
17+
CallExpression,
18+
ChainExpression,
19+
SimpleCallExpression
1720
} from 'estree';
1821
import type { Atrule, Rule } from './css';
1922

@@ -151,8 +154,7 @@ export interface DebugTag extends BaseNode {
151154
/** A `{@render foo(...)} tag */
152155
export interface RenderTag extends BaseNode {
153156
type: 'RenderTag';
154-
expression: Identifier;
155-
arguments: Array<Expression | SpreadElement>;
157+
expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression });
156158
}
157159

158160
type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag;

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2684,7 +2684,7 @@ export function sanitize_slots(props) {
26842684
}
26852685

26862686
/**
2687-
* @param {() => Function} get_snippet
2687+
* @param {() => Function | null | undefined} get_snippet
26882688
* @param {Node} node
26892689
* @param {(() => any)[]} args
26902690
* @returns {void}
@@ -2695,7 +2695,9 @@ export function snippet_effect(get_snippet, node, ...args) {
26952695
// Only rerender when the snippet function itself changes,
26962696
// not when an eagerly-read prop inside the snippet function changes
26972697
const snippet = get_snippet();
2698-
untrack(() => snippet(node, ...args));
2698+
if (snippet) {
2699+
untrack(() => snippet(node, ...args));
2700+
}
26992701
return () => {
27002702
if (block.d !== null) {
27012703
remove(block.d);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export function add_snippet_symbol(fn) {
118118
* @param {any} snippet_fn
119119
*/
120120
export function validate_snippet(snippet_fn) {
121-
if (snippet_fn[snippet_symbol] !== true) {
121+
if (snippet_fn && snippet_fn[snippet_symbol] !== true) {
122122
throw new Error(
123123
'The argument to `{@render ...}` must be a snippet function, not a component or some other kind of function. ' +
124124
'If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`.'
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `
5+
<p>foo</p>
6+
<hr>
7+
<button>toggle</button>
8+
`,
9+
10+
async test({ assert, target }) {
11+
const btn = target.querySelector('button');
12+
await btn?.click();
13+
assert.htmlEqual(
14+
target.innerHTML,
15+
`
16+
<p>bar</p>
17+
<hr>
18+
<p>foo</p>
19+
<button>toggle</button>
20+
`
21+
);
22+
}
23+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let { snippets, snippet, optional } = $props();
3+
</script>
4+
5+
{@render snippets[snippet]()}
6+
<hr>
7+
{@render optional?.()}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script>
2+
import Child from './child.svelte';
3+
let snippet = $state(0);
4+
let show = $state(false);
5+
</script>
6+
7+
{#snippet foo()}
8+
<p>foo</p>
9+
{/snippet}
10+
11+
{#snippet bar()}
12+
<p>bar</p>
13+
{/snippet}
14+
15+
<Child snippets={[foo, bar]} {snippet} optional={show ? foo : undefined} />
16+
17+
<button on:click={() => { snippet = 1; show = true; }}>toggle</button>

packages/svelte/types/index.d.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ declare module 'svelte/animate' {
475475
}
476476

477477
declare module 'svelte/compiler' {
478-
import type { AssignmentExpression, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, ArrayExpression, MemberExpression, ObjectExpression, Pattern, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, FunctionExpression, Node, Program, SpreadElement } from 'estree';
478+
import type { AssignmentExpression, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, ArrayExpression, MemberExpression, ObjectExpression, Pattern, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, FunctionExpression, Node, Program, ChainExpression, SimpleCallExpression } from 'estree';
479479
import type { Location } from 'locate-character';
480480
import type { SourceMap } from 'magic-string';
481481
import type { Context } from 'zimmerframe';
@@ -1197,8 +1197,7 @@ declare module 'svelte/compiler' {
11971197
/** A `{@render foo(...)} tag */
11981198
interface RenderTag extends BaseNode {
11991199
type: 'RenderTag';
1200-
expression: Identifier;
1201-
arguments: Array<Expression | SpreadElement>;
1200+
expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression });
12021201
}
12031202

12041203
type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag;

0 commit comments

Comments
 (0)