Skip to content

Commit

Permalink
feat: allow #each to iterate over iterables (#8626)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dummdidumm authored May 26, 2023
1 parent 5dd707d commit d969855
Show file tree
Hide file tree
Showing 22 changed files with 97 additions and 58 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
* Treat slots as if they don't exist when using CSS adjacent and general sibling combinators ([#8284](https://github.com/sveltejs/svelte/issues/8284))
* 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)).
* Explicitly disallow `var` declarations extending the reactive statement scope ([#6800](https://github.com/sveltejs/svelte/pull/6800))
* Allow `#each` to iterate over iterables like `Set`, `Map` etc ([#7425](https://github.com/sveltejs/svelte/issues/7425))
* Warn about `:` in attributes and props to prevent ambiguity with Svelte directives ([#6823](https://github.com/sveltejs/svelte/issues/6823))

## 3.59.1
Expand Down
2 changes: 2 additions & 0 deletions site/content/docs/03-template-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ An each block can also have an `{:else}` clause, which is rendered if the list i
{/each}
```

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.


### {#await ...}

Expand Down
2 changes: 1 addition & 1 deletion src/compiler/compile/internal_exports.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 1 addition & 6 deletions src/compiler/compile/render_dom/wrappers/EachBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,8 @@ export default class EachBlockWrapper extends Wrapper {
const needs_anchor = this.next
? !this.next.is_dom_node()
: !parent_node || !this.parent.is_dom_node();
const snippet = this.node.expression.manipulate(block);
const snippet = x`@ensure_array_like(${this.node.expression.manipulate(block)})`;
block.chunks.init.push(b`let ${this.vars.each_block_value} = ${snippet};`);
if (this.renderer.options.dev) {
block.chunks.init.push(b`@validate_each_argument(${this.vars.each_block_value});`);
}

/** @type {import('estree').Identifier} */
const initial_anchor_node = {
Expand Down Expand Up @@ -480,7 +477,6 @@ export default class EachBlockWrapper extends Wrapper {
this.block.maintain_context = true;
this.updates.push(b`
${this.vars.each_block_value} = ${snippet};
${this.renderer.options.dev && b`@validate_each_argument(${this.vars.each_block_value});`}
${this.block.has_outros && b`@group_outros();`}
${
Expand Down Expand Up @@ -628,7 +624,6 @@ export default class EachBlockWrapper extends Wrapper {
const update = b`
${!this.block.has_update_method && b`const #old_length = ${this.vars.each_block_value}.length;`}
${this.vars.each_block_value} = ${snippet};
${this.renderer.options.dev && b`@validate_each_argument(${this.vars.each_block_value});`}
let #i;
for (#i = ${start}; #i < ${data_length}; #i += 1) {
Expand Down
18 changes: 9 additions & 9 deletions src/runtime/internal/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SvelteComponent } from './Component.js';
import { is_void } from '../../shared/utils/names.js';
import { VERSION } from '../../shared/version.js';
import { contenteditable_truthy_values } from './utils.js';
import { ensure_array_like } from './each.js';

/**
* @template T
Expand Down Expand Up @@ -208,16 +209,15 @@ export function set_data_maybe_contenteditable_dev(text, data, attr_value) {
}
}

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

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { transition_in, transition_out } from './transitions.js';
import { run_all } from './utils.js';

// general each functions:

export function ensure_array_like(array_like_or_iterator) {
return array_like_or_iterator?.length !== undefined
? array_like_or_iterator
: Array.from(array_like_or_iterator);
}

// keyed each functions:

/** @returns {void} */
export function destroy_block(block, lookup) {
block.d(1);
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/internal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export * from './await_block.js';
export * from './dom.js';
export * from './environment.js';
export * from './globals.js';
export * from './keyed_each.js';
export * from './each.js';
export * from './lifecycle.js';
export * from './loop.js';
export * from './scheduler.js';
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/internal/ssr.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { set_current_component, current_component } from './lifecycle.js';
import { run_all, blank_object } from './utils.js';
import { boolean_attributes } from '../../shared/boolean_attributes.js';
import { ensure_array_like } from './each.js';
export { is_void } from '../../shared/utils/names.js';

export const invalid_attribute_name_character =
Expand Down Expand Up @@ -107,6 +108,7 @@ export function escape_object(obj) {

/** @returns {string} */
export function each(items, fn) {
items = ensure_array_like(items);
let str = '';
for (let i = 0; i < items.length; i += 1) {
str += fn(items[i], i);
Expand Down
8 changes: 3 additions & 5 deletions test/js/samples/debug-foo-bar-baz-things/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import {
detach_dev,
dispatch_dev,
element,
ensure_array_like_dev,
init,
insert_dev,
noop,
safe_not_equal,
set_data_dev,
space,
text,
validate_each_argument,
validate_slots
} from "svelte/internal";

Expand Down Expand Up @@ -89,8 +89,7 @@ function create_fragment(ctx) {
let p;
let t1;
let t2;
let each_value = /*things*/ ctx[0];
validate_each_argument(each_value);
let each_value = ensure_array_like_dev(/*things*/ ctx[0]);
let each_blocks = [];

for (let i = 0; i < each_value.length; i += 1) {
Expand Down Expand Up @@ -126,8 +125,7 @@ function create_fragment(ctx) {
},
p: function update(ctx, [dirty]) {
if (dirty & /*things*/ 1) {
each_value = /*things*/ ctx[0];
validate_each_argument(each_value);
each_value = ensure_array_like_dev(/*things*/ ctx[0]);
let i;

for (i = 0; i < each_value.length; i += 1) {
Expand Down
8 changes: 3 additions & 5 deletions test/js/samples/debug-foo/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import {
detach_dev,
dispatch_dev,
element,
ensure_array_like_dev,
init,
insert_dev,
noop,
safe_not_equal,
set_data_dev,
space,
text,
validate_each_argument,
validate_slots
} from "svelte/internal";

Expand Down Expand Up @@ -83,8 +83,7 @@ function create_fragment(ctx) {
let p;
let t1;
let t2;
let each_value = /*things*/ ctx[0];
validate_each_argument(each_value);
let each_value = ensure_array_like_dev(/*things*/ ctx[0]);
let each_blocks = [];

for (let i = 0; i < each_value.length; i += 1) {
Expand Down Expand Up @@ -120,8 +119,7 @@ function create_fragment(ctx) {
},
p: function update(ctx, [dirty]) {
if (dirty & /*things*/ 1) {
each_value = /*things*/ ctx[0];
validate_each_argument(each_value);
each_value = ensure_array_like_dev(/*things*/ ctx[0]);
let i;

for (i = 0; i < each_value.length; i += 1) {
Expand Down
8 changes: 3 additions & 5 deletions test/js/samples/debug-no-dependencies/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import {
detach_dev,
dispatch_dev,
empty,
ensure_array_like_dev,
init,
insert_dev,
noop,
safe_not_equal,
space,
text,
validate_each_argument,
validate_slots
} from "svelte/internal";

Expand Down Expand Up @@ -65,8 +65,7 @@ function create_each_block(ctx) {

function create_fragment(ctx) {
let each_1_anchor;
let each_value = things;
validate_each_argument(each_value);
let each_value = ensure_array_like_dev(things);
let each_blocks = [];

for (let i = 0; i < each_value.length; i += 1) {
Expand Down Expand Up @@ -95,8 +94,7 @@ function create_fragment(ctx) {
},
p: function update(ctx, [dirty]) {
if (dirty & /*things*/ 0) {
each_value = things;
validate_each_argument(each_value);
each_value = ensure_array_like_dev(things);
let i;

for (i = 0; i < each_value.length; i += 1) {
Expand Down
5 changes: 3 additions & 2 deletions test/js/samples/deconflict-builtins/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
detach,
element,
empty,
ensure_array_like,
init,
insert,
noop,
Expand Down Expand Up @@ -46,7 +47,7 @@ function create_each_block(ctx) {

function create_fragment(ctx) {
let each_1_anchor;
let each_value = /*createElement*/ ctx[0];
let each_value = ensure_array_like(/*createElement*/ ctx[0]);
let each_blocks = [];

for (let i = 0; i < each_value.length; i += 1) {
Expand All @@ -72,7 +73,7 @@ function create_fragment(ctx) {
},
p(ctx, [dirty]) {
if (dirty & /*createElement*/ 1) {
each_value = /*createElement*/ ctx[0];
each_value = ensure_array_like(/*createElement*/ ctx[0]);
let i;

for (i = 0; i < each_value.length; i += 1) {
Expand Down
5 changes: 3 additions & 2 deletions test/js/samples/each-block-array-literal/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
detach,
element,
empty,
ensure_array_like,
init,
insert,
noop,
Expand Down Expand Up @@ -46,7 +47,7 @@ function create_each_block(ctx) {

function create_fragment(ctx) {
let each_1_anchor;
let each_value = [/*a*/ ctx[0], /*b*/ ctx[1], /*c*/ ctx[2], /*d*/ ctx[3], /*e*/ ctx[4]];
let each_value = ensure_array_like([/*a*/ ctx[0], /*b*/ ctx[1], /*c*/ ctx[2], /*d*/ ctx[3], /*e*/ ctx[4]]);
let each_blocks = [];

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

for (i = 0; i < 5; i += 1) {
Expand Down
5 changes: 3 additions & 2 deletions test/js/samples/each-block-changed-check/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
destroy_each,
detach,
element,
ensure_array_like,
init,
insert,
noop,
Expand Down Expand Up @@ -83,7 +84,7 @@ function create_fragment(ctx) {
let t0;
let p;
let t1;
let each_value = /*comments*/ ctx[0];
let each_value = ensure_array_like(/*comments*/ ctx[0]);
let each_blocks = [];

for (let i = 0; i < each_value.length; i += 1) {
Expand Down Expand Up @@ -113,7 +114,7 @@ function create_fragment(ctx) {
},
p(ctx, [dirty]) {
if (dirty & /*comments, elapsed, time*/ 7) {
each_value = /*comments*/ ctx[0];
each_value = ensure_array_like(/*comments*/ ctx[0]);
let i;

for (i = 0; i < each_value.length; i += 1) {
Expand Down
5 changes: 3 additions & 2 deletions test/js/samples/each-block-keyed-animated/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
detach,
element,
empty,
ensure_array_like,
fix_and_destroy_block,
fix_position,
init,
Expand Down Expand Up @@ -68,7 +69,7 @@ function create_fragment(ctx) {
let each_blocks = [];
let each_1_lookup = new Map();
let each_1_anchor;
let each_value = /*things*/ ctx[0];
let each_value = ensure_array_like(/*things*/ ctx[0]);
const get_key = ctx => /*thing*/ ctx[1].id;

for (let i = 0; i < each_value.length; i += 1) {
Expand Down Expand Up @@ -96,7 +97,7 @@ function create_fragment(ctx) {
},
p(ctx, [dirty]) {
if (dirty & /*things*/ 1) {
each_value = /*things*/ ctx[0];
each_value = ensure_array_like(/*things*/ ctx[0]);
for (let i = 0; i < each_blocks.length; i += 1) each_blocks[i].r();
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);
for (let i = 0; i < each_blocks.length; i += 1) each_blocks[i].a();
Expand Down
5 changes: 3 additions & 2 deletions test/js/samples/each-block-keyed/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
detach,
element,
empty,
ensure_array_like,
init,
insert,
noop,
Expand Down Expand Up @@ -53,7 +54,7 @@ function create_fragment(ctx) {
let each_blocks = [];
let each_1_lookup = new Map();
let each_1_anchor;
let each_value = /*things*/ ctx[0];
let each_value = ensure_array_like(/*things*/ ctx[0]);
const get_key = ctx => /*thing*/ ctx[1].id;

for (let i = 0; i < each_value.length; i += 1) {
Expand Down Expand Up @@ -81,7 +82,7 @@ function create_fragment(ctx) {
},
p(ctx, [dirty]) {
if (dirty & /*things*/ 1) {
each_value = /*things*/ ctx[0];
each_value = ensure_array_like(/*things*/ ctx[0]);
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);
}
},
Expand Down
2 changes: 1 addition & 1 deletion test/runtime/runtime.shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ async function run_test(dir) {
const dir = `${cwd}/_output/${hydrate ? 'hydratable' : 'normal'}`;
const out = `${dir}/${file.replace(/\.svelte$/, '.js')}`;

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

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

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export default {
compileOptions: {
dev: true
},
error: '{#each} only iterates over array-like objects.'
error: '{#each} only works with iterable values.'
};
Loading

0 comments on commit d969855

Please sign in to comment.