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.
pull/8654/head
Simon H 2 years ago committed by GitHub
parent 5dd707d4f5
commit d9698551fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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 ...}

@ -1,2 +1,2 @@
// This file is automatically generated
export default new Set(["HtmlTag","HtmlTagHydration","ResizeObserverSingleton","SvelteComponent","SvelteComponentDev","SvelteComponentTyped","SvelteElement","action_destroyer","add_attribute","add_classes","add_flush_callback","add_iframe_resize_listener","add_location","add_render_callback","add_styles","add_transform","afterUpdate","append","append_dev","append_empty_stylesheet","append_hydration","append_hydration_dev","append_styles","assign","attr","attr_dev","attribute_to_object","beforeUpdate","bind","binding_callbacks","blank_object","bubble","check_outros","children","claim_comment","claim_component","claim_element","claim_html_tag","claim_space","claim_svg_element","claim_text","clear_loops","comment","component_subscribe","compute_rest_props","compute_slots","construct_svelte_component","construct_svelte_component_dev","contenteditable_truthy_values","createEventDispatcher","create_animation","create_bidirectional_transition","create_component","create_custom_element","create_in_transition","create_out_transition","create_slot","create_ssr_component","current_component","custom_event","dataset_dev","debug","destroy_block","destroy_component","destroy_each","detach","detach_after_dev","detach_before_dev","detach_between_dev","detach_dev","dirty_components","dispatch_dev","each","element","element_is","empty","end_hydrating","escape","escape_attribute_value","escape_object","exclude_internal_props","fix_and_destroy_block","fix_and_outro_and_destroy_block","fix_position","flush","flush_render_callbacks","getAllContexts","getContext","get_all_dirty_from_scope","get_binding_group_value","get_current_component","get_custom_elements_slots","get_root_for_style","get_slot_changes","get_spread_object","get_spread_update","get_store_value","get_svelte_dataset","globals","group_outros","handle_promise","hasContext","has_prop","head_selector","identity","init","init_binding_group","init_binding_group_dynamic","insert","insert_dev","insert_hydration","insert_hydration_dev","intros","invalid_attribute_name_character","is_client","is_crossorigin","is_empty","is_function","is_promise","is_void","listen","listen_dev","loop","loop_guard","merge_ssr_styles","missing_component","mount_component","noop","not_equal","now","null_to_empty","object_without_properties","onDestroy","onMount","once","outro_and_destroy_block","prevent_default","prop_dev","query_selector_all","raf","resize_observer_border_box","resize_observer_content_box","resize_observer_device_pixel_content_box","run","run_all","safe_not_equal","schedule_update","select_multiple_value","select_option","select_options","select_value","self","setContext","set_attributes","set_current_component","set_custom_element_data","set_custom_element_data_map","set_data","set_data_contenteditable","set_data_contenteditable_dev","set_data_dev","set_data_maybe_contenteditable","set_data_maybe_contenteditable_dev","set_dynamic_element_data","set_input_type","set_input_value","set_now","set_raf","set_store_value","set_style","set_svg_attributes","space","split_css_unit","spread","src_url_equal","start_hydrating","stop_immediate_propagation","stop_propagation","subscribe","svg_element","text","tick","time_ranges_to_array","to_number","toggle_class","transition_in","transition_out","trusted","update_await_block_branch","update_keyed_each","update_slot","update_slot_base","validate_component","validate_dynamic_element","validate_each_argument","validate_each_keys","validate_slots","validate_store","validate_void_dynamic_element","xlink_attr"]);
export default new Set(["HtmlTag","HtmlTagHydration","ResizeObserverSingleton","SvelteComponent","SvelteComponentDev","SvelteComponentTyped","SvelteElement","action_destroyer","add_attribute","add_classes","add_flush_callback","add_iframe_resize_listener","add_location","add_render_callback","add_styles","add_transform","afterUpdate","append","append_dev","append_empty_stylesheet","append_hydration","append_hydration_dev","append_styles","assign","attr","attr_dev","attribute_to_object","beforeUpdate","bind","binding_callbacks","blank_object","bubble","check_outros","children","claim_comment","claim_component","claim_element","claim_html_tag","claim_space","claim_svg_element","claim_text","clear_loops","comment","component_subscribe","compute_rest_props","compute_slots","construct_svelte_component","construct_svelte_component_dev","contenteditable_truthy_values","createEventDispatcher","create_animation","create_bidirectional_transition","create_component","create_custom_element","create_in_transition","create_out_transition","create_slot","create_ssr_component","current_component","custom_event","dataset_dev","debug","destroy_block","destroy_component","destroy_each","detach","detach_after_dev","detach_before_dev","detach_between_dev","detach_dev","dirty_components","dispatch_dev","each","element","element_is","empty","end_hydrating","ensure_array_like","ensure_array_like_dev","escape","escape_attribute_value","escape_object","exclude_internal_props","fix_and_destroy_block","fix_and_outro_and_destroy_block","fix_position","flush","flush_render_callbacks","getAllContexts","getContext","get_all_dirty_from_scope","get_binding_group_value","get_current_component","get_custom_elements_slots","get_root_for_style","get_slot_changes","get_spread_object","get_spread_update","get_store_value","get_svelte_dataset","globals","group_outros","handle_promise","hasContext","has_prop","head_selector","identity","init","init_binding_group","init_binding_group_dynamic","insert","insert_dev","insert_hydration","insert_hydration_dev","intros","invalid_attribute_name_character","is_client","is_crossorigin","is_empty","is_function","is_promise","is_void","listen","listen_dev","loop","loop_guard","merge_ssr_styles","missing_component","mount_component","noop","not_equal","now","null_to_empty","object_without_properties","onDestroy","onMount","once","outro_and_destroy_block","prevent_default","prop_dev","query_selector_all","raf","resize_observer_border_box","resize_observer_content_box","resize_observer_device_pixel_content_box","run","run_all","safe_not_equal","schedule_update","select_multiple_value","select_option","select_options","select_value","self","setContext","set_attributes","set_current_component","set_custom_element_data","set_custom_element_data_map","set_data","set_data_contenteditable","set_data_contenteditable_dev","set_data_dev","set_data_maybe_contenteditable","set_data_maybe_contenteditable_dev","set_dynamic_element_data","set_input_type","set_input_value","set_now","set_raf","set_store_value","set_style","set_svg_attributes","space","split_css_unit","spread","src_url_equal","start_hydrating","stop_immediate_propagation","stop_propagation","subscribe","svg_element","text","tick","time_ranges_to_array","to_number","toggle_class","transition_in","transition_out","trusted","update_await_block_branch","update_keyed_each","update_slot","update_slot_base","validate_component","validate_dynamic_element","validate_each_keys","validate_slots","validate_store","validate_void_dynamic_element","xlink_attr"]);

