Skip to content

Commit d969855

Browse files
authored
feat: allow #each to iterate over iterables (#8626)
closes #7425 Uses a new ensure_array_like function to use Array.from in case the variable doesn't have a length property ('length' in 'some string' fails, therefore obj?.length). This ensures other places can stay unmodified. Using for (const x of y) constructs would require large changes across the each block code where it's uncertain that it would work for all cases since the array length is needed in various places.
1 parent 5dd707d commit d969855

File tree

22 files changed

+97
-58
lines changed

22 files changed

+97
-58
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
* Treat slots as if they don't exist when using CSS adjacent and general sibling combinators ([#8284](https://github.com/sveltejs/svelte/issues/8284))
3131
* Fix transitions so that they don't require a `style-src 'unsafe-inline'` Content Security Policy (CSP) ([#6662](https://github.com/sveltejs/svelte/issues/6662)).
3232
* Explicitly disallow `var` declarations extending the reactive statement scope ([#6800](https://github.com/sveltejs/svelte/pull/6800))
33+
* Allow `#each` to iterate over iterables like `Set`, `Map` etc ([#7425](https://github.com/sveltejs/svelte/issues/7425))
3334
* Warn about `:` in attributes and props to prevent ambiguity with Svelte directives ([#6823](https://github.com/sveltejs/svelte/issues/6823))
3435

3536
## 3.59.1

site/content/docs/03-template-syntax.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ An each block can also have an `{:else}` clause, which is rendered if the list i
296296
{/each}
297297
```
298298

299+
It is possible to iterate over iterables like `Map` or `Set`. Iterables need to be finite and static (they shouldn't change while being iterated over). Under the hood, they are transformed to an array using `Array.from` before being passed off to rendering. If you're writing performance-sensitive code, try to avoid iterables and use regular arrays as they are more performant.
300+
299301

300302
### {#await ...}
301303

src/compiler/compile/internal_exports.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/compiler/compile/render_dom/wrappers/EachBlock.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,8 @@ export default class EachBlockWrapper extends Wrapper {
206206
const needs_anchor = this.next
207207
? !this.next.is_dom_node()
208208
: !parent_node || !this.parent.is_dom_node();
209-
const snippet = this.node.expression.manipulate(block);
209+
const snippet = x`@ensure_array_like(${this.node.expression.manipulate(block)})`;
210210
block.chunks.init.push(b`let ${this.vars.each_block_value} = ${snippet};`);
211-
if (this.renderer.options.dev) {
212-
block.chunks.init.push(b`@validate_each_argument(${this.vars.each_block_value});`);
213-
}
214211

215212
/** @type {import('estree').Identifier} */
216213
const initial_anchor_node = {
@@ -480,7 +477,6 @@ export default class EachBlockWrapper extends Wrapper {
480477
this.block.maintain_context = true;
481478
this.updates.push(b`
482479
${this.vars.each_block_value} = ${snippet};
483-
${this.renderer.options.dev && b`@validate_each_argument(${this.vars.each_block_value});`}
484480
485481
${this.block.has_outros && b`@group_outros();`}
486482
${
@@ -628,7 +624,6 @@ export default class EachBlockWrapper extends Wrapper {
628624
const update = b`
629625
${!this.block.has_update_method && b`const #old_length = ${this.vars.each_block_value}.length;`}
630626
${this.vars.each_block_value} = ${snippet};
631-
${this.renderer.options.dev && b`@validate_each_argument(${this.vars.each_block_value});`}
632627
633628
let #i;
634629
for (#i = ${start}; #i < ${data_length}; #i += 1) {

src/runtime/internal/dev.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { SvelteComponent } from './Component.js';
1212
import { is_void } from '../../shared/utils/names.js';
1313
import { VERSION } from '../../shared/version.js';
1414
import { contenteditable_truthy_values } from './utils.js';
15+
import { ensure_array_like } from './each.js';
1516

1617
/**
1718
* @template T
@@ -208,16 +209,15 @@ export function set_data_maybe_contenteditable_dev(text, data, attr_value) {
208209
}
209210
}
210211

211-
/**
212-
* @returns {void} */
213-
export function validate_each_argument(arg) {
214-
if (typeof arg !== 'string' && !(arg && typeof arg === 'object' && 'length' in arg)) {
215-
let msg = '{#each} only iterates over array-like objects.';
216-
if (typeof Symbol === 'function' && arg && Symbol.iterator in arg) {
217-
msg += ' You can use a spread to convert this iterable into an array.';
218-
}
219-
throw new Error(msg);
212+
export function ensure_array_like_dev(arg) {
213+
if (
214+
typeof arg !== 'string' &&
215+
!(arg && typeof arg === 'object' && 'length' in arg) &&
216+
!(typeof Symbol === 'function' && arg && Symbol.iterator in arg)
217+
) {
218+
throw new Error('{#each} only works with iterable values.');
220219
}
220+
return ensure_array_like(arg);
221221
}
222222

223223
/**

src/runtime/internal/keyed_each.js renamed to src/runtime/internal/each.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { transition_in, transition_out } from './transitions.js';
22
import { run_all } from './utils.js';
33

4+
// general each functions:
5+
6+
export function ensure_array_like(array_like_or_iterator) {
7+
return array_like_or_iterator?.length !== undefined
8+
? array_like_or_iterator
9+
: Array.from(array_like_or_iterator);
10+
}
11+
12+
// keyed each functions:
13+
414
/** @returns {void} */
515
export function destroy_block(block, lookup) {
616
block.d(1);

src/runtime/internal/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export * from './await_block.js';
33
export * from './dom.js';
44
export * from './environment.js';
55
export * from './globals.js';
6-
export * from './keyed_each.js';
6+
export * from './each.js';
77
export * from './lifecycle.js';
88
export * from './loop.js';
99
export * from './scheduler.js';

src/runtime/internal/ssr.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { set_current_component, current_component } from './lifecycle.js';
22
import { run_all, blank_object } from './utils.js';
33
import { boolean_attributes } from '../../shared/boolean_attributes.js';
4+
import { ensure_array_like } from './each.js';
45
export { is_void } from '../../shared/utils/names.js';
56

67
export const invalid_attribute_name_character =
@@ -107,6 +108,7 @@ export function escape_object(obj) {
107108

108109
/** @returns {string} */
109110
export function each(items, fn) {
111+
items = ensure_array_like(items);
110112
let str = '';
111113
for (let i = 0; i < items.length; i += 1) {
112114
str += fn(items[i], i);

test/js/samples/debug-foo-bar-baz-things/expected.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import {
77
detach_dev,
88
dispatch_dev,
99
element,
10+
ensure_array_like_dev,
1011
init,
1112
insert_dev,
1213
noop,
1314
safe_not_equal,
1415
set_data_dev,
1516
space,
1617
text,
17-
validate_each_argument,
1818
validate_slots
1919
} from "svelte/internal";
2020

@@ -89,8 +89,7 @@ function create_fragment(ctx) {
8989
let p;
9090
let t1;
9191
let t2;
92-
let each_value = /*things*/ ctx[0];
93-
validate_each_argument(each_value);
92+
let each_value = ensure_array_like_dev(/*things*/ ctx[0]);
9493
let each_blocks = [];
9594

9695
for (let i = 0; i < each_value.length; i += 1) {
@@ -126,8 +125,7 @@ function create_fragment(ctx) {
126125
},
127126
p: function update(ctx, [dirty]) {
128127
if (dirty & /*things*/ 1) {
129-
each_value = /*things*/ ctx[0];
130-
validate_each_argument(each_value);
128+
each_value = ensure_array_like_dev(/*things*/ ctx[0]);
131129
let i;
132130

133131
for (i = 0; i < each_value.length; i += 1) {

test/js/samples/debug-foo/expected.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import {
77
detach_dev,
88
dispatch_dev,
99
element,
10+
ensure_array_like_dev,
1011
init,
1112
insert_dev,
1213
noop,
1314
safe_not_equal,
1415
set_data_dev,
1516
space,
1617
text,
17-
validate_each_argument,
1818
validate_slots
1919
} from "svelte/internal";
2020

@@ -83,8 +83,7 @@ function create_fragment(ctx) {
8383
let p;
8484
let t1;
8585
let t2;
86-
let each_value = /*things*/ ctx[0];
87-
validate_each_argument(each_value);
86+
let each_value = ensure_array_like_dev(/*things*/ ctx[0]);
8887
let each_blocks = [];
8988

9089
for (let i = 0; i < each_value.length; i += 1) {
@@ -120,8 +119,7 @@ function create_fragment(ctx) {
120119
},
121120
p: function update(ctx, [dirty]) {
122121
if (dirty & /*things*/ 1) {
123-
each_value = /*things*/ ctx[0];
124-
validate_each_argument(each_value);
122+
each_value = ensure_array_like_dev(/*things*/ ctx[0]);
125123
let i;
126124

127125
for (i = 0; i < each_value.length; i += 1) {

test/js/samples/debug-no-dependencies/expected.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import {
55
detach_dev,
66
dispatch_dev,
77
empty,
8+
ensure_array_like_dev,
89
init,
910
insert_dev,
1011
noop,
1112
safe_not_equal,
1213
space,
1314
text,
14-
validate_each_argument,
1515
validate_slots
1616
} from "svelte/internal";
1717

@@ -65,8 +65,7 @@ function create_each_block(ctx) {
6565

6666
function create_fragment(ctx) {
6767
let each_1_anchor;
68-
let each_value = things;
69-
validate_each_argument(each_value);
68+
let each_value = ensure_array_like_dev(things);
7069
let each_blocks = [];
7170

7271
for (let i = 0; i < each_value.length; i += 1) {
@@ -95,8 +94,7 @@ function create_fragment(ctx) {
9594
},
9695
p: function update(ctx, [dirty]) {
9796
if (dirty & /*things*/ 0) {
98-
each_value = things;
99-
validate_each_argument(each_value);
97+
each_value = ensure_array_like_dev(things);
10098
let i;
10199

102100
for (i = 0; i < each_value.length; i += 1) {

test/js/samples/deconflict-builtins/expected.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
detach,
77
element,
88
empty,
9+
ensure_array_like,
910
init,
1011
insert,
1112
noop,
@@ -46,7 +47,7 @@ function create_each_block(ctx) {
4647

4748
function create_fragment(ctx) {
4849
let each_1_anchor;
49-
let each_value = /*createElement*/ ctx[0];
50+
let each_value = ensure_array_like(/*createElement*/ ctx[0]);
5051
let each_blocks = [];
5152

5253
for (let i = 0; i < each_value.length; i += 1) {
@@ -72,7 +73,7 @@ function create_fragment(ctx) {
7273
},
7374
p(ctx, [dirty]) {
7475
if (dirty & /*createElement*/ 1) {
75-
each_value = /*createElement*/ ctx[0];
76+
each_value = ensure_array_like(/*createElement*/ ctx[0]);
7677
let i;
7778

7879
for (i = 0; i < each_value.length; i += 1) {

test/js/samples/each-block-array-literal/expected.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
detach,
77
element,
88
empty,
9+
ensure_array_like,
910
init,
1011
insert,
1112
noop,
@@ -46,7 +47,7 @@ function create_each_block(ctx) {
4647

4748
function create_fragment(ctx) {
4849
let each_1_anchor;
49-
let each_value = [/*a*/ ctx[0], /*b*/ ctx[1], /*c*/ ctx[2], /*d*/ ctx[3], /*e*/ ctx[4]];
50+
let each_value = ensure_array_like([/*a*/ ctx[0], /*b*/ ctx[1], /*c*/ ctx[2], /*d*/ ctx[3], /*e*/ ctx[4]]);
5051
let each_blocks = [];
5152

5253
for (let i = 0; i < 5; i += 1) {
@@ -72,7 +73,7 @@ function create_fragment(ctx) {
7273
},
7374
p(ctx, [dirty]) {
7475
if (dirty & /*a, b, c, d, e*/ 31) {
75-
each_value = [/*a*/ ctx[0], /*b*/ ctx[1], /*c*/ ctx[2], /*d*/ ctx[3], /*e*/ ctx[4]];
76+
each_value = ensure_array_like([/*a*/ ctx[0], /*b*/ ctx[1], /*c*/ ctx[2], /*d*/ ctx[3], /*e*/ ctx[4]]);
7677
let i;
7778

7879
for (i = 0; i < 5; i += 1) {

test/js/samples/each-block-changed-check/expected.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
destroy_each,
88
detach,
99
element,
10+
ensure_array_like,
1011
init,
1112
insert,
1213
noop,
@@ -83,7 +84,7 @@ function create_fragment(ctx) {
8384
let t0;
8485
let p;
8586
let t1;
86-
let each_value = /*comments*/ ctx[0];
87+
let each_value = ensure_array_like(/*comments*/ ctx[0]);
8788
let each_blocks = [];
8889

8990
for (let i = 0; i < each_value.length; i += 1) {
@@ -113,7 +114,7 @@ function create_fragment(ctx) {
113114
},
114115
p(ctx, [dirty]) {
115116
if (dirty & /*comments, elapsed, time*/ 7) {
116-
each_value = /*comments*/ ctx[0];
117+
each_value = ensure_array_like(/*comments*/ ctx[0]);
117118
let i;
118119

119120
for (i = 0; i < each_value.length; i += 1) {

test/js/samples/each-block-keyed-animated/expected.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
detach,
77
element,
88
empty,
9+
ensure_array_like,
910
fix_and_destroy_block,
1011
fix_position,
1112
init,
@@ -68,7 +69,7 @@ function create_fragment(ctx) {
6869
let each_blocks = [];
6970
let each_1_lookup = new Map();
7071
let each_1_anchor;
71-
let each_value = /*things*/ ctx[0];
72+
let each_value = ensure_array_like(/*things*/ ctx[0]);
7273
const get_key = ctx => /*thing*/ ctx[1].id;
7374

7475
for (let i = 0; i < each_value.length; i += 1) {
@@ -96,7 +97,7 @@ function create_fragment(ctx) {
9697
},
9798
p(ctx, [dirty]) {
9899
if (dirty & /*things*/ 1) {
99-
each_value = /*things*/ ctx[0];
100+
each_value = ensure_array_like(/*things*/ ctx[0]);
100101
for (let i = 0; i < each_blocks.length; i += 1) each_blocks[i].r();
101102
each_blocks = update_keyed_each(each_blocks, dirty, get_key, 1, ctx, each_value, each_1_lookup, each_1_anchor.parentNode, fix_and_destroy_block, create_each_block, each_1_anchor, get_each_context);
102103
for (let i = 0; i < each_blocks.length; i += 1) each_blocks[i].a();

test/js/samples/each-block-keyed/expected.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
detach,
77
element,
88
empty,
9+
ensure_array_like,
910
init,
1011
insert,
1112
noop,
@@ -53,7 +54,7 @@ function create_fragment(ctx) {
5354
let each_blocks = [];
5455
let each_1_lookup = new Map();
5556
let each_1_anchor;
56-
let each_value = /*things*/ ctx[0];
57+
let each_value = ensure_array_like(/*things*/ ctx[0]);
5758
const get_key = ctx => /*thing*/ ctx[1].id;
5859

5960
for (let i = 0; i < each_value.length; i += 1) {
@@ -81,7 +82,7 @@ function create_fragment(ctx) {
8182
},
8283
p(ctx, [dirty]) {
8384
if (dirty & /*things*/ 1) {
84-
each_value = /*things*/ ctx[0];
85+
each_value = ensure_array_like(/*things*/ ctx[0]);
8586
each_blocks = update_keyed_each(each_blocks, dirty, get_key, 1, ctx, each_value, each_1_lookup, each_1_anchor.parentNode, destroy_block, create_each_block, each_1_anchor, get_each_context);
8687
}
8788
},

test/runtime/runtime.shared.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ async function run_test(dir) {
214214
const dir = `${cwd}/_output/${hydrate ? 'hydratable' : 'normal'}`;
215215
const out = `${dir}/${file.replace(/\.svelte$/, '.js')}`;
216216

217-
mkdirp(dir);
217+
mkdirp(path.dirname(out)); // file could be in subdirectory, therefore don't use dir
218218

219219
const { js } = compile(fs.readFileSync(`${cwd}/${file}`, 'utf-8').replace(/\r/g, ''), {
220220
...compileOptions,

test/runtime/samples/dev-warning-each-block-no-sets-maps/_config.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

test/runtime/samples/dev-warning-each-block-no-sets-maps/main.svelte

Lines changed: 0 additions & 7 deletions
This file was deleted.

test/runtime/samples/dev-warning-each-block-require-arraylike/_config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ export default {
22
compileOptions: {
33
dev: true
44
},
5-
error: '{#each} only iterates over array-like objects.'
5+
error: '{#each} only works with iterable values.'
66
};

0 commit comments

Comments
 (0)