diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts index 6d14cb6e00..3bf6f08789 100644 --- a/src/compile/nodes/EachBlock.ts +++ b/src/compile/nodes/EachBlock.ts @@ -6,6 +6,7 @@ import createDebuggingComment from '../../utils/createDebuggingComment'; import Expression from './shared/Expression'; import mapChildren from './shared/mapChildren'; import TemplateScope from './shared/TemplateScope'; +import unpackDestructuring from '../../utils/unpackDestructuring'; export default class EachBlock extends Node { type: 'EachBlock'; @@ -18,7 +19,7 @@ export default class EachBlock extends Node { context: string; key: Expression; scope: TemplateScope; - destructuredContexts: string[]; + contexts: Array<{ name: string, tail: string }>; children: Node[]; else?: ElseBlock; @@ -27,7 +28,7 @@ export default class EachBlock extends Node { super(compiler, parent, scope, info); this.expression = new Expression(compiler, this, scope, info.expression); - this.context = info.context; + this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring this.index = info.index; this.key = info.key @@ -36,7 +37,12 @@ export default class EachBlock extends Node { this.scope = scope.child(); - this.scope.add(this.context, this.expression.dependencies); + this.contexts = []; + unpackDestructuring(this.contexts, info.context, ''); + + this.contexts.forEach(context => { + this.scope.add(context.key.name, this.expression.dependencies); + }); if (this.index) { // index can only change if this is a keyed each block @@ -44,12 +50,6 @@ export default class EachBlock extends Node { this.scope.add(this.index, dependencies); } - // TODO more general approach to destructuring - this.destructuredContexts = info.destructuredContexts || []; - this.destructuredContexts.forEach(name => { - this.scope.add(name, this.expression.dependencies); - }); - this.children = mapChildren(compiler, this, this.scope, info.children); this.else = info.else @@ -90,17 +90,13 @@ export default class EachBlock extends Node { this.block.getUniqueName(this.index); // this prevents name collisions (#1254) } - this.contextProps = [ + this.contextProps = this.contexts.map(prop => `${prop.key.name}: list[i]${prop.tail}`); + + // TODO only add these if necessary + this.contextProps.push( `${listName}: list`, - `${this.context}: list[i]`, `${indexName}: i` - ]; - - if (this.destructuredContexts) { - for (let i = 0; i < this.destructuredContexts.length; i += 1) { - this.contextProps.push(`${this.destructuredContexts[i]}: list[i][${i}]`); - } - } + ); this.compiler.target.blocks.push(this.block); this.initChildren(this.block, stripWhitespace, nextSibling); @@ -481,8 +477,7 @@ export default class EachBlock extends Node { const { compiler } = this; const { snippet } = this.expression; - const props = [`${this.context}: item`] - .concat(this.destructuredContexts.map((name, i) => `${name}: item[${i}]`)); + const props = this.contexts.map(prop => `${prop.key.name}: item${prop.tail}`); const getContext = this.index ? `(item, i) => Object.assign({}, ctx, { ${props.join(', ')}, ${this.index}: i })` diff --git a/src/parse/read/context.ts b/src/parse/read/context.ts new file mode 100644 index 0000000000..feb31d150e --- /dev/null +++ b/src/parse/read/context.ts @@ -0,0 +1,114 @@ +import { Parser } from '../index'; + +type Identifier = { + start: number; + end: number; + type: 'Identifier'; + name: string; +}; + +type Property = { + start: number; + end: number; + type: 'Property'; + key: Identifier; + value: Context; +}; + +type Context = { + start: number; + end: number; + type: 'Identifier' | 'ArrayPattern' | 'ObjectPattern'; + name?: string; + elements?: Context[]; + properties?: Property[]; +} + +function errorOnAssignmentPattern(parser: Parser) { + if (parser.eat('=')) { + parser.error({ + code: 'invalid-assignment-pattern', + message: 'Assignment patterns are not supported' + }, parser.index - 1); + } +} + +export default function readContext(parser: Parser) { + const context: Context = { + start: parser.index, + end: null, + type: null + }; + + if (parser.eat('[')) { + context.type = 'ArrayPattern'; + context.elements = []; + + do { + parser.allowWhitespace(); + context.elements.push(readContext(parser)); + parser.allowWhitespace(); + } while (parser.eat(',')); + + errorOnAssignmentPattern(parser); + parser.eat(']', true); + } + + else if (parser.eat('{')) { + context.type = 'ObjectPattern'; + context.properties = []; + + do { + parser.allowWhitespace(); + + const start = parser.index; + const name = parser.readIdentifier(); + const key: Identifier = { + start, + end: parser.index, + type: 'Identifier', + name + }; + parser.allowWhitespace(); + + const value = parser.eat(':') + ? readContext(parser) + : key; + + const property: Property = { + start, + end: value.end, + type: 'Property', + key, + value + }; + + context.properties.push(property); + + parser.allowWhitespace(); + } while (parser.eat(',')); + + errorOnAssignmentPattern(parser); + parser.eat('}', true); + } + + else { + const name = parser.readIdentifier(); + if (name) { + context.type = 'Identifier'; + context.end = parser.index; + context.name = name; + } + + else { + parser.error({ + code: 'invalid-context', + message: 'Expected a name, array pattern or object pattern' + }); + } + + errorOnAssignmentPattern(parser); + } + + return context; +} \ No newline at end of file diff --git a/src/parse/state/mustache.ts b/src/parse/state/mustache.ts index 1033b64069..77cbad1058 100644 --- a/src/parse/state/mustache.ts +++ b/src/parse/state/mustache.ts @@ -1,3 +1,4 @@ +import readContext from '../read/context'; import readExpression from '../read/expression'; import { whitespace } from '../../utils/patterns'; import { trimStart, trimEnd } from '../../utils/trim'; @@ -248,40 +249,7 @@ export default function mustache(parser: Parser) { parser.eat('as', true); parser.requireWhitespace(); - if (parser.eat('[')) { - parser.allowWhitespace(); - - block.destructuredContexts = []; - - do { - parser.allowWhitespace(); - - const destructuredContext = parser.readIdentifier(); - if (!destructuredContext) parser.error({ - code: `expected-name`, - message: `Expected name` - }); - - block.destructuredContexts.push(destructuredContext); - parser.allowWhitespace(); - } while (parser.eat(',')); - - if (!block.destructuredContexts.length) parser.error({ - code: `expected-name`, - message: `Expected name` - }); - - block.context = block.destructuredContexts.join('_'); - - parser.allowWhitespace(); - parser.eat(']', true); - } else { - block.context = parser.readIdentifier(); - if (!block.context) parser.error({ - code: `expected-name`, - message: `Expected name` - }); - } + block.context = readContext(parser); parser.allowWhitespace(); diff --git a/src/utils/unpackDestructuring.ts b/src/utils/unpackDestructuring.ts new file mode 100644 index 0000000000..2c48d5c762 --- /dev/null +++ b/src/utils/unpackDestructuring.ts @@ -0,0 +1,20 @@ +export default function unpackDestructuring( + contexts: Array<{ name: string, tail: string }>, + node: Node, + tail: string +) { + if (node.type === 'Identifier') { + contexts.push({ + key: node, + tail + }); + } else if (node.type === 'ArrayPattern') { + node.elements.forEach((element, i) => { + unpackDestructuring(contexts, element, `${tail}[${i}]`); + }); + } else if (node.type === 'ObjectPattern') { + node.properties.forEach((property) => { + unpackDestructuring(contexts, property.value, `${tail}.${property.key.name}`); + }); + } +} \ No newline at end of file diff --git a/src/validate/html/index.ts b/src/validate/html/index.ts index 8d1502dc3f..ed4eaf4d38 100644 --- a/src/validate/html/index.ts +++ b/src/validate/html/index.ts @@ -8,6 +8,7 @@ import fuzzymatch from '../utils/fuzzymatch' import flattenReference from '../../utils/flattenReference'; import { Validator } from '../index'; import { Node } from '../../interfaces'; +import unpackDestructuring from '../../utils/unpackDestructuring'; function isEmptyBlock(node: Node) { if (!/Block$/.test(node.type) || !node.children) return false; @@ -60,19 +61,17 @@ export default function validateHtml(validator: Validator, html: Node) { } else if (node.type === 'EachBlock') { - if (validator.helpers.has(node.context)) { - let c: number = node.expression.end; - - // find start of context - while (/\s/.test(validator.source[c])) c += 1; - c += 2; - while (/\s/.test(validator.source[c])) c += 1; - - validator.warn({ start: c, end: c + node.context.length }, { - code: `each-context-clash`, - message: `Context clashes with a helper. Rename one or the other to eliminate any ambiguity` - }); - } + const contexts = []; + unpackDestructuring(contexts, node.context, ''); + + contexts.forEach(prop => { + if (validator.helpers.has(prop.key.name)) { + validator.warn(prop.key, { + code: `each-context-clash`, + message: `Context clashes with a helper. Rename one or the other to eliminate any ambiguity` + }); + } + }); } if (validator.options.dev && isEmptyBlock(node)) { diff --git a/test/js/samples/deconflict-builtins/expected-bundle.js b/test/js/samples/deconflict-builtins/expected-bundle.js index 008d284885..208154d7f3 100644 --- a/test/js/samples/deconflict-builtins/expected-bundle.js +++ b/test/js/samples/deconflict-builtins/expected-bundle.js @@ -250,8 +250,8 @@ function create_each_block(component, ctx) { function get_each_context(ctx, list, i) { return assign(assign({}, ctx), { - each_value: list, node: list[i], + each_value: list, node_index: i }); } diff --git a/test/js/samples/deconflict-builtins/expected.js b/test/js/samples/deconflict-builtins/expected.js index 5ef4396ea0..a0e71964c4 100644 --- a/test/js/samples/deconflict-builtins/expected.js +++ b/test/js/samples/deconflict-builtins/expected.js @@ -98,8 +98,8 @@ function create_each_block(component, ctx) { function get_each_context(ctx, list, i) { return assign(assign({}, ctx), { - each_value: list, node: list[i], + each_value: list, node_index: i }); } diff --git a/test/js/samples/each-block-changed-check/expected-bundle.js b/test/js/samples/each-block-changed-check/expected-bundle.js index facaec2b93..b65f46deb2 100644 --- a/test/js/samples/each-block-changed-check/expected-bundle.js +++ b/test/js/samples/each-block-changed-check/expected-bundle.js @@ -297,8 +297,8 @@ function create_each_block(component, ctx) { function get_each_context(ctx, list, i) { return assign(assign({}, ctx), { - each_value: list, comment: list[i], + each_value: list, i: i }); } diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js index 05af12e226..3987454f83 100644 --- a/test/js/samples/each-block-changed-check/expected.js +++ b/test/js/samples/each-block-changed-check/expected.js @@ -143,8 +143,8 @@ function create_each_block(component, ctx) { function get_each_context(ctx, list, i) { return assign(assign({}, ctx), { - each_value: list, comment: list[i], + each_value: list, i: i }); } diff --git a/test/parser/samples/each-block-destructured/output.json b/test/parser/samples/each-block-destructured/output.json index 38d5ddc770..554b0cdcb0 100644 --- a/test/parser/samples/each-block-destructured/output.json +++ b/test/parser/samples/each-block-destructured/output.json @@ -1,5 +1,4 @@ { - "hash": "gtdm5e", "html": { "start": 0, "end": 62, @@ -54,11 +53,25 @@ ] } ], - "destructuredContexts": [ - "key", - "value" - ], - "context": "key_value" + "context": { + "start": 18, + "end": null, + "type": "ArrayPattern", + "elements": [ + { + "start": 19, + "end": 22, + "type": "Identifier", + "name": "key" + }, + { + "start": 24, + "end": 29, + "type": "Identifier", + "name": "value" + } + ] + } } ] }, diff --git a/test/parser/samples/each-block-else/output.json b/test/parser/samples/each-block-else/output.json index 9f8c5da79b..283b0f0418 100644 --- a/test/parser/samples/each-block-else/output.json +++ b/test/parser/samples/each-block-else/output.json @@ -1,5 +1,4 @@ { - "hash": "ljl07n", "html": { "start": 0, "end": 77, @@ -37,7 +36,12 @@ ] } ], - "context": "animal", + "context": { + "start": 18, + "end": 24, + "type": "Identifier", + "name": "animal" + }, "else": { "start": 50, "end": 70, diff --git a/test/parser/samples/each-block-indexed/output.json b/test/parser/samples/each-block-indexed/output.json index 9ffa02aaa8..1039e67b7c 100644 --- a/test/parser/samples/each-block-indexed/output.json +++ b/test/parser/samples/each-block-indexed/output.json @@ -1,5 +1,4 @@ { - "hash": "1143n2g", "html": { "start": 0, "end": 58, @@ -54,7 +53,12 @@ ] } ], - "context": "animal", + "context": { + "start": 18, + "end": 24, + "type": "Identifier", + "name": "animal" + }, "index": "i" } ] diff --git a/test/parser/samples/each-block-keyed/output.json b/test/parser/samples/each-block-keyed/output.json index c4cbf98b9e..e627c5c8c9 100644 --- a/test/parser/samples/each-block-keyed/output.json +++ b/test/parser/samples/each-block-keyed/output.json @@ -36,7 +36,12 @@ ] } ], - "context": "todo", + "context": { + "start": 16, + "end": 20, + "type": "Identifier", + "name": "todo" + }, "key": { "type": "MemberExpression", "start": 22, diff --git a/test/parser/samples/each-block/output.json b/test/parser/samples/each-block/output.json index 7df4a20eba..a92f3410d1 100644 --- a/test/parser/samples/each-block/output.json +++ b/test/parser/samples/each-block/output.json @@ -1,5 +1,4 @@ { - "hash": "mzeq0s", "html": { "start": 0, "end": 50, @@ -37,7 +36,12 @@ ] } ], - "context": "animal" + "context": { + "start": 18, + "end": 24, + "type": "Identifier", + "name": "animal" + } } ] }, diff --git a/test/parser/samples/unusual-identifier/output.json b/test/parser/samples/unusual-identifier/output.json index e4a290c0a6..64fbd4902b 100644 --- a/test/parser/samples/unusual-identifier/output.json +++ b/test/parser/samples/unusual-identifier/output.json @@ -1,5 +1,4 @@ { - "hash": "8weqxs", "html": { "start": 0, "end": 41, @@ -37,7 +36,12 @@ ] } ], - "context": "𐊧" + "context": { + "start": 17, + "end": 19, + "type": "Identifier", + "name": "𐊧" + } } ] }, diff --git a/test/runtime/samples/each-block-destructured-object/_config.js b/test/runtime/samples/each-block-destructured-object/_config.js new file mode 100644 index 0000000000..f5cb534447 --- /dev/null +++ b/test/runtime/samples/each-block-destructured-object/_config.js @@ -0,0 +1,22 @@ +export default { + data: { + animalPawsEntries: [ + { animal: 'raccoon', pawType: 'hands' }, + { animal: 'eagle', pawType: 'wings' } + ] + }, + + html: ` +
raccoon: hands
+eagle: wings
+ `, + + test ( assert, component, target ) { + component.set({ + animalPawsEntries: [{ animal: 'cow', pawType: 'hooves' }] + }); + assert.htmlEqual( target.innerHTML, ` +cow: hooves
+ `); + }, +}; diff --git a/test/runtime/samples/each-block-destructured-object/main.html b/test/runtime/samples/each-block-destructured-object/main.html new file mode 100644 index 0000000000..d759dabf9b --- /dev/null +++ b/test/runtime/samples/each-block-destructured-object/main.html @@ -0,0 +1,3 @@ +{#each animalPawsEntries as { animal, pawType } } +{animal}: {pawType}
+{/each}