From e7f448d395a8cec59f1e8ed148394366cc6f785d Mon Sep 17 00:00:00 2001 From: PaulBGD Date: Mon, 5 Mar 2018 17:42:17 -0600 Subject: [PATCH 001/316] Add TypeScript definitions for store (fixes #1207) --- store.d.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 store.d.ts diff --git a/store.d.ts b/store.d.ts new file mode 100644 index 0000000000..db29eaaa2a --- /dev/null +++ b/store.d.ts @@ -0,0 +1,23 @@ +interface Options { + immutable: boolean; +} + +interface ObserveOptions { + defer: boolean; + init: boolean; +} + +interface Cancellable { + cancel: () => void; +} + +export declare class Store { + constructor(state: State, options?: Options); + + public compute(key: string, dependencies: string[]): void; + public get(): State; + public get(key: string): T; + public observe(key: string, callback: (value: T) => any, options?: ObserveOptions): Cancellable; + public onchange(callback: (state: State) => any): Cancellable; + public set(state: State); +} From 62c5f5f8cf59795ea53bd540c620706d01a0476e Mon Sep 17 00:00:00 2001 From: PaulBGD Date: Mon, 5 Mar 2018 17:46:17 -0600 Subject: [PATCH 002/316] Add to files --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 3524d42eb4..702e457388 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "shared.js", "store.js", "store.umd.js", + "store.d.ts", "README.md" ], "scripts": { From 8dc17b77db85a8d974c10239676a5e169e01e072 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 29 Apr 2018 11:34:45 -0400 Subject: [PATCH 003/316] allow arbitrary expressions in each block keys - fixes #703 --- src/compile/nodes/EachBlock.ts | 20 +++++++----- src/parse/state/mustache.ts | 18 +---------- src/shared/keyed-each.js | 9 +++--- .../samples/each-block-keyed/output.json | 20 ++++++++++-- .../each-block-keyed-non-prop/_config.js | 31 +++++++++++++++++++ .../each-block-keyed-non-prop/main.html | 3 ++ 6 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 test/runtime/samples/each-block-keyed-non-prop/_config.js create mode 100644 test/runtime/samples/each-block-keyed-non-prop/main.html diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts index f96b330064..6047d77b56 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(); @@ -262,7 +265,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`); @@ -282,11 +285,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); } `); @@ -313,7 +319,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} From 61ee380e26a5f3351896708b2a68483bbedf8c30 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 29 Apr 2018 14:15:52 -0400 Subject: [PATCH 004/316] deduplicate each block context generation (#1287) --- src/compile/nodes/EachBlock.ts | 36 ++++++++++--------- .../expected-bundle.js | 24 ++++++------- .../each-block-changed-check/expected.js | 24 ++++++------- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts index f96b330064..a7a061ad58 100644 --- a/src/compile/nodes/EachBlock.ts +++ b/src/compile/nodes/EachBlock.ts @@ -63,7 +63,7 @@ export default class EachBlock extends Node { this.var = block.getUniqueName(`each`); this.iterations = block.getUniqueName(`${this.var}_blocks`); - this.each_context = block.getUniqueName(`${this.var}_context`); + this.get_each_context = block.getUniqueName(`get_${this.var}_context`); const { dependencies } = this.expression; block.addDependencies(dependencies); @@ -88,14 +88,14 @@ export default class EachBlock extends Node { } this.contextProps = [ - `${listName}: ${listName}`, - `${this.context}: ${listName}[#i]`, - `${indexName}: #i` + `${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]}: ${listName}[#i][${i}]`); + this.contextProps.push(`${this.destructuredContexts[i]}: list[i][${i}]`); } } @@ -162,6 +162,14 @@ export default class EachBlock extends Node { block.builders.init.addLine(`var ${each_block_value} = ${snippet};`); + this.compiler.target.blocks.push(deindent` + function ${this.get_each_context}(ctx, list, i) { + return @assign(@assign({}, ctx), { + ${this.contextProps.join(',\n')} + }); + } + `); + if (this.key) { this.buildKeyed(block, parentNode, parentNodes, snippet, vars); } else { @@ -349,9 +357,7 @@ export default class EachBlock extends Node { var ${iterations} = []; for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) { - ${iterations}[#i] = ${create_each_block}(#component, @assign(@assign({}, ctx), { - ${this.contextProps.join(',\n')} - })); + ${iterations}[#i] = ${create_each_block}(#component, ${this.get_each_context}(ctx, ${each_block_value}, #i)); } `); @@ -395,24 +401,24 @@ export default class EachBlock extends Node { ? this.block.hasIntroMethod ? deindent` if (${iterations}[#i]) { - ${iterations}[#i].p(changed, ${this.each_context}); + ${iterations}[#i].p(changed, child_ctx); } else { - ${iterations}[#i] = ${create_each_block}(#component, ${this.each_context}); + ${iterations}[#i] = ${create_each_block}(#component, child_ctx); ${iterations}[#i].c(); } ${iterations}[#i].i(${updateMountNode}, ${anchor}); ` : deindent` if (${iterations}[#i]) { - ${iterations}[#i].p(changed, ${this.each_context}); + ${iterations}[#i].p(changed, child_ctx); } else { - ${iterations}[#i] = ${create_each_block}(#component, ${this.each_context}); + ${iterations}[#i] = ${create_each_block}(#component, child_ctx); ${iterations}[#i].c(); ${iterations}[#i].m(${updateMountNode}, ${anchor}); } ` : deindent` - ${iterations}[#i] = ${create_each_block}(#component, ${this.each_context}); + ${iterations}[#i] = ${create_each_block}(#component, child_ctx); ${iterations}[#i].c(); ${iterations}[#i].${mountOrIntro}(${updateMountNode}, ${anchor}); `; @@ -447,9 +453,7 @@ export default class EachBlock extends Node { ${each_block_value} = ${snippet}; for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) { - var ${this.each_context} = @assign(@assign({}, ctx), { - ${this.contextProps.join(',\n')} - }); + const child_ctx = ${this.get_each_context}(ctx, ${each_block_value}, #i); ${forLoopBody} } 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 f4d29ba84a..facaec2b93 100644 --- a/test/js/samples/each-block-changed-check/expected-bundle.js +++ b/test/js/samples/each-block-changed-check/expected-bundle.js @@ -163,11 +163,7 @@ function create_main_fragment(component, ctx) { var each_blocks = []; for (var i = 0; i < each_value.length; i += 1) { - each_blocks[i] = create_each_block(component, assign(assign({}, ctx), { - each_value: each_value, - comment: each_value[i], - i: i - })); + each_blocks[i] = create_each_block(component, get_each_context(ctx, each_value, i)); } return { @@ -196,16 +192,12 @@ function create_main_fragment(component, ctx) { each_value = ctx.comments; for (var i = 0; i < each_value.length; i += 1) { - var each_context = assign(assign({}, ctx), { - each_value: each_value, - comment: each_value[i], - i: i - }); + const child_ctx = get_each_context(ctx, each_value, i); if (each_blocks[i]) { - each_blocks[i].p(changed, each_context); + each_blocks[i].p(changed, child_ctx); } else { - each_blocks[i] = create_each_block(component, each_context); + each_blocks[i] = create_each_block(component, child_ctx); each_blocks[i].c(); each_blocks[i].m(text.parentNode, text); } @@ -303,6 +295,14 @@ function create_each_block(component, ctx) { }; } +function get_each_context(ctx, list, i) { + return assign(assign({}, ctx), { + each_value: list, + comment: list[i], + i: i + }); +} + function SvelteComponent(options) { init(this, options); this._state = assign({}, options.data); diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js index 75dfc20a7d..05af12e226 100644 --- a/test/js/samples/each-block-changed-check/expected.js +++ b/test/js/samples/each-block-changed-check/expected.js @@ -9,11 +9,7 @@ function create_main_fragment(component, ctx) { var each_blocks = []; for (var i = 0; i < each_value.length; i += 1) { - each_blocks[i] = create_each_block(component, assign(assign({}, ctx), { - each_value: each_value, - comment: each_value[i], - i: i - })); + each_blocks[i] = create_each_block(component, get_each_context(ctx, each_value, i)); } return { @@ -42,16 +38,12 @@ function create_main_fragment(component, ctx) { each_value = ctx.comments; for (var i = 0; i < each_value.length; i += 1) { - var each_context = assign(assign({}, ctx), { - each_value: each_value, - comment: each_value[i], - i: i - }); + const child_ctx = get_each_context(ctx, each_value, i); if (each_blocks[i]) { - each_blocks[i].p(changed, each_context); + each_blocks[i].p(changed, child_ctx); } else { - each_blocks[i] = create_each_block(component, each_context); + each_blocks[i] = create_each_block(component, child_ctx); each_blocks[i].c(); each_blocks[i].m(text.parentNode, text); } @@ -149,6 +141,14 @@ function create_each_block(component, ctx) { }; } +function get_each_context(ctx, list, i) { + return assign(assign({}, ctx), { + each_value: list, + comment: list[i], + i: i + }); +} + function SvelteComponent(options) { init(this, options); this._state = assign({}, options.data); From f5048fcf104fa10b8b282697cd5e807d6c52a353 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 29 Apr 2018 14:39:30 -0400 Subject: [PATCH 005/316] deduplicate each block context generation for keyed blocks --- src/compile/nodes/EachBlock.ts | 12 +++------- src/shared/keyed-each.js | 10 ++++---- .../deconflict-builtins/expected-bundle.js | 24 +++++++++---------- .../samples/deconflict-builtins/expected.js | 24 +++++++++---------- 4 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts index b94b6f2ea3..6d14cb6e00 100644 --- a/src/compile/nodes/EachBlock.ts +++ b/src/compile/nodes/EachBlock.ts @@ -66,7 +66,7 @@ export default class EachBlock extends Node { this.var = block.getUniqueName(`each`); this.iterations = block.getUniqueName(`${this.var}_blocks`); - this.get_each_context = block.getUniqueName(`get_${this.var}_context`); + this.get_each_context = this.compiler.getUniqueName(`get_${this.var}_context`); const { dependencies } = this.expression; block.addDependencies(dependencies); @@ -296,9 +296,7 @@ export default class EachBlock extends Node { const ${get_key} = ctx => ${this.key.snippet}; for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) { - let child_ctx = @assign(@assign({}, ctx), { - ${this.contextProps.join(',\n')} - }); + let child_ctx = ${this.get_each_context}(ctx, ${each_block_value}, #i); let key = ${get_key}(child_ctx); ${blocks}[#i] = ${lookup}[key] = ${create_each_block}(#component, key, child_ctx); } @@ -327,11 +325,7 @@ export default class EachBlock extends Node { block.builders.update.addBlock(deindent` var ${each_block_value} = ${snippet}; - ${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')} - }); - }); + ${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.get_each_context}); `); if (!parentNode) { diff --git a/src/shared/keyed-each.js b/src/shared/keyed-each.js index 278acde6db..8921b6fed5 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, get_key, dynamic, list, lookup, node, has_outro, create_each_block, intro_method, next, get_context) { +export function updateKeyedEach(old_blocks, component, changed, get_key, dynamic, ctx, list, lookup, node, has_outro, create_each_block, intro_method, next, get_context) { var o = old_blocks.length; var n = list.length; @@ -24,15 +24,15 @@ export function updateKeyedEach(old_blocks, component, changed, get_key, dynamic var i = n; while (i--) { - var ctx = get_context(i); - var key = get_key(ctx); + var child_ctx = get_context(ctx, list, i); + var key = get_key(child_ctx); var block = lookup[key]; if (!block) { - block = create_each_block(component, key, ctx); + block = create_each_block(component, key, child_ctx); block.c(); } else if (dynamic) { - block.p(changed, ctx); + block.p(changed, child_ctx); } new_blocks[i] = new_lookup[key] = block; diff --git a/test/js/samples/deconflict-builtins/expected-bundle.js b/test/js/samples/deconflict-builtins/expected-bundle.js index b677e75e29..008d284885 100644 --- a/test/js/samples/deconflict-builtins/expected-bundle.js +++ b/test/js/samples/deconflict-builtins/expected-bundle.js @@ -161,11 +161,7 @@ function create_main_fragment(component, ctx) { var each_blocks = []; for (var i = 0; i < each_value.length; i += 1) { - each_blocks[i] = create_each_block(component, assign(assign({}, ctx), { - each_value: each_value, - node: each_value[i], - node_index: i - })); + each_blocks[i] = create_each_block(component, get_each_context(ctx, each_value, i)); } return { @@ -190,16 +186,12 @@ function create_main_fragment(component, ctx) { each_value = ctx.createElement; for (var i = 0; i < each_value.length; i += 1) { - var each_context = assign(assign({}, ctx), { - each_value: each_value, - node: each_value[i], - node_index: i - }); + const child_ctx = get_each_context(ctx, each_value, i); if (each_blocks[i]) { - each_blocks[i].p(changed, each_context); + each_blocks[i].p(changed, child_ctx); } else { - each_blocks[i] = create_each_block(component, each_context); + each_blocks[i] = create_each_block(component, child_ctx); each_blocks[i].c(); each_blocks[i].m(each_anchor.parentNode, each_anchor); } @@ -256,6 +248,14 @@ function create_each_block(component, ctx) { }; } +function get_each_context(ctx, list, i) { + return assign(assign({}, ctx), { + each_value: list, + node: list[i], + node_index: i + }); +} + function SvelteComponent(options) { init(this, options); this._state = assign({}, options.data); diff --git a/test/js/samples/deconflict-builtins/expected.js b/test/js/samples/deconflict-builtins/expected.js index ce7e2fa52f..5ef4396ea0 100644 --- a/test/js/samples/deconflict-builtins/expected.js +++ b/test/js/samples/deconflict-builtins/expected.js @@ -9,11 +9,7 @@ function create_main_fragment(component, ctx) { var each_blocks = []; for (var i = 0; i < each_value.length; i += 1) { - each_blocks[i] = create_each_block(component, assign(assign({}, ctx), { - each_value: each_value, - node: each_value[i], - node_index: i - })); + each_blocks[i] = create_each_block(component, get_each_context(ctx, each_value, i)); } return { @@ -38,16 +34,12 @@ function create_main_fragment(component, ctx) { each_value = ctx.createElement; for (var i = 0; i < each_value.length; i += 1) { - var each_context = assign(assign({}, ctx), { - each_value: each_value, - node: each_value[i], - node_index: i - }); + const child_ctx = get_each_context(ctx, each_value, i); if (each_blocks[i]) { - each_blocks[i].p(changed, each_context); + each_blocks[i].p(changed, child_ctx); } else { - each_blocks[i] = create_each_block(component, each_context); + each_blocks[i] = create_each_block(component, child_ctx); each_blocks[i].c(); each_blocks[i].m(each_anchor.parentNode, each_anchor); } @@ -104,6 +96,14 @@ function create_each_block(component, ctx) { }; } +function get_each_context(ctx, list, i) { + return assign(assign({}, ctx), { + each_value: list, + node: list[i], + node_index: i + }); +} + function SvelteComponent(options) { init(this, options); this._state = assign({}, options.data); From 506ab3952ecfb9f7588833fbc9aa375608714a8a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 29 Apr 2018 16:25:43 -0400 Subject: [PATCH 006/316] destructuring --- src/compile/nodes/EachBlock.ts | 35 +++--- src/parse/read/context.ts | 114 ++++++++++++++++++ src/parse/state/mustache.ts | 36 +----- src/utils/unpackDestructuring.ts | 20 +++ src/validate/html/index.ts | 25 ++-- .../deconflict-builtins/expected-bundle.js | 2 +- .../samples/deconflict-builtins/expected.js | 2 +- .../expected-bundle.js | 2 +- .../each-block-changed-check/expected.js | 2 +- .../each-block-destructured/output.json | 25 +++- .../samples/each-block-else/output.json | 8 +- .../samples/each-block-indexed/output.json | 8 +- .../samples/each-block-keyed/output.json | 7 +- test/parser/samples/each-block/output.json | 8 +- .../samples/unusual-identifier/output.json | 8 +- .../each-block-destructured-object/_config.js | 22 ++++ .../each-block-destructured-object/main.html | 3 + 17 files changed, 241 insertions(+), 86 deletions(-) create mode 100644 src/parse/read/context.ts create mode 100644 src/utils/unpackDestructuring.ts create mode 100644 test/runtime/samples/each-block-destructured-object/_config.js create mode 100644 test/runtime/samples/each-block-destructured-object/main.html 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} From 96075937057fffa3d84feae7f8b76c61e7180a20 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 29 Apr 2018 17:08:24 -0400 Subject: [PATCH 007/316] sparse array patterns --- src/parse/read/context.ts | 9 +++++++-- src/utils/unpackDestructuring.ts | 2 ++ .../_config.js | 20 +++++++++++++++++++ .../main.html | 3 +++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 test/runtime/samples/each-block-destructured-array-sparse/_config.js create mode 100644 test/runtime/samples/each-block-destructured-array-sparse/main.html diff --git a/src/parse/read/context.ts b/src/parse/read/context.ts index feb31d150e..3ab18695bd 100644 --- a/src/parse/read/context.ts +++ b/src/parse/read/context.ts @@ -46,8 +46,13 @@ export default function readContext(parser: Parser) { do { parser.allowWhitespace(); - context.elements.push(readContext(parser)); - parser.allowWhitespace(); + + if (parser.template[parser.index] === ',') { + context.elements.push(null); + } else { + context.elements.push(readContext(parser)); + parser.allowWhitespace(); + } } while (parser.eat(',')); errorOnAssignmentPattern(parser); diff --git a/src/utils/unpackDestructuring.ts b/src/utils/unpackDestructuring.ts index 2c48d5c762..749d26b34b 100644 --- a/src/utils/unpackDestructuring.ts +++ b/src/utils/unpackDestructuring.ts @@ -3,6 +3,8 @@ export default function unpackDestructuring( node: Node, tail: string ) { + if (!node) return; + if (node.type === 'Identifier') { contexts.push({ key: node, diff --git a/test/runtime/samples/each-block-destructured-array-sparse/_config.js b/test/runtime/samples/each-block-destructured-array-sparse/_config.js new file mode 100644 index 0000000000..f504cfe377 --- /dev/null +++ b/test/runtime/samples/each-block-destructured-array-sparse/_config.js @@ -0,0 +1,20 @@ +export default { + data: { + animalPawsEntries: [ + ['raccoon', 'hands'], + ['eagle', 'wings'] + ] + }, + + html: ` +

hands

+

wings

+ `, + + test ( assert, component, target ) { + component.set({ animalPawsEntries: [['foo', 'bar']] }); + assert.htmlEqual( target.innerHTML, ` +

bar

+ `); + }, +}; diff --git a/test/runtime/samples/each-block-destructured-array-sparse/main.html b/test/runtime/samples/each-block-destructured-array-sparse/main.html new file mode 100644 index 0000000000..7ca8e403d8 --- /dev/null +++ b/test/runtime/samples/each-block-destructured-array-sparse/main.html @@ -0,0 +1,3 @@ +{#each animalPawsEntries as [, pawType]} +

{pawType}

+{/each} From 70034ea997f530d06e96d1761e4a48170140a1d5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 29 Apr 2018 17:28:51 -0400 Subject: [PATCH 008/316] bind to destructured properties --- src/compile/dom/Block.ts | 10 ++-- src/compile/nodes/Binding.ts | 5 +- src/compile/nodes/Component.ts | 5 +- src/compile/nodes/EachBlock.ts | 47 +++++++++---------- src/compile/nodes/Fragment.ts | 3 +- src/parse/read/context.ts | 2 +- .../_config.js | 39 +++++++++++++++ .../main.html | 5 ++ 8 files changed, 76 insertions(+), 40 deletions(-) create mode 100644 test/runtime/samples/each-block-destructured-object-binding/_config.js create mode 100644 test/runtime/samples/each-block-destructured-object-binding/main.html diff --git a/src/compile/dom/Block.ts b/src/compile/dom/Block.ts index f41892ccc9..11f34268b3 100644 --- a/src/compile/dom/Block.ts +++ b/src/compile/dom/Block.ts @@ -9,8 +9,7 @@ export interface BlockOptions { compiler?: Compiler; comment?: string; key?: string; - indexNames?: Map; - listNames?: Map; + bindings?: Map; dependencies?: Set; } @@ -23,8 +22,8 @@ export default class Block { first: string; dependencies: Set; - indexNames: Map; - listNames: Map; + + bindings: Map; builders: { init: CodeBuilder; @@ -62,8 +61,7 @@ export default class Block { this.dependencies = new Set(); - this.indexNames = options.indexNames; - this.listNames = options.listNames; + this.bindings = options.bindings; this.builders = { init: new CodeBuilder(), diff --git a/src/compile/nodes/Binding.ts b/src/compile/nodes/Binding.ts index 3c56da0f43..c282cf1a9d 100644 --- a/src/compile/nodes/Binding.ts +++ b/src/compile/nodes/Binding.ts @@ -195,14 +195,13 @@ function getEventHandler( ? getTailSnippet(binding.value.node) : ''; - const list = `ctx.${block.listNames.get(name)}`; - const index = `ctx.${block.indexNames.get(name)}`; + const head = block.bindings.get(name); return { usesContext: true, usesState: true, usesStore: storeDependencies.length > 0, - mutation: `${list}[${index}]${tail} = ${value};`, + mutation: `${head}${tail} = ${value};`, props: dependencies.map(prop => `${prop}: ctx.${prop}`), storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`) }; diff --git a/src/compile/nodes/Component.ts b/src/compile/nodes/Component.ts index d40466a052..1a872fe6ce 100644 --- a/src/compile/nodes/Component.ts +++ b/src/compile/nodes/Component.ts @@ -239,12 +239,11 @@ export default class Component extends Node { const computed = isComputed(binding.value.node); const tail = binding.value.node.type === 'MemberExpression' ? getTailSnippet(binding.value.node) : ''; - const list = block.listNames.get(key); - const index = block.indexNames.get(key); + const head = block.bindings.get(key); const lhs = binding.value.node.type === 'MemberExpression' ? binding.value.snippet - : `ctx.${list}[ctx.${index}]${tail} = childState.${binding.name}`; + : `${head}${tail} = childState.${binding.name}`; setFromChild = deindent` ${lhs} = childState.${binding.name}; diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts index 3bf6f08789..6d771dbf87 100644 --- a/src/compile/nodes/EachBlock.ts +++ b/src/compile/nodes/EachBlock.ts @@ -76,15 +76,16 @@ export default class EachBlock extends Node { name: this.compiler.getUniqueName('create_each_block'), key: this.key, - indexNames: new Map(block.indexNames), - listNames: new Map(block.listNames) + bindings: new Map(block.bindings) }); - const listName = this.compiler.getUniqueName('each_value'); + this.each_block_value = this.compiler.getUniqueName('each_value'); + const indexName = this.index || this.compiler.getUniqueName(`${this.context}_index`); - this.block.indexNames.set(this.context, indexName); - this.block.listNames.set(this.context, listName); + this.contexts.forEach(prop => { + this.block.bindings.set(prop.key.name, `ctx.${this.each_block_value}[ctx.${indexName}]${prop.tail}`); + }); if (this.index) { this.block.getUniqueName(this.index); // this prevents name collisions (#1254) @@ -94,7 +95,7 @@ export default class EachBlock extends Node { // TODO only add these if necessary this.contextProps.push( - `${listName}: list`, + `${this.each_block_value}: list`, `${indexName}: i` ); @@ -131,7 +132,6 @@ export default class EachBlock extends Node { const each = this.var; const create_each_block = this.block.name; - const each_block_value = this.block.listNames.get(this.context); const iterations = this.iterations; const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode(); @@ -150,7 +150,6 @@ export default class EachBlock extends Node { const vars = { each, create_each_block, - each_block_value, length, iterations, anchor, @@ -159,7 +158,7 @@ export default class EachBlock extends Node { const { snippet } = this.expression; - block.builders.init.addLine(`var ${each_block_value} = ${snippet};`); + block.builders.init.addLine(`var ${this.each_block_value} = ${snippet};`); this.compiler.target.blocks.push(deindent` function ${this.get_each_context}(ctx, list, i) { @@ -191,7 +190,7 @@ export default class EachBlock extends Node { // TODO neaten this up... will end up with an empty line in the block block.builders.init.addBlock(deindent` - if (!${each_block_value}.${length}) { + if (!${this.each_block_value}.${length}) { ${each_block_else} = ${this.else.block.name}(#component, ctx); ${each_block_else}.c(); } @@ -207,9 +206,9 @@ export default class EachBlock extends Node { if (this.else.block.hasUpdateMethod) { block.builders.update.addBlock(deindent` - if (!${each_block_value}.${length} && ${each_block_else}) { + if (!${this.each_block_value}.${length} && ${each_block_else}) { ${each_block_else}.p(changed, ctx); - } else if (!${each_block_value}.${length}) { + } else if (!${this.each_block_value}.${length}) { ${each_block_else} = ${this.else.block.name}(#component, ctx); ${each_block_else}.c(); ${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor}); @@ -221,7 +220,7 @@ export default class EachBlock extends Node { `); } else { block.builders.update.addBlock(deindent` - if (${each_block_value}.${length}) { + if (${this.each_block_value}.${length}) { if (${each_block_else}) { ${each_block_else}.u(); ${each_block_else}.d(); @@ -263,7 +262,6 @@ export default class EachBlock extends Node { { each, create_each_block, - each_block_value, length, anchor, mountOrIntro, @@ -291,8 +289,8 @@ 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) { - let child_ctx = ${this.get_each_context}(ctx, ${each_block_value}, #i); + for (var #i = 0; #i < ${this.each_block_value}.${length}; #i += 1) { + let child_ctx = ${this.get_each_context}(ctx, ${this.each_block_value}, #i); let key = ${get_key}(child_ctx); ${blocks}[#i] = ${lookup}[key] = ${create_each_block}(#component, key, child_ctx); } @@ -319,9 +317,9 @@ export default class EachBlock extends Node { const dynamic = this.block.hasUpdateMethod; block.builders.update.addBlock(deindent` - var ${each_block_value} = ${snippet}; + var ${this.each_block_value} = ${snippet}; - ${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.get_each_context}); + ${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.get_each_context}); `); if (!parentNode) { @@ -342,7 +340,6 @@ export default class EachBlock extends Node { snippet: string, { create_each_block, - each_block_value, length, iterations, anchor, @@ -352,8 +349,8 @@ export default class EachBlock extends Node { block.builders.init.addBlock(deindent` var ${iterations} = []; - for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) { - ${iterations}[#i] = ${create_each_block}(#component, ${this.get_each_context}(ctx, ${each_block_value}, #i)); + for (var #i = 0; #i < ${this.each_block_value}.${length}; #i += 1) { + ${iterations}[#i] = ${create_each_block}(#component, ${this.get_each_context}(ctx, ${this.each_block_value}, #i)); } `); @@ -441,15 +438,15 @@ export default class EachBlock extends Node { ${iterations}[#i].u(); ${iterations}[#i].d(); } - ${iterations}.length = ${each_block_value}.${length}; + ${iterations}.length = ${this.each_block_value}.${length}; `; block.builders.update.addBlock(deindent` if (${condition}) { - ${each_block_value} = ${snippet}; + ${this.each_block_value} = ${snippet}; - for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) { - const child_ctx = ${this.get_each_context}(ctx, ${each_block_value}, #i); + for (var #i = ${start}; #i < ${this.each_block_value}.${length}; #i += 1) { + const child_ctx = ${this.get_each_context}(ctx, ${this.each_block_value}, #i); ${forLoopBody} } diff --git a/src/compile/nodes/Fragment.ts b/src/compile/nodes/Fragment.ts index f0de86fa97..e874fe3707 100644 --- a/src/compile/nodes/Fragment.ts +++ b/src/compile/nodes/Fragment.ts @@ -23,8 +23,7 @@ export default class Fragment extends Node { name: '@create_main_fragment', key: null, - indexNames: new Map(), - listNames: new Map(), + bindings: new Map(), dependencies: new Set(), }); diff --git a/src/parse/read/context.ts b/src/parse/read/context.ts index 3ab18695bd..77fd2cbf68 100644 --- a/src/parse/read/context.ts +++ b/src/parse/read/context.ts @@ -77,7 +77,7 @@ export default function readContext(parser: Parser) { parser.allowWhitespace(); const value = parser.eat(':') - ? readContext(parser) + ? (parser.allowWhitespace(), readContext(parser)) : key; const property: Property = { diff --git a/test/runtime/samples/each-block-destructured-object-binding/_config.js b/test/runtime/samples/each-block-destructured-object-binding/_config.js new file mode 100644 index 0000000000..42fba9d8b8 --- /dev/null +++ b/test/runtime/samples/each-block-destructured-object-binding/_config.js @@ -0,0 +1,39 @@ +export default { + data: { + people: [{ name: { first: 'Doctor', last: 'Who' } }], + }, + + html: ` + + +

Doctor Who

+ `, + + test(assert, component, target, window) { + const inputs = target.querySelectorAll('input'); + + inputs[1].value = 'Oz'; + inputs[1].dispatchEvent(new window.Event('input')); + + const { people } = component.get(); + + assert.deepEqual(people, [ + { name: { first: 'Doctor', last: 'Oz' } } + ]); + + assert.htmlEqual(target.innerHTML, ` + + +

Doctor Oz

+ `); + + people[0].name.first = 'Frank'; + component.set({ people }); + + assert.htmlEqual(target.innerHTML, ` + + +

Frank Oz

+ `); + }, +}; diff --git a/test/runtime/samples/each-block-destructured-object-binding/main.html b/test/runtime/samples/each-block-destructured-object-binding/main.html new file mode 100644 index 0000000000..049387bc3d --- /dev/null +++ b/test/runtime/samples/each-block-destructured-object-binding/main.html @@ -0,0 +1,5 @@ +{#each people as { name: { first: f, last: l } } } + + +

{f} {l}

+{/each} From 2866b11c7d43b6a9ae9bc573d1d42c032ff14aee Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 29 Apr 2018 18:32:28 -0400 Subject: [PATCH 009/316] WIP towards #984 --- src/compile/nodes/Binding.ts | 17 +- src/compile/nodes/Element.ts | 9 +- src/shared/dom.js | 25 +++ src/validate/html/validateElement.ts | 2 +- .../bind-width-height.solo/expected-bundle.js | 178 ++++++++++++++++++ .../bind-width-height.solo/expected.js | 50 +++++ .../samples/bind-width-height.solo/input.html | 3 + 7 files changed, 278 insertions(+), 6 deletions(-) create mode 100644 test/js/samples/bind-width-height.solo/expected-bundle.js create mode 100644 test/js/samples/bind-width-height.solo/expected.js create mode 100644 test/js/samples/bind-width-height.solo/input.html diff --git a/src/compile/nodes/Binding.ts b/src/compile/nodes/Binding.ts index 3c56da0f43..bbe8aa46ac 100644 --- a/src/compile/nodes/Binding.ts +++ b/src/compile/nodes/Binding.ts @@ -14,6 +14,9 @@ const readOnlyMediaAttributes = new Set([ 'played' ]); +// TODO a lot of this element-specific stuff should live in Element — +// Binding should ideally be agnostic between Element and Component + export default class Binding extends Node { name: string; value: Expression; @@ -57,7 +60,10 @@ export default class Binding extends Node { const node: Element = this.parent; const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(node.getStaticAttributeValue('type')); - const isReadOnly = node.isMediaNode() && readOnlyMediaAttributes.has(this.name); + const isReadOnly = ( + (node.isMediaNode() && readOnlyMediaAttributes.has(this.name)) || + this.name === 'width' || this.name === 'height' + ); let updateCondition: string; @@ -103,8 +109,7 @@ export default class Binding extends Node { if (this.name === 'currentTime' || this.name === 'volume') { updateCondition = `!isNaN(${snippet})`; - if (this.name === 'currentTime') - initialUpdate = null; + if (this.name === 'currentTime') initialUpdate = null; } if (this.name === 'paused') { @@ -117,6 +122,12 @@ export default class Binding extends Node { initialUpdate = null; } + // bind:width and bind:height + if (this.name === 'width' || this.name === 'height') { + initialUpdate = null; + updateDom = null; + } + return { name: this.name, object: name, diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index a025eb7b05..3b84b0c369 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -262,7 +262,7 @@ export default class Element extends Node { parentNode; block.addVariable(name); - const renderStatement = getRenderStatement(this.compiler, this.namespace, this.name); + const renderStatement = getRenderStatement(this.namespace, this.name); block.builders.create.addLine( `${name} = ${renderStatement};` ); @@ -916,7 +916,6 @@ export default class Element extends Node { } function getRenderStatement( - compiler: Compiler, namespace: string, name: string ) { @@ -971,6 +970,12 @@ const events = [ node.name === 'input' && /radio|checkbox|range/.test(node.getStaticAttributeValue('type')) }, + { + eventNames: ['resize'], + filter: (node: Element, name: string) => + (name === 'width' || name === 'height') + }, + // media events { eventNames: ['timeupdate'], diff --git a/src/shared/dom.js b/src/shared/dom.js index 0eb53585d7..5661a121c1 100644 --- a/src/shared/dom.js +++ b/src/shared/dom.js @@ -193,3 +193,28 @@ export function selectMultipleValue(select) { return option.__value; }); } + +export function addResizeListener(element, fn) { + if (getComputedStyle(element).position === 'static') { + element.style.position = 'relative'; + } + + const object = document.createElement('object'); + object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;'); + object.type = 'text/html'; + + if (isIE) element.appendChild(object); + object.data = 'about:blank'; + if (!isIE) element.appendChild(object); + + object.onload = () => { + object.contentDocument.defaultView.addEventListener('resize', fn); + }; + + return { + cancel: () => { + object.contentDocument.defaultView.removeEventListener('resize', fn); + element.removeChild(object); + } + }; +} \ No newline at end of file diff --git a/src/validate/html/validateElement.ts b/src/validate/html/validateElement.ts index 83de0b49aa..3e73d390b1 100644 --- a/src/validate/html/validateElement.ts +++ b/src/validate/html/validateElement.ts @@ -157,7 +157,7 @@ export default function validateElement( message: `'${name}' binding can only be used with