From a283083751e3fa40d50945a362ba1586fb4f36b0 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:19:48 +0100 Subject: [PATCH] feat: each without as (#14396) * feat: each without as WIP closes #8348 * properly * docs * changeset * real world demo * simplify * typo --------- Co-authored-by: Rich Harris --- .changeset/sixty-zoos-enjoy.md | 5 ++ .../docs/03-template-syntax/03-each.md | 24 ++++++++++ .../src/compiler/phases/1-parse/state/tag.js | 23 +++++++--- .../phases/2-analyze/visitors/EachBlock.js | 2 +- .../3-transform/client/visitors/EachBlock.js | 13 +++--- .../3-transform/server/visitors/EachBlock.js | 6 ++- packages/svelte/src/compiler/phases/scope.js | 46 ++++++++++--------- .../svelte/src/compiler/types/template.d.ts | 3 +- .../samples/each-without-as/_config.js | 5 ++ .../samples/each-without-as/main.svelte | 7 +++ packages/svelte/types/index.d.ts | 3 +- 11 files changed, 98 insertions(+), 39 deletions(-) create mode 100644 .changeset/sixty-zoos-enjoy.md create mode 100644 packages/svelte/tests/runtime-runes/samples/each-without-as/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/each-without-as/main.svelte diff --git a/.changeset/sixty-zoos-enjoy.md b/.changeset/sixty-zoos-enjoy.md new file mode 100644 index 0000000000..71f3db3e1f --- /dev/null +++ b/.changeset/sixty-zoos-enjoy.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: support `#each` without `as` diff --git a/documentation/docs/03-template-syntax/03-each.md b/documentation/docs/03-template-syntax/03-each.md index e4246b6e9a..df0ba4d8f5 100644 --- a/documentation/docs/03-template-syntax/03-each.md +++ b/documentation/docs/03-template-syntax/03-each.md @@ -74,6 +74,30 @@ You can freely use destructuring and rest patterns in each blocks. {/each} ``` +## Each blocks without an item + +```svelte + +{#each expression}...{/each} +``` + +```svelte + +{#each expression, index}...{/each} +``` + +In case you just want to render something `n` times, you can omit the `as` part ([demo](/playground/untitled#H4sIAAAAAAAAE3WR0W7CMAxFf8XKNAk0WsSeUEaRpn3Guoc0MbQiJFHiMlDVf18SOrZJ48259_jaVgZmxBEZZ28thgCNFV6xBdt1GgPj7wOji0t2EqI-wa_OleGEmpLWiID_6dIaQkMxhm1UdwKpRQhVzWSaVORJNdvWpqbhAYVsYQCNZk8thzWMC_DCHMZk3wPSThNQ088I3mghD9UwSwHwlLE5PMIzVFUFq3G7WUZ2OyUvU3JOuZU332wCXTRmtPy1NgzXZtUFp8WFw9536uWqpbIgPEaDsJBW90cTOHh0KGi2XsBq5-cT6-3nPauxXqHnsHJnCFZ3CvJVkyuCQ0mFF9TZyCQ162WGvteLKfG197Y3iv_pz_fmS68Hxt8iPBPj5HscP8YvCNX7uhYCAAA=)): + +```svelte +
+ {#each { length: 8 }, rank} + {#each { length: 8 }, file} +
+ {/each} + {/each} +
+``` + ## Else blocks ```svelte diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 3366c9ec97..317afe6f2f 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -1,4 +1,4 @@ -/** @import { ArrowFunctionExpression, Expression, Identifier } from 'estree' */ +/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { Parser } from '../index.js' */ import read_pattern from '../read/context.js'; @@ -142,16 +142,25 @@ function open(parser) { parser.index = end; } } - parser.eat('as', true); - parser.require_whitespace(); - - const context = read_pattern(parser); - - parser.allow_whitespace(); + /** @type {Pattern | null} */ + let context = null; let index; let key; + if (parser.eat('as')) { + parser.require_whitespace(); + + context = read_pattern(parser); + } else { + // {#each Array.from({ length: 10 }), i} is read as a sequence expression, + // which is set back above - we now gotta reset the index as a consequence + // to properly read the , i part + parser.index = /** @type {number} */ (expression.end); + } + + parser.allow_whitespace(); + if (parser.eat(',')) { parser.allow_whitespace(); index = parser.read_identifier(); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js index ac9e75bf8c..bd6c936f99 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js @@ -16,7 +16,7 @@ export function EachBlock(node, context) { validate_block_not_empty(node.fallback, context); const id = node.context; - if (id.type === 'Identifier' && (id.name === '$state' || id.name === '$derived')) { + if (id?.type === 'Identifier' && (id.name === '$state' || id.name === '$derived')) { // TODO weird that this is necessary e.state_invalid_placement(node, id.name); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 55d7ded247..d34f39f4c7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -47,8 +47,8 @@ export function EachBlock(node, context) { const key_is_item = node.key?.type === 'Identifier' && - node.context.type === 'Identifier' && - node.context.name === node.key.name; + node.context?.type === 'Identifier' && + node.context?.name === node.key.name; // if the each block expression references a store subscription, we need // to use mutable stores internally @@ -147,7 +147,7 @@ export function EachBlock(node, context) { // which needs a reference to the index const index = each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index); - const item = node.context.type === 'Identifier' ? node.context : b.id('$$item'); + const item = node.context?.type === 'Identifier' ? node.context : b.id('$$item'); let uses_index = each_node_meta.contains_group_binding; let key_uses_index = false; @@ -185,7 +185,7 @@ export function EachBlock(node, context) { if (!context.state.analysis.runes) sequence.push(invalidate); if (invalidate_store) sequence.push(invalidate_store); - if (node.context.type === 'Identifier') { + if (node.context?.type === 'Identifier') { const binding = /** @type {Binding} */ (context.state.scope.get(node.context.name)); child_state.transform[node.context.name] = { @@ -218,7 +218,7 @@ export function EachBlock(node, context) { }; delete key_state.transform[node.context.name]; - } else { + } else if (node.context) { const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item; for (const path of extract_paths(node.context)) { @@ -260,11 +260,12 @@ export function EachBlock(node, context) { let key_function = b.id('$.index'); if (node.metadata.keyed) { + const pattern = /** @type {Pattern} */ (node.context); // can only be keyed when a context is provided const expression = /** @type {Expression} */ ( context.visit(/** @type {Expression} */ (node.key), key_state) ); - key_function = b.arrow(key_uses_index ? [node.context, index] : [node.context], expression); + key_function = b.arrow(key_uses_index ? [pattern, index] : [pattern], expression); } if (node.index && each_node_meta.contains_group_binding) { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js index 478bb355a7..104f1f2405 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js @@ -21,7 +21,11 @@ export function EachBlock(node, context) { state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection))); /** @type {Statement[]} */ - const each = [b.let(/** @type {Pattern} */ (node.context), b.member(array_id, index, true))]; + const each = []; + + if (node.context) { + each.push(b.let(node.context, b.member(array_id, index, true))); + } if (index.name !== node.index && node.index != null) { each.push(b.let(node.index, index)); diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 454bb8c34e..7f22aa7c87 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -527,31 +527,33 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { const scope = state.scope.child(); scopes.set(node, scope); - // declarations - for (const id of extract_identifiers(node.context)) { - const binding = scope.declare(id, 'each', 'const'); - - let inside_rest = false; - let is_rest_id = false; - walk(node.context, null, { - Identifier(node) { - if (inside_rest && node === id) { - is_rest_id = true; + if (node.context) { + // declarations + for (const id of extract_identifiers(node.context)) { + const binding = scope.declare(id, 'each', 'const'); + + let inside_rest = false; + let is_rest_id = false; + walk(node.context, null, { + Identifier(node) { + if (inside_rest && node === id) { + is_rest_id = true; + } + }, + RestElement(_, { next }) { + const prev = inside_rest; + inside_rest = true; + next(); + inside_rest = prev; } - }, - RestElement(_, { next }) { - const prev = inside_rest; - inside_rest = true; - next(); - inside_rest = prev; - } - }); + }); - binding.metadata = { inside_rest: is_rest_id }; - } + binding.metadata = { inside_rest: is_rest_id }; + } - // Visit to pick up references from default initializers - visit(node.context, { scope }); + // Visit to pick up references from default initializers + visit(node.context, { scope }); + } if (node.index) { const is_keyed = diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index ede4e1693c..1758b98d24 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -401,7 +401,8 @@ export namespace AST { export interface EachBlock extends BaseNode { type: 'EachBlock'; expression: Expression; - context: Pattern; + /** The `entry` in `{#each item as entry}`. `null` if `as` part is omitted */ + context: Pattern | null; body: Fragment; fallback?: Fragment; index?: string; diff --git a/packages/svelte/tests/runtime-runes/samples/each-without-as/_config.js b/packages/svelte/tests/runtime-runes/samples/each-without-as/_config.js new file mode 100644 index 0000000000..29a59cf6dd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-without-as/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `
hi
hi
0
1
` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-without-as/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-without-as/main.svelte new file mode 100644 index 0000000000..975ba2667b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-without-as/main.svelte @@ -0,0 +1,7 @@ +{#each [10, 20]} +
hi
+{/each} + +{#each [10, 20], i} +
{i}
+{/each} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index a03b7de570..0814796d27 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1193,7 +1193,8 @@ declare module 'svelte/compiler' { export interface EachBlock extends BaseNode { type: 'EachBlock'; expression: Expression; - context: Pattern; + /** The `entry` in `{#each item as entry}`. `null` if `as` part is omitted */ + context: Pattern | null; body: Fragment; fallback?: Fragment; index?: string;