Skip to content

feat: each without as #14396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sixty-zoos-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: support `#each` without `as`
24 changes: 24 additions & 0 deletions documentation/docs/03-template-syntax/03-each.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,30 @@ You can freely use destructuring and rest patterns in each blocks.
{/each}
```

## Each blocks without an item

```svelte
<!--- copy: false --->
{#each expression}...{/each}
```

```svelte
<!--- copy: false --->
{#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
<div class="chess-board">
{#each { length: 8 }, rank}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to search for the meanings of rank and file.

rank iterates over the rows (length: 8 means there are 8 rows).
file iterates over the columns for each row (length: 8 means there are 8 columns).

Instead of chess-specific terminology, why not use more generic names like rowIndex and columnIndex?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because now you know something that you didn't know before

{#each { length: 8 }, file}
<div class:black={(rank + file) % 2 === 1}></div>
{/each}
{/each}
</div>
```

## Else blocks

```svelte
Expand Down
23 changes: 16 additions & 7 deletions packages/svelte/src/compiler/phases/1-parse/state/tag.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -143,16 +143,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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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] = {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
46 changes: 24 additions & 22 deletions packages/svelte/src/compiler/phases/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/compiler/types/template.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { test } from '../../test';

export default test({
html: `<div>hi</div> <div>hi</div> <div>0</div> <div>1</div>`
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{#each [10, 20]}
<div>hi</div>
{/each}

{#each [10, 20], i}
<div>{i}</div>
{/each}
3 changes: 2 additions & 1 deletion packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1188,7 +1188,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;
Expand Down