@ -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 = {
@ -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();`}
${
@ -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) {

@ -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
@ -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);
}
/**

@ -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);

@ -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';

@ -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 =
@ -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);

@ -7,6 +7,7 @@ import {
detach_dev,
dispatch_dev,
element,
ensure_array_like_dev,
init,
insert_dev,
noop,
@ -14,7 +15,6 @@ import {
set_data_dev,
space,
text,
validate_each_argument,
validate_slots
} from "svelte/internal";
@ -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) {
@ -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) {

@ -7,6 +7,7 @@ import {
detach_dev,
dispatch_dev,
element,
ensure_array_like_dev,
init,
insert_dev,
noop,
@ -14,7 +15,6 @@ import {
set_data_dev,
space,
text,
validate_each_argument,
validate_slots
} from "svelte/internal";
@ -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) {
@ -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) {

@ -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";
@ -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) {
@ -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) {

@ -6,6 +6,7 @@ import {
detach,
element,
empty,
ensure_array_like,
init,
insert,
noop,
@ -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) {
@ -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) {

@ -6,6 +6,7 @@ import {
detach,
element,
empty,
ensure_array_like,
init,
insert,
noop,
@ -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) {
@ -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) {

@ -7,6 +7,7 @@ import {
destroy_each,
detach,
element,
ensure_array_like,
init,
insert,
noop,
@ -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) {
@ -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) {

@ -6,6 +6,7 @@ import {
detach,
element,
empty,
ensure_array_like,
fix_and_destroy_block,
fix_position,
init,
@ -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) {
@ -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();

@ -6,6 +6,7 @@ import {
detach,
element,
empty,
ensure_array_like,
init,
insert,
noop,
@ -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) {
@ -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);
}
},

@ -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,

@ -1,7 +0,0 @@
export default {
compileOptions: {
dev: true
},
error:
'{#each} only iterates over array-like objects. You can use a spread to convert this iterable into an array.'
};

@ -1,7 +0,0 @@
<script>
const foo = new Set([1, 2, 3]);
</script>
{#each foo as item}
<div>{item}</div>
{/each}

@ -2,5 +2,5 @@ export default {
compileOptions: {
dev: true
},
error: '{#each} only iterates over array-like objects.'
error: '{#each} only works with iterable values.'
};

@ -0,0 +1,29 @@
export default {
html: `
<p>1</p>
<p>2</p>
<p>1 0</p>
<p>2 1</p>
<p>1 0</p>
<p>2 1</p>
`,
test({ assert, component, target }) {
component.numbers = new Set([2, 3]);
assert.htmlEqual(
target.innerHTML,
`
<p>2</p>
<p>3</p>
<p>2 0</p>
<p>3 1</p>
<p>2 0</p>
<p>3 1</p>
`
);
}
};

@ -0,0 +1,15 @@
<script>
export let numbers = new Set([1, 2]);
</script>
{#each numbers as i}
<p>{i}</p>
{/each}
{#each numbers as i, index}
<p>{i} {index}</p>
{/each}
{#each numbers as i, index (i)}
<p>{i} {index}</p>
{/each}
Loading…
Cancel
Save