Skip to content

Commit a283083

Browse files
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 <[email protected]>
1 parent a39605e commit a283083

File tree

11 files changed

+98
-39
lines changed

11 files changed

+98
-39
lines changed

.changeset/sixty-zoos-enjoy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: support `#each` without `as`

documentation/docs/03-template-syntax/03-each.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,30 @@ You can freely use destructuring and rest patterns in each blocks.
7474
{/each}
7575
```
7676

77+
## Each blocks without an item
78+
79+
```svelte
80+
<!--- copy: false --->
81+
{#each expression}...{/each}
82+
```
83+
84+
```svelte
85+
<!--- copy: false --->
86+
{#each expression, index}...{/each}
87+
```
88+
89+
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=)):
90+
91+
```svelte
92+
<div class="chess-board">
93+
{#each { length: 8 }, rank}
94+
{#each { length: 8 }, file}
95+
<div class:black={(rank + file) % 2 === 1}></div>
96+
{/each}
97+
{/each}
98+
</div>
99+
```
100+
77101
## Else blocks
78102

79103
```svelte

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

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { ArrowFunctionExpression, Expression, Identifier } from 'estree' */
1+
/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { Parser } from '../index.js' */
44
import read_pattern from '../read/context.js';
@@ -142,16 +142,25 @@ function open(parser) {
142142
parser.index = end;
143143
}
144144
}
145-
parser.eat('as', true);
146-
parser.require_whitespace();
147-
148-
const context = read_pattern(parser);
149-
150-
parser.allow_whitespace();
151145

146+
/** @type {Pattern | null} */
147+
let context = null;
152148
let index;
153149
let key;
154150

151+
if (parser.eat('as')) {
152+
parser.require_whitespace();
153+
154+
context = read_pattern(parser);
155+
} else {
156+
// {#each Array.from({ length: 10 }), i} is read as a sequence expression,
157+
// which is set back above - we now gotta reset the index as a consequence
158+
// to properly read the , i part
159+
parser.index = /** @type {number} */ (expression.end);
160+
}
161+
162+
parser.allow_whitespace();
163+
155164
if (parser.eat(',')) {
156165
parser.allow_whitespace();
157166
index = parser.read_identifier();

packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function EachBlock(node, context) {
1616
validate_block_not_empty(node.fallback, context);
1717

1818
const id = node.context;
19-
if (id.type === 'Identifier' && (id.name === '$state' || id.name === '$derived')) {
19+
if (id?.type === 'Identifier' && (id.name === '$state' || id.name === '$derived')) {
2020
// TODO weird that this is necessary
2121
e.state_invalid_placement(node, id.name);
2222
}

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ export function EachBlock(node, context) {
4747

4848
const key_is_item =
4949
node.key?.type === 'Identifier' &&
50-
node.context.type === 'Identifier' &&
51-
node.context.name === node.key.name;
50+
node.context?.type === 'Identifier' &&
51+
node.context?.name === node.key.name;
5252

5353
// if the each block expression references a store subscription, we need
5454
// to use mutable stores internally
@@ -147,7 +147,7 @@ export function EachBlock(node, context) {
147147
// which needs a reference to the index
148148
const index =
149149
each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index);
150-
const item = node.context.type === 'Identifier' ? node.context : b.id('$$item');
150+
const item = node.context?.type === 'Identifier' ? node.context : b.id('$$item');
151151

152152
let uses_index = each_node_meta.contains_group_binding;
153153
let key_uses_index = false;
@@ -185,7 +185,7 @@ export function EachBlock(node, context) {
185185
if (!context.state.analysis.runes) sequence.push(invalidate);
186186
if (invalidate_store) sequence.push(invalidate_store);
187187

188-
if (node.context.type === 'Identifier') {
188+
if (node.context?.type === 'Identifier') {
189189
const binding = /** @type {Binding} */ (context.state.scope.get(node.context.name));
190190

191191
child_state.transform[node.context.name] = {
@@ -218,7 +218,7 @@ export function EachBlock(node, context) {
218218
};
219219

220220
delete key_state.transform[node.context.name];
221-
} else {
221+
} else if (node.context) {
222222
const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item;
223223

224224
for (const path of extract_paths(node.context)) {
@@ -260,11 +260,12 @@ export function EachBlock(node, context) {
260260
let key_function = b.id('$.index');
261261

262262
if (node.metadata.keyed) {
263+
const pattern = /** @type {Pattern} */ (node.context); // can only be keyed when a context is provided
263264
const expression = /** @type {Expression} */ (
264265
context.visit(/** @type {Expression} */ (node.key), key_state)
265266
);
266267

267-
key_function = b.arrow(key_uses_index ? [node.context, index] : [node.context], expression);
268+
key_function = b.arrow(key_uses_index ? [pattern, index] : [pattern], expression);
268269
}
269270

270271
if (node.index && each_node_meta.contains_group_binding) {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ export function EachBlock(node, context) {
2121
state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection)));
2222

2323
/** @type {Statement[]} */
24-
const each = [b.let(/** @type {Pattern} */ (node.context), b.member(array_id, index, true))];
24+
const each = [];
25+
26+
if (node.context) {
27+
each.push(b.let(node.context, b.member(array_id, index, true)));
28+
}
2529

2630
if (index.name !== node.index && node.index != null) {
2731
each.push(b.let(node.index, index));

packages/svelte/src/compiler/phases/scope.js

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -527,31 +527,33 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
527527
const scope = state.scope.child();
528528
scopes.set(node, scope);
529529

530-
// declarations
531-
for (const id of extract_identifiers(node.context)) {
532-
const binding = scope.declare(id, 'each', 'const');
533-
534-
let inside_rest = false;
535-
let is_rest_id = false;
536-
walk(node.context, null, {
537-
Identifier(node) {
538-
if (inside_rest && node === id) {
539-
is_rest_id = true;
530+
if (node.context) {
531+
// declarations
532+
for (const id of extract_identifiers(node.context)) {
533+
const binding = scope.declare(id, 'each', 'const');
534+
535+
let inside_rest = false;
536+
let is_rest_id = false;
537+
walk(node.context, null, {
538+
Identifier(node) {
539+
if (inside_rest && node === id) {
540+
is_rest_id = true;
541+
}
542+
},
543+
RestElement(_, { next }) {
544+
const prev = inside_rest;
545+
inside_rest = true;
546+
next();
547+
inside_rest = prev;
540548
}
541-
},
542-
RestElement(_, { next }) {
543-
const prev = inside_rest;
544-
inside_rest = true;
545-
next();
546-
inside_rest = prev;
547-
}
548-
});
549+
});
549550

550-
binding.metadata = { inside_rest: is_rest_id };
551-
}
551+
binding.metadata = { inside_rest: is_rest_id };
552+
}
552553

553-
// Visit to pick up references from default initializers
554-
visit(node.context, { scope });
554+
// Visit to pick up references from default initializers
555+
visit(node.context, { scope });
556+
}
555557

556558
if (node.index) {
557559
const is_keyed =

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,8 @@ export namespace AST {
401401
export interface EachBlock extends BaseNode {
402402
type: 'EachBlock';
403403
expression: Expression;
404-
context: Pattern;
404+
/** The `entry` in `{#each item as entry}`. `null` if `as` part is omitted */
405+
context: Pattern | null;
405406
body: Fragment;
406407
fallback?: Fragment;
407408
index?: string;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `<div>hi</div> <div>hi</div> <div>0</div> <div>1</div>`
5+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{#each [10, 20]}
2+
<div>hi</div>
3+
{/each}
4+
5+
{#each [10, 20], i}
6+
<div>{i}</div>
7+
{/each}

packages/svelte/types/index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1193,7 +1193,8 @@ declare module 'svelte/compiler' {
11931193
export interface EachBlock extends BaseNode {
11941194
type: 'EachBlock';
11951195
expression: Expression;
1196-
context: Pattern;
1196+
/** The `entry` in `{#each item as entry}`. `null` if `as` part is omitted */
1197+
context: Pattern | null;
11971198
body: Fragment;
11981199
fallback?: Fragment;
11991200
index?: string;

0 commit comments

Comments
 (0)