diff --git a/.changeset/four-loops-agree.md b/.changeset/four-loops-agree.md new file mode 100644 index 0000000000..46d30e8464 --- /dev/null +++ b/.changeset/four-loops-agree.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow declarations in the template diff --git a/documentation/docs/03-template-syntax/11-declaration.md b/documentation/docs/03-template-syntax/11-declaration.md new file mode 100644 index 0000000000..ebc31fa631 --- /dev/null +++ b/documentation/docs/03-template-syntax/11-declaration.md @@ -0,0 +1,32 @@ +--- +title: {} +--- + +Declaration tags define local variables and functions inside markup. + +You can use `let`, `const`, `var` and `function` declarations: + +```svelte +{#each boxes as box} + {const area = box.width * box.height} + {function label(value) { + return `${value} square pixels`; + }} + +

{label(area)}

+{/each} +``` + +Unlike [`{@const ...}`](@const), declaration tags are plain JavaScript declarations. This means `{const ...}` is not reactive by itself; use runes such as `$state` or `$derived` when you need reactive values: + +```svelte +{#if user} + {let name = $state(user.name)} + {let greeting = $derived(`Hello ${name}`)} + + +

{greeting}

+{/if} +``` + +Declaration tags are only allowed as an immediate child of a block — `{#if ...}`, `{#each ...}`, `{#snippet ...}` and so on — a `` or a ``. diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 01b2dd1494..0186985de2 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -399,6 +399,18 @@ Invalid selector Cannot declare a variable with the same name as an import from ` + +{#if id} + {let name = $state(await id)} + {let greeting = $derived(await `Hello ${name}`)} + + +

{greeting}

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/markup-declaration-tags/main.svelte b/packages/svelte/tests/runtime-runes/samples/markup-declaration-tags/main.svelte new file mode 100644 index 0000000000..28adec813b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/markup-declaration-tags/main.svelte @@ -0,0 +1,18 @@ + + + + +{#if visible} + {let counter = $state({ value: initial })} + {let doubled = $derived(counter.value * 2)} + {var suffix = ' total'} + {function format(value) { + return `${value}${suffix}`; + }} + + +

{format(doubled)}

+{/if} diff --git a/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_config.js b/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_config.js new file mode 100644 index 0000000000..232ae9dcd7 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + props: { + items: [1, 3] + } +}); diff --git a/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_expected.html b/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_expected.html new file mode 100644 index 0000000000..d299c763b5 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_expected.html @@ -0,0 +1 @@ +

value: 2

value: 6

diff --git a/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/main.svelte b/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/main.svelte new file mode 100644 index 0000000000..aee0a30f3c --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/main.svelte @@ -0,0 +1,11 @@ + + +{#each items as item} + {const doubled = item * 2} + {function label(value) { + return `value: ${value}`; + }} +

{label(doubled)}

+{/each} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/errors.json new file mode 100644 index 0000000000..39e0b5c07d --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_tag_invalid_type", + "message": "Declaration tags must be `let`, `const`, `var` or `function` declarations", + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 7 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte new file mode 100644 index 0000000000..3dd25b21db --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte @@ -0,0 +1,3 @@ +{#if true} + {class Foo {}} +{/if} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-placement/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-placement/errors.json new file mode 100644 index 0000000000..bf8427ee89 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-placement/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_tag_invalid_placement", + "message": "Declaration tags must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, `` or ``", + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 15 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-placement/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-placement/input.svelte new file mode 100644 index 0000000000..2d9c799deb --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-placement/input.svelte @@ -0,0 +1,5 @@ + + +{let b = a + 1} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 3f71d44177..dda06bfc64 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -852,7 +852,7 @@ declare module 'svelte/attachments' { declare module 'svelte/compiler' { import type { SourceMap } from 'magic-string'; - import type { ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression, SourceLocation } from 'estree'; + import type { ArrayExpression, ArrowFunctionExpression, FunctionDeclaration, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression, SourceLocation } from 'estree'; import type { Location } from 'locate-character'; import type { default as ts } from 'esrap/languages/ts'; /** @@ -1303,6 +1303,12 @@ declare module 'svelte/compiler' { }; } + /** A `{let ...}`, `{const ...}`, `{var ...}` or `{function ...}` tag */ + export interface DeclarationTag extends BaseNode { + type: 'DeclarationTag'; + declaration: VariableDeclaration | FunctionDeclaration; + } + /** A `{@debug ...}` tag */ export interface DebugTag extends BaseNode { type: 'DebugTag'; @@ -1613,6 +1619,7 @@ declare module 'svelte/compiler' { export type Tag = | AST.AttachTag | AST.ConstTag + | AST.DeclarationTag | AST.DebugTag | AST.ExpressionTag | AST.HtmlTag