diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts index d143cdd1ec..e605085f48 100644 --- a/src/compile/nodes/EachBlock.ts +++ b/src/compile/nodes/EachBlock.ts @@ -5,6 +5,7 @@ import map_children from './shared/map_children'; import TemplateScope from './shared/TemplateScope'; import AbstractBlock from './shared/AbstractBlock'; import { Node as INode } from '../../interfaces'; +import { new_tail } from '../utils/tail'; function unpack_destructuring(contexts: Array<{ name: string, tail: string }>, node: INode, tail: string) { if (!node) return; @@ -19,8 +20,20 @@ function unpack_destructuring(contexts: Array<{ name: string, tail: string }>, n unpack_destructuring(contexts, element, `${tail}[${i}]`); }); } else if (node.type === 'ObjectPattern') { + const used_properties = []; + node.properties.forEach((property) => { - unpack_destructuring(contexts, property.value, `${tail}.${property.key.name}`); + if (property.kind === 'rest') { + unpack_destructuring( + contexts, + property.value, + `@object_without_properties(${tail}, ${JSON.stringify(used_properties)})` + ); + } else { + used_properties.push(property.key.name); + + unpack_destructuring(contexts, property.value,`${tail}.${property.key.name}`); + } }); } } @@ -53,7 +66,7 @@ export default class EachBlock extends AbstractBlock { this.scope = scope.child(); this.contexts = []; - unpack_destructuring(this.contexts, info.context, ''); + unpack_destructuring(this.contexts, info.context, new_tail()); this.contexts.forEach(context => { this.scope.add(context.key.name, this.expression.dependencies, this); diff --git a/src/compile/render-dom/wrappers/EachBlock.ts b/src/compile/render-dom/wrappers/EachBlock.ts index 050107a573..fe5d5746e5 100644 --- a/src/compile/render-dom/wrappers/EachBlock.ts +++ b/src/compile/render-dom/wrappers/EachBlock.ts @@ -6,6 +6,7 @@ import EachBlock from '../../nodes/EachBlock'; import FragmentWrapper from './Fragment'; import deindent from '../../utils/deindent'; import ElseBlock from '../../nodes/ElseBlock'; +import { attach_head } from '../../utils/tail'; class ElseBlockWrapper extends Wrapper { node: ElseBlock; @@ -127,7 +128,7 @@ export default class EachBlockWrapper extends Wrapper { this.block.bindings.set(prop.key.name, { object: this.vars.each_block_value, property: this.index_name, - snippet: `${this.vars.each_block_value}[${this.index_name}]${prop.tail}` + snippet: attach_head(`${this.vars.each_block_value}[${this.index_name}]`, prop.tail) }); }); @@ -177,7 +178,7 @@ export default class EachBlockWrapper extends Wrapper { ? block.get_unique_name(`${this.var}_anchor`) : (this.next && this.next.var) || 'null'; - this.context_props = this.node.contexts.map(prop => `child_ctx.${prop.key.name} = list[i]${prop.tail};`); + this.context_props = this.node.contexts.map(prop => `child_ctx.${prop.key.name} = ${attach_head('list[i]', prop.tail)};`); if (this.node.has_binding) this.context_props.push(`child_ctx.${this.vars.each_block_value} = list;`); if (this.node.has_binding || this.node.index) this.context_props.push(`child_ctx.${this.index_name} = i;`); diff --git a/src/compile/utils/tail.ts b/src/compile/utils/tail.ts new file mode 100644 index 0000000000..87a3daf4fe --- /dev/null +++ b/src/compile/utils/tail.ts @@ -0,0 +1,7 @@ +export function new_tail(): string { + return '%%tail_head%%'; +} + +export function attach_head(head: string, tail: string): string { + return tail.replace('%%tail_head%%', head); +} diff --git a/src/internal/dom.js b/src/internal/dom.js index b84568a988..46ffaa8472 100644 --- a/src/internal/dom.js +++ b/src/internal/dom.js @@ -38,6 +38,16 @@ export function element(name) { return document.createElement(name); } +export function object_without_properties(obj, exclude) { + const target = {}; + for (const k in obj) { + if (Object.prototype.hasOwnProperty.call(obj, k) && exclude.indexOf(k) === -1) { + target[k] = obj[k]; + } + } + return target; +} + export function svg_element(name) { return document.createElementNS('http://www.w3.org/2000/svg', name); } diff --git a/src/parse/read/context.ts b/src/parse/read/context.ts index cece1c42d6..775e33b79e 100644 --- a/src/parse/read/context.ts +++ b/src/parse/read/context.ts @@ -11,7 +11,7 @@ type Property = { start: number; end: number; type: 'Property'; - kind: string; + kind: 'init' | 'rest'; shorthand: boolean; key: Identifier; value: Context; @@ -69,6 +69,41 @@ export default function read_context(parser: Parser) { do { parser.allow_whitespace(); + if (parser.eat('...')) { + parser.allow_whitespace(); + + const start = parser.index; + const name = parser.read_identifier(); + const key: Identifier = { + start, + end: parser.index, + type: 'Identifier', + name + } + const property: Property = { + start, + end: parser.index, + type: 'Property', + kind: 'rest', + shorthand: true, + key, + value: key + } + + context.properties.push(property); + + parser.allow_whitespace(); + + if (parser.eat(',')) { + parser.error({ + code: `comma-after-rest`, + message: `Comma is not permitted after the rest element` + }, parser.index - 1); + } + + break; + } + const start = parser.index; const name = parser.read_identifier(); const key: Identifier = { @@ -122,4 +157,4 @@ export default function read_context(parser: Parser) { } return context; -} \ No newline at end of file +} diff --git a/test/runtime/samples/each-block-destructured-object-rest/_config.js b/test/runtime/samples/each-block-destructured-object-rest/_config.js new file mode 100644 index 0000000000..561d4e8442 --- /dev/null +++ b/test/runtime/samples/each-block-destructured-object-rest/_config.js @@ -0,0 +1,20 @@ +export default { + props: { + animalEntries: [ + { animal: 'raccoon', class: 'mammal' }, + { animal: 'eagle', class: 'bird' } + ] + }, + + html: ` +

raccoon

+

eagle

+ `, + + test({ assert, component, target }) { + component.animalEntries = [{ animal: 'cow', class: 'mammal' }]; + assert.htmlEqual(target.innerHTML, ` +

cow

+ `); + }, +}; diff --git a/test/runtime/samples/each-block-destructured-object-rest/main.svelte b/test/runtime/samples/each-block-destructured-object-rest/main.svelte new file mode 100644 index 0000000000..9c51b0ec23 --- /dev/null +++ b/test/runtime/samples/each-block-destructured-object-rest/main.svelte @@ -0,0 +1,7 @@ + + +{#each animalEntries as { animal, ...props } } +

{animal}

+{/each} diff --git a/test/validator/samples/each-block-destructured-object-rest-comma-after/errors.json b/test/validator/samples/each-block-destructured-object-rest-comma-after/errors.json new file mode 100644 index 0000000000..549b6960eb --- /dev/null +++ b/test/validator/samples/each-block-destructured-object-rest-comma-after/errors.json @@ -0,0 +1,15 @@ +[{ + "code": "comma-after-rest", + "message": "Comma is not permitted after the rest element", + "pos": 100, + "start": { + "line": 5, + "column": 53, + "character": 100 + }, + "end": { + "line": 5, + "column": 53, + "character": 100 + } +}] diff --git a/test/validator/samples/each-block-destructured-object-rest-comma-after/input.svelte b/test/validator/samples/each-block-destructured-object-rest-comma-after/input.svelte new file mode 100644 index 0000000000..661b2dfea7 --- /dev/null +++ b/test/validator/samples/each-block-destructured-object-rest-comma-after/input.svelte @@ -0,0 +1,7 @@ + + +{#each animalEntries as { animal, features: { ...rest, eyes } } } +

{animal} {eyes}

+{/each}