diff --git a/.changeset/curvy-cups-cough.md b/.changeset/curvy-cups-cough.md new file mode 100644 index 000000000..e16cb8377 --- /dev/null +++ b/.changeset/curvy-cups-cough.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +breaking: snippets can now take multiple arguments, support default parameters. Because of this, the type signature has changed diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 7e440ae13..e7d4fe807 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -68,7 +68,7 @@ export type ToggleEventHandler = EventHandler { // Implicit children prop every element has // Add this here so that libraries doing `$props()` don't need a separate interface - children?: import('svelte').Snippet; + children?: import('svelte').Snippet; // Clipboard Events 'on:copy'?: ClipboardEventHandler | undefined | null; diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1d6168fa3..01c0369a1 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -10,6 +10,7 @@ }, "files": [ "src", + "!src/**/*.test.*", "types", "compiler.cjs", "*.d.ts", diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 98741e29f..5c73007a3 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -90,7 +90,10 @@ const parse = { 'duplicate-script-element': () => `A component can have a single top-level + +{#snippet foo({ count }, { doubled })} +

clicks: {count}, doubled: {doubled}

+{/snippet} + +{@render foo({ count }, { doubled })} + + diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-argument-multiple/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-argument-multiple/_config.js new file mode 100644 index 000000000..e756ff5cc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-argument-multiple/_config.js @@ -0,0 +1,21 @@ +import { test } from '../../test'; + +export default test({ + html: ` +

clicks: 0, doubled: 0

+ + `, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + await btn?.click(); + assert.htmlEqual( + target.innerHTML, + ` +

clicks: 1, doubled: 2

+ + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-argument-multiple/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-argument-multiple/main.svelte new file mode 100644 index 000000000..6de6a8564 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-argument-multiple/main.svelte @@ -0,0 +1,14 @@ + + +{#snippet foo(n: number, doubled: number)} +

clicks: {n}, doubled: {doubled}

+{/snippet} + +{@render foo(count, doubled)} + + diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-complicated-defaults/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-complicated-defaults/_config.js new file mode 100644 index 000000000..4a403b328 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-complicated-defaults/_config.js @@ -0,0 +1,31 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + /** @type {HTMLButtonElement | null} */ + const increment = target.querySelector('#increment'); + /** @type {HTMLButtonElement | null} */ + const change_ref = target.querySelector('#change-ref'); + /** @type {HTMLParagraphElement | null} */ + const count = target.querySelector('#count'); + /** @type {HTMLParagraphElement | null} */ + const fallback_count = target.querySelector('#fallback-count'); + + assert.htmlEqual(count?.innerHTML ?? '', 'Count: 0'); + assert.htmlEqual(fallback_count?.innerHTML ?? '', 'Fallback count: 0'); + + await increment?.click(); + assert.htmlEqual(count?.innerHTML ?? '', 'Count: 1'); + assert.htmlEqual(fallback_count?.innerHTML ?? '', 'Fallback count: 0'); + + await change_ref?.click(); + await increment?.click(); + assert.htmlEqual(count?.innerHTML ?? '', 'Count: 1'); + assert.htmlEqual(fallback_count?.innerHTML ?? '', 'Fallback count: 1'); + + await change_ref?.click(); + await increment?.click(); + assert.htmlEqual(count?.innerHTML ?? '', 'Count: 2'); + assert.htmlEqual(fallback_count?.innerHTML ?? '', 'Fallback count: 1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-complicated-defaults/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-complicated-defaults/main.svelte new file mode 100644 index 000000000..cd3106757 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-complicated-defaults/main.svelte @@ -0,0 +1,26 @@ + + +{#snippet counter(c = count)} +

Count: {count.value}

+

Fallback count: {fallback_count.value}

+ + +{/snippet} + +{@render counter(toggle_state ? fallback_count : undefined)} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-optional-arguments/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-optional-arguments/_config.js new file mode 100644 index 000000000..203cb1c43 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-optional-arguments/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const count = target.querySelector('button'); + const fallback = target.querySelector('p'); + + assert.htmlEqual(count?.innerHTML ?? '', '0'); + assert.htmlEqual(fallback?.innerHTML ?? '', 'fallback'); + + await count?.click(); + assert.htmlEqual(count?.innerHTML ?? '', '1'); + assert.htmlEqual(fallback?.innerHTML ?? '', 'fallback'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-optional-arguments/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-optional-arguments/main.svelte new file mode 100644 index 000000000..03d3c29c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-optional-arguments/main.svelte @@ -0,0 +1,28 @@ + + +{#snippet counter(c)} + {#if c} + + {:else} +

fallback

+ {/if} +{/snippet} + +{@render counter()} +{@render counter(count)} + diff --git a/packages/svelte/tests/types/snippet.ts b/packages/svelte/tests/types/snippet.ts index 5a1e46c24..edc5aba12 100644 --- a/packages/svelte/tests/types/snippet.ts +++ b/packages/svelte/tests/types/snippet.ts @@ -20,18 +20,18 @@ const d: Snippet = (a: string, b: number) => { const e: Snippet = (a: string) => { return return_type; }; +// @ts-expect-error const f: Snippet = (a) => { - // @ts-expect-error a?.x; return return_type; }; -const g: Snippet = (a) => { +const g: Snippet<[boolean]> = (a) => { // @ts-expect-error a === ''; a === true; return return_type; }; -const h: Snippet<{ a: true }> = (a) => { +const h: Snippet<[{ a: true }]> = (a) => { a.a === true; return return_type; }; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 0e6a7cde8..06cc79118 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -24,7 +24,7 @@ declare module 'svelte' { (Props extends { children?: any } ? {} : Slots extends { default: any } - ? { children?: Snippet } + ? { children?: Snippet<[]> } : {}); /** @@ -191,11 +191,20 @@ declare module 'svelte' { * ``` * You can only call a snippet through the `{@render ...}` tag. */ - export interface Snippet { - (arg: T): typeof SnippetReturn & { - _: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"'; - }; - } + export type Snippet = + // this conditional allows tuples but not arrays. Arrays would indicate a + // rest parameter type, which is not supported. If rest parameters are added + // in the future, the condition can be removed. + number extends T['length'] + ? never + : { + ( + this: void, + ...args: T + ): typeof SnippetReturn & { + _: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"'; + }; + }; interface DispatchOptions { cancelable?: boolean; @@ -316,7 +325,9 @@ declare module 'svelte' { new (options: ComponentConstructorOptions | undefined; + children?: ((this: void) => unique symbol & { + _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\""; + }) | undefined; })>): SvelteComponent; }, options: { target: Node; @@ -339,7 +350,9 @@ declare module 'svelte' { new (options: ComponentConstructorOptions | undefined; + children?: ((this: void) => unique symbol & { + _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\""; + }) | undefined; })>): SvelteComponent; }, options: { target: Node; @@ -479,7 +492,7 @@ declare module 'svelte/animate' { } declare module 'svelte/compiler' { - import type { AssignmentExpression, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, ArrayExpression, MemberExpression, ObjectExpression, Pattern, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, FunctionExpression, Node, Program } from 'estree'; + import type { AssignmentExpression, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, ArrayExpression, MemberExpression, ObjectExpression, Pattern, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, FunctionExpression, Node, Program, SpreadElement } from 'estree'; import type { Location } from 'locate-character'; import type { SourceMap } from 'magic-string'; import type { Context } from 'zimmerframe'; @@ -1180,7 +1193,7 @@ declare module 'svelte/compiler' { interface RenderTag extends BaseNode { type: 'RenderTag'; expression: Identifier; - argument: null | Expression; + arguments: Array; } type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag; @@ -1450,7 +1463,7 @@ declare module 'svelte/compiler' { interface SnippetBlock extends BaseNode { type: 'SnippetBlock'; expression: Identifier; - context: null | Pattern; + parameters: Pattern[]; body: Fragment; } @@ -1727,7 +1740,9 @@ declare module 'svelte/legacy' { } ? {} : Slots extends { default: any; } ? { - children?: Snippet | undefined; + children?: ((this: void) => unique symbol & { + _: "functions passed to {@render ...} tags must use the `Snippet` type imported from \"svelte\""; + }) | undefined; } : {})>): SvelteComponent; } & Exports; // This should contain all the public interfaces (not all of them are actually importable, check current Svelte for which ones are). @@ -1755,7 +1770,7 @@ declare module 'svelte/legacy' { (Props extends { children?: any } ? {} : Slots extends { default: any } - ? { children?: Snippet } + ? { children?: Snippet<[]> } : {}); /** @@ -1853,11 +1868,20 @@ declare module 'svelte/legacy' { * ``` * You can only call a snippet through the `{@render ...}` tag. */ - interface Snippet { - (arg: T): typeof SnippetReturn & { - _: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"'; - }; - } + type Snippet = + // this conditional allows tuples but not arrays. Arrays would indicate a + // rest parameter type, which is not supported. If rest parameters are added + // in the future, the condition can be removed. + number extends T['length'] + ? never + : { + ( + this: void, + ...args: T + ): typeof SnippetReturn & { + _: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"'; + }; + }; } declare module 'svelte/motion' { diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md index b227a9a9d..850de82f3 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/03-snippets.md @@ -58,7 +58,7 @@ Snippets, and _render tags_, are a way to create reusable chunks of markup insid {/each} ``` -A snippet can have at most one parameter. You can destructure it, just like a function argument ([demo](/#H4sIAAAAAAAAE5VTYW-bMBD9KyeiKYlEY4jWfSAk2n5H6QcXDmwVbMs2SzuL_z6DTRqp2rQJ2Ycfd_ced2eXtLxHkxRPLhF0wKRIfiiVpIl9V_PB_MTeoj8bOep6RkpTa67spRKV7dECH2iHBs7wNCOVdcFU1ui6gC2zVpmCEMVrMw4HxaSVhnzLMnLMsm26Ol95Y1kBHr9BDHnHbAHHO6ymynIpfF7LuAncwKgBCj0Xrx_5mMb2jh3f6KB6PNRy2AaXKf1fuY__KPfxj3KlQGikL5aQdpUxm-dTJUryUVdRsvwSqEviX2fIbYzgSvmCt7wbNe4ceMUpRIoUFkkpBBkw7ZfMZXC-BLKSDx3Q3p5djJrA-SR-X4K9DdHT6u-jo-flFlKSO3ThIDcSR6LIKUhGWrN1QGhs16LLbXgbjoe5U1PkozCfzu7uy2WtpfuuUTSo1_9ffPZrJKGLoyuwNxjBv0Q4wmdSR2aFi9jS2Pc-FIrlEKeilcI-GP4LfVtxOM1gyO1XSLp6vtD6tdNyFE0BV8YtngKuaNNw0RWQx_jKDlR33M9E5h-PQhZxfxEt6gIaLdWDYbSR191RvcFXv_LMb7p7obssXZ5Dvt_f9HgzdzZKibOZZ9mXmHkdTTpaefqsd4OIay4_hksd_I0fZMNbjk1SWD3i9Dz9BpdEPu8sBAAA)): +Snippet parameters can be destructured ([demo](/#H4sIAAAAAAAAE5VTYW-bMBD9KyeiKYlEY4jWfSAk2n5H6QcXDmwVbMs2SzuL_z6DTRqp2rQJ2Ycfd_ced2eXtLxHkxRPLhF0wKRIfiiVpIl9V_PB_MTeoj8bOep6RkpTa67spRKV7dECH2iHBs7wNCOVdcFU1ui6gC2zVpmCEMVrMw4HxaSVhnzLMnLMsm26Ol95Y1kBHr9BDHnHbAHHO6ymynIpfF7LuAncwKgBCj0Xrx_5mMb2jh3f6KB6PNRy2AaXKf1fuY__KPfxj3KlQGikL5aQdpUxm-dTJUryUVdRsvwSqEviX2fIbYzgSvmCt7wbNe4ceMUpRIoUFkkpBBkw7ZfMZXC-BLKSDx3Q3p5djJrA-SR-X4K9DdHT6u-jo-flFlKSO3ThIDcSR6LIKUhGWrN1QGhs16LLbXgbjoe5U1PkozCfzu7uy2WtpfuuUTSo1_9ffPZrJKGLoyuwNxjBv0Q4wmdSR2aFi9jS2Pc-FIrlEKeilcI-GP4LfVtxOM1gyO1XSLp6vtD6tdNyFE0BV8YtngKuaNNw0RWQx_jKDlR33M9E5h-PQhZxfxEt6gIaLdWDYbSR191RvcFXv_LMb7p7obssXZ5Dvt_f9HgzdzZKibOZZ9mXmHkdTTpaefqsd4OIay4_hksd_I0fZMNbjk1SWD3i9Dz9BpdEPu8sBAAA)): ```svelte {#snippet figure({ src, caption, width, height })} @@ -69,6 +69,8 @@ A snippet can have at most one parameter. You can destructure it, just like a fu {/snippet} ``` +Like function declarations, snippets can have an arbitrary number of parameters, which can have default values. You cannot use rest parameters however. + ## Snippet scope Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the ` +``` + +With this change, red squigglies will appear if you try and use the component without providing a `data` prop and a `row` snippet. Notice that the type argument provided to `Snippet` is a tuple, since snippets can have multiple parameters. + +We can tighten things up further by declaring a generic, so that `data` and `row` refer to the same type: + +```diff +- +``` + ## Snippets and slots In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/special-elements#slot). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5. They continue to work, however, and you can mix and match snippets and slots in your components. - -## Typing snippets - -Right now, it's not possible to add types for snippets and their parameters. This is something we hope to address before we ship Svelte 5. diff --git a/vitest.config.js b/vitest.config.js index 30e3917ca..2af320902 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -37,6 +37,7 @@ export default defineConfig({ dir: '.', reporters: ['dot'], include: [ + 'packages/svelte/**/*.test.ts', 'packages/svelte/tests/*/test.ts', 'packages/svelte/tests/runtime-browser/test-ssr.ts' ],