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 <rich.harris@vercel.com>
pull/14501/head
Simon H 3 weeks ago committed by GitHub
parent a39605ec7a
commit a283083751
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: support `#each` without `as`

@ -74,6 +74,30 @@ You can freely use destructuring and rest patterns in each blocks.
{/each} {/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}
{#each { length: 8 }, file}
<div class:black={(rank + file) % 2 === 1}></div>
{/each}
{/each}
</div>
```
## Else blocks ## Else blocks
```svelte ```svelte

@ -1,4 +1,4 @@
/** @import { ArrowFunctionExpression, Expression, Identifier } from 'estree' */ /** @import { ArrowFunctionExpression, Expression, Identifier, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
import read_pattern from '../read/context.js'; import read_pattern from '../read/context.js';
@ -142,16 +142,25 @@ function open(parser) {
parser.index = end; 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 index;
let key; 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(',')) { if (parser.eat(',')) {
parser.allow_whitespace(); parser.allow_whitespace();
index = parser.read_identifier(); index = parser.read_identifier();

@ -16,7 +16,7 @@ export function EachBlock(node, context) {
validate_block_not_empty(node.fallback, context); validate_block_not_empty(node.fallback, context);
const id = node.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 // TODO weird that this is necessary
e.state_invalid_placement(node, id.name); e.state_invalid_placement(node, id.name);
} }

@ -47,8 +47,8 @@ export function EachBlock(node, context) {
const key_is_item = const key_is_item =
node.key?.type === 'Identifier' && node.key?.type === 'Identifier' &&
node.context.type === 'Identifier' && node.context?.type === 'Identifier' &&
node.context.name === node.key.name; node.context?.name === node.key.name;
// if the each block expression references a store subscription, we need // if the each block expression references a store subscription, we need
// to use mutable stores internally // to use mutable stores internally
@ -147,7 +147,7 @@ export function EachBlock(node, context) {
// which needs a reference to the index // which needs a reference to the index
const index = const index =
each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.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 uses_index = each_node_meta.contains_group_binding;
let key_uses_index = false; let key_uses_index = false;
@ -185,7 +185,7 @@ export function EachBlock(node, context) {
if (!context.state.analysis.runes) sequence.push(invalidate); if (!context.state.analysis.runes) sequence.push(invalidate);
if (invalidate_store) sequence.push(invalidate_store); 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)); const binding = /** @type {Binding} */ (context.state.scope.get(node.context.name));
child_state.transform[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]; delete key_state.transform[node.context.name];
} else { } else if (node.context) {
const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item; const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item;
for (const path of extract_paths(node.context)) { for (const path of extract_paths(node.context)) {
@ -260,11 +260,12 @@ export function EachBlock(node, context) {
let key_function = b.id('$.index'); let key_function = b.id('$.index');
if (node.metadata.keyed) { if (node.metadata.keyed) {
const pattern = /** @type {Pattern} */ (node.context); // can only be keyed when a context is provided
const expression = /** @type {Expression} */ ( const expression = /** @type {Expression} */ (
context.visit(/** @type {Expression} */ (node.key), key_state) 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) { if (node.index && each_node_meta.contains_group_binding) {

@ -21,7 +21,11 @@ export function EachBlock(node, context) {
state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection))); state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection)));
/** @type {Statement[]} */ /** @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) { if (index.name !== node.index && node.index != null) {
each.push(b.let(node.index, index)); each.push(b.let(node.index, index));

@ -527,31 +527,33 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
const scope = state.scope.child(); const scope = state.scope.child();
scopes.set(node, scope); scopes.set(node, scope);
// declarations if (node.context) {
for (const id of extract_identifiers(node.context)) { // declarations
const binding = scope.declare(id, 'each', 'const'); for (const id of extract_identifiers(node.context)) {
const binding = scope.declare(id, 'each', 'const');
let inside_rest = false;
let is_rest_id = false; let inside_rest = false;
walk(node.context, null, { let is_rest_id = false;
Identifier(node) { walk(node.context, null, {
if (inside_rest && node === id) { Identifier(node) {
is_rest_id = true; 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 to pick up references from default initializers
visit(node.context, { scope }); visit(node.context, { scope });
}
if (node.index) { if (node.index) {
const is_keyed = const is_keyed =

@ -401,7 +401,8 @@ export namespace AST {
export interface EachBlock extends BaseNode { export interface EachBlock extends BaseNode {
type: 'EachBlock'; type: 'EachBlock';
expression: Expression; expression: Expression;
context: Pattern; /** The `entry` in `{#each item as entry}`. `null` if `as` part is omitted */
context: Pattern | null;
body: Fragment; body: Fragment;
fallback?: Fragment; fallback?: Fragment;
index?: string; index?: string;

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<div>hi</div> <div>hi</div> <div>0</div> <div>1</div>`
});

@ -0,0 +1,7 @@
{#each [10, 20]}
<div>hi</div>
{/each}
{#each [10, 20], i}
<div>{i}</div>
{/each}

@ -1193,7 +1193,8 @@ declare module 'svelte/compiler' {
export interface EachBlock extends BaseNode { export interface EachBlock extends BaseNode {
type: 'EachBlock'; type: 'EachBlock';
expression: Expression; expression: Expression;
context: Pattern; /** The `entry` in `{#each item as entry}`. `null` if `as` part is omitted */
context: Pattern | null;
body: Fragment; body: Fragment;
fallback?: Fragment; fallback?: Fragment;
index?: string; index?: string;

Loading…
Cancel
Save