diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts index a7a061ad58..b94b6f2ea3 100644 --- a/src/compile/nodes/EachBlock.ts +++ b/src/compile/nodes/EachBlock.ts @@ -16,7 +16,7 @@ export default class EachBlock extends Node { iterations: string; index: string; context: string; - key: string; + key: Expression; scope: TemplateScope; destructuredContexts: string[]; @@ -29,7 +29,10 @@ export default class EachBlock extends Node { this.expression = new Expression(compiler, this, scope, info.expression); this.context = info.context; this.index = info.index; - this.key = info.key; + + this.key = info.key + ? new Expression(compiler, this, scope, info.key) + : null; this.scope = scope.child(); @@ -270,7 +273,7 @@ export default class EachBlock extends Node { mountOrIntro, } ) { - const key = block.getUniqueName('key'); + const get_key = block.getUniqueName('get_key'); const blocks = block.getUniqueName(`${each}_blocks`); const lookup = block.getUniqueName(`${each}_lookup`); @@ -290,11 +293,14 @@ export default class EachBlock extends Node { } block.builders.init.addBlock(deindent` + const ${get_key} = ctx => ${this.key.snippet}; + for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) { - var ${key} = ${each_block_value}[#i].${this.key}; - ${blocks}[#i] = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign(@assign({}, ctx), { + let child_ctx = @assign(@assign({}, ctx), { ${this.contextProps.join(',\n')} - })); + }); + let key = ${get_key}(child_ctx); + ${blocks}[#i] = ${lookup}[key] = ${create_each_block}(#component, key, child_ctx); } `); @@ -321,7 +327,7 @@ export default class EachBlock extends Node { block.builders.update.addBlock(deindent` var ${each_block_value} = ${snippet}; - ${blocks} = @updateKeyedEach(${blocks}, #component, changed, "${this.key}", ${dynamic ? '1' : '0'}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, function(#i) { + ${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, function(#i) { return @assign(@assign({}, ctx), { ${this.contextProps.join(',\n')} }); diff --git a/src/parse/state/mustache.ts b/src/parse/state/mustache.ts index ed15da9702..1033b64069 100644 --- a/src/parse/state/mustache.ts +++ b/src/parse/state/mustache.ts @@ -299,23 +299,7 @@ export default function mustache(parser: Parser) { if (parser.eat('(')) { parser.allowWhitespace(); - const expression = readExpression(parser); - - // TODO eventually, we should accept any expression, and turn - // it into a function. For now, assume that every expression - // follows the `foo.id` pattern, and equates to `@id` - if ( - expression.type !== 'MemberExpression' || - expression.property.computed || - expression.property.type !== 'Identifier' - ) { - parser.error({ - code: `invalid-key`, - message: 'invalid key' - }, expression.start); - } - - block.key = expression.property.name; + block.key = readExpression(parser); parser.allowWhitespace(); parser.eat(')', true); parser.allowWhitespace(); diff --git a/src/shared/keyed-each.js b/src/shared/keyed-each.js index 6592058b3b..278acde6db 100644 --- a/src/shared/keyed-each.js +++ b/src/shared/keyed-each.js @@ -10,7 +10,7 @@ export function outroAndDestroyBlock(block, lookup) { }); } -export function updateKeyedEach(old_blocks, component, changed, key_prop, dynamic, list, lookup, node, has_outro, create_each_block, intro_method, next, get_context) { +export function updateKeyedEach(old_blocks, component, changed, get_key, dynamic, list, lookup, node, has_outro, create_each_block, intro_method, next, get_context) { var o = old_blocks.length; var n = list.length; @@ -24,14 +24,15 @@ export function updateKeyedEach(old_blocks, component, changed, key_prop, dynami var i = n; while (i--) { - var key = list[i][key_prop]; + var ctx = get_context(i); + var key = get_key(ctx); var block = lookup[key]; if (!block) { - block = create_each_block(component, key, get_context(i)); + block = create_each_block(component, key, ctx); block.c(); } else if (dynamic) { - block.p(changed, get_context(i)); + block.p(changed, ctx); } new_blocks[i] = new_lookup[key] = block; diff --git a/test/parser/samples/each-block-keyed/output.json b/test/parser/samples/each-block-keyed/output.json index 461992347b..c4cbf98b9e 100644 --- a/test/parser/samples/each-block-keyed/output.json +++ b/test/parser/samples/each-block-keyed/output.json @@ -1,5 +1,4 @@ { - "hash": "1x6az5m", "html": { "start": 0, "end": 54, @@ -38,7 +37,24 @@ } ], "context": "todo", - "key": "id" + "key": { + "type": "MemberExpression", + "start": 22, + "end": 29, + "object": { + "type": "Identifier", + "start": 22, + "end": 26, + "name": "todo" + }, + "property": { + "type": "Identifier", + "start": 27, + "end": 29, + "name": "id" + }, + "computed": false + } } ] }, diff --git a/test/runtime/samples/each-block-keyed-non-prop/_config.js b/test/runtime/samples/each-block-keyed-non-prop/_config.js new file mode 100644 index 0000000000..5141422211 --- /dev/null +++ b/test/runtime/samples/each-block-keyed-non-prop/_config.js @@ -0,0 +1,31 @@ +export default { + data: { + words: ['foo', 'bar', 'baz'] + }, + + html: ` +

foo

+

bar

+

baz

+ `, + + test(assert, component, target) { + const [p1, p2, p3] = target.querySelectorAll('p'); + + component.set({ + words: ['foo', 'baz'], + }); + + assert.htmlEqual(target.innerHTML, ` +

foo

+

baz

+ `); + + const [p4, p5] = target.querySelectorAll('p'); + + assert.ok(!target.contains(p2), '

element should be removed'); + + assert.equal(p1, p4, 'first

element should be retained'); + assert.equal(p3, p5, 'last

element should be retained'); + }, +}; diff --git a/test/runtime/samples/each-block-keyed-non-prop/main.html b/test/runtime/samples/each-block-keyed-non-prop/main.html new file mode 100644 index 0000000000..a3d28bb3e4 --- /dev/null +++ b/test/runtime/samples/each-block-keyed-non-prop/main.html @@ -0,0 +1,3 @@ +{#each words as word (word)} +

{word}

+{/each}