From d03693114df5898740cd4a44f42531b02f2239fb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 28 Apr 2018 21:00:07 -0400 Subject: [PATCH 001/319] fix #1368 --- src/Stats.ts | 13 +++++++++---- src/index.ts | 2 +- test/stats/index.js | 8 ++++++++ test/stats/samples/hooks/_config.js | 5 ++++- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/Stats.ts b/src/Stats.ts index c9e62aef21..6faca18e75 100644 --- a/src/Stats.ts +++ b/src/Stats.ts @@ -78,7 +78,9 @@ export default class Stats { total: now() - this.startTime }, collapseTimings(this.timings)); - const imports = compiler.imports.map(node => { + // TODO would be good to have this info even + // if options.generate is false + const imports = compiler && compiler.imports.map(node => { return { source: node.source.value, specifiers: node.specifiers.map(specifier => { @@ -94,9 +96,12 @@ export default class Stats { } }); - const hooks: Record = {}; - if (compiler.templateProperties.oncreate) hooks.oncreate = true; - if (compiler.templateProperties.ondestroy) hooks.ondestroy = true; + const hooks: Record = compiler && { + oncreate: !!compiler.templateProperties.oncreate, + ondestroy: !!compiler.templateProperties.ondestroy, + onstate: !!compiler.templateProperties.onstate, + onupdate: !!compiler.templateProperties.onupdate + }; return { timings, diff --git a/src/index.ts b/src/index.ts index 6bef0581bd..2895dd2977 100644 --- a/src/index.ts +++ b/src/index.ts @@ -132,7 +132,7 @@ function compile(source: string, _options: CompileOptions) { stats.stop('validate'); if (options.generate === false) { - return { ast: ast, stats, js: null, css: null }; + return { ast, stats: stats.render(null), js: null, css: null }; } const compiler = options.generate === 'ssr' ? generateSSR : generate; diff --git a/test/stats/index.js b/test/stats/index.js index fba9926061..550c887a6a 100644 --- a/test/stats/index.js +++ b/test/stats/index.js @@ -56,4 +56,12 @@ describe('stats', () => { } }); }); + + it('returns a stats object when options.generate is false', () => { + const { stats } = svelte.compile('', { + generate: false + }); + + assert.equal(typeof stats.timings.total, 'number'); + }); }); diff --git a/test/stats/samples/hooks/_config.js b/test/stats/samples/hooks/_config.js index 78fc771a06..5778d44102 100644 --- a/test/stats/samples/hooks/_config.js +++ b/test/stats/samples/hooks/_config.js @@ -1,7 +1,10 @@ export default { test(assert, stats) { assert.deepEqual(stats.hooks, { - oncreate: true + oncreate: true, + ondestroy: false, + onstate: false, + onupdate: false }); } }; \ No newline at end of file From ef39f009028c048a8380e197eabbfe93bdbce2e0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 28 Apr 2018 22:26:18 -0400 Subject: [PATCH 002/319] assign custom methods to custom element prototype - fixes #1369 --- src/compile/dom/index.ts | 13 +++++++------ .../custom-elements/samples/custom-method/main.html | 13 +++++++++++++ test/custom-elements/samples/custom-method/test.js | 12 ++++++++++++ test/js/samples/action/expected-bundle.js | 2 +- test/js/samples/action/expected.js | 2 +- .../css-shadow-dom-keyframes/expected-bundle.js | 6 ++++-- .../js/samples/css-shadow-dom-keyframes/expected.js | 6 ++++-- 7 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 test/custom-elements/samples/custom-method/main.html create mode 100644 test/custom-elements/samples/custom-method/test.js diff --git a/src/compile/dom/index.ts b/src/compile/dom/index.ts index 3db1158449..f835272b3f 100644 --- a/src/compile/dom/index.ts +++ b/src/compile/dom/index.ts @@ -113,8 +113,6 @@ export default function dom( ? 'svelte/shared.js' : options.shared || ''; - let prototypeBase = `${name}.prototype`; - const proto = sharedPath ? `@proto` : deindent` @@ -294,8 +292,9 @@ export default function dom( `} } - customElements.define("${compiler.tag}", ${name}); - @assign(@assign(${prototypeBase}, ${proto}), { + @assign(${name}.prototype, ${proto}); + ${templateProperties.methods && `@assign(${name}.prototype, %methods);`} + @assign(${name}.prototype, { _mount(target, anchor) { target.insertBefore(this, anchor); }, @@ -304,6 +303,8 @@ export default function dom( this.parentNode.removeChild(this); } }); + + customElements.define("${compiler.tag}", ${name}); `); } else { builder.addBlock(deindent` @@ -311,8 +312,8 @@ export default function dom( ${constructorBody} } - @assign(${prototypeBase}, ${proto}); - ${templateProperties.methods && `@assign(${prototypeBase}, %methods);`} + @assign(${name}.prototype, ${proto}); + ${templateProperties.methods && `@assign(${name}.prototype, %methods);`} `); } diff --git a/test/custom-elements/samples/custom-method/main.html b/test/custom-elements/samples/custom-method/main.html new file mode 100644 index 0000000000..b388472f23 --- /dev/null +++ b/test/custom-elements/samples/custom-method/main.html @@ -0,0 +1,13 @@ +

{foo}

+ + \ No newline at end of file diff --git a/test/custom-elements/samples/custom-method/test.js b/test/custom-elements/samples/custom-method/test.js new file mode 100644 index 0000000000..f6ef68c8c0 --- /dev/null +++ b/test/custom-elements/samples/custom-method/test.js @@ -0,0 +1,12 @@ +import * as assert from 'assert'; +import './main.html'; + +export default function (target) { + target.innerHTML = ''; + const el = target.querySelector('custom-element'); + + el.updateFoo(42); + + const p = el.shadowRoot.querySelector('p'); + assert.equal(p.textContent, '42'); +} \ No newline at end of file diff --git a/test/js/samples/action/expected-bundle.js b/test/js/samples/action/expected-bundle.js index f1ee2f4b20..1998828627 100644 --- a/test/js/samples/action/expected-bundle.js +++ b/test/js/samples/action/expected-bundle.js @@ -136,7 +136,7 @@ var proto = { /* generated by Svelte vX.Y.Z */ function link(node) { - + function onClick(event) { event.preventDefault(); history.pushState(null, null, event.target.href); diff --git a/test/js/samples/action/expected.js b/test/js/samples/action/expected.js index 40459f03f9..50c235340a 100644 --- a/test/js/samples/action/expected.js +++ b/test/js/samples/action/expected.js @@ -2,7 +2,7 @@ import { assign, createElement, detachNode, init, insertNode, noop, proto } from "svelte/shared.js"; function link(node) { - + function onClick(event) { event.preventDefault(); history.pushState(null, null, event.target.href); diff --git a/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js b/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js index 86200cb8c0..6788ad53e3 100644 --- a/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js +++ b/test/js/samples/css-shadow-dom-keyframes/expected-bundle.js @@ -185,8 +185,8 @@ class SvelteComponent extends HTMLElement { } } -customElements.define("custom-element", SvelteComponent); -assign(assign(SvelteComponent.prototype, proto), { +assign(SvelteComponent.prototype, proto); +assign(SvelteComponent.prototype, { _mount(target, anchor) { target.insertBefore(this, anchor); }, @@ -196,4 +196,6 @@ assign(assign(SvelteComponent.prototype, proto), { } }); +customElements.define("custom-element", SvelteComponent); + export default SvelteComponent; diff --git a/test/js/samples/css-shadow-dom-keyframes/expected.js b/test/js/samples/css-shadow-dom-keyframes/expected.js index fa4eec75b0..81c7818b0c 100644 --- a/test/js/samples/css-shadow-dom-keyframes/expected.js +++ b/test/js/samples/css-shadow-dom-keyframes/expected.js @@ -51,8 +51,8 @@ class SvelteComponent extends HTMLElement { } } -customElements.define("custom-element", SvelteComponent); -assign(assign(SvelteComponent.prototype, proto), { +assign(SvelteComponent.prototype, proto); +assign(SvelteComponent.prototype, { _mount(target, anchor) { target.insertBefore(this, anchor); }, @@ -61,4 +61,6 @@ assign(assign(SvelteComponent.prototype, proto), { this.parentNode.removeChild(this); } }); + +customElements.define("custom-element", SvelteComponent); export default SvelteComponent; \ No newline at end of file From 1fb404151993189329d0b06592bd4e62ba8b8be7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 28 Apr 2018 22:53:32 -0400 Subject: [PATCH 003/319] overwrite this in custom event handlers - fixes #1297 --- src/compile/nodes/EventHandler.ts | 8 +++++++ src/compile/nodes/shared/Expression.ts | 10 ++++---- src/utils/flattenReference.ts | 7 +++++- .../event-handler-custom-this/_config.js | 22 ++++++++++++++++++ .../event-handler-custom-this/main.html | 23 +++++++++++++++++++ 5 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 test/runtime/samples/event-handler-custom-this/_config.js create mode 100644 test/runtime/samples/event-handler-custom-this/main.html diff --git a/src/compile/nodes/EventHandler.ts b/src/compile/nodes/EventHandler.ts index a733b65540..06d63f7f97 100644 --- a/src/compile/nodes/EventHandler.ts +++ b/src/compile/nodes/EventHandler.ts @@ -80,5 +80,13 @@ export default class EventHandler extends Node { this.args.forEach(arg => { arg.overwriteThis(this.parent.var); }); + + if (this.isCustomEvent && this.callee && this.callee.name === 'this') { + const node = this.callee.nodes[0]; + compiler.code.overwrite(node.start, node.end, this.parent.var, { + storeName: true, + contentOnly: true + }); + } } } \ No newline at end of file diff --git a/src/compile/nodes/shared/Expression.ts b/src/compile/nodes/shared/Expression.ts index f4278cebec..906ad29c01 100644 --- a/src/compile/nodes/shared/Expression.ts +++ b/src/compile/nodes/shared/Expression.ts @@ -107,7 +107,7 @@ export default class Expression { } if (isReference(node, parent)) { - const { name } = flattenReference(node); + const { name, nodes } = flattenReference(node); if (currentScope.has(name) || (name === 'event' && isEventHandler)) return; @@ -139,11 +139,9 @@ export default class Expression { } if (node.type === 'MemberExpression') { - walk(node, { - enter(node) { - code.addSourcemapLocation(node.start); - code.addSourcemapLocation(node.end); - } + nodes.forEach(node => { + code.addSourcemapLocation(node.start); + code.addSourcemapLocation(node.end); }); } diff --git a/src/utils/flattenReference.ts b/src/utils/flattenReference.ts index 7e000875db..1ac16950de 100644 --- a/src/utils/flattenReference.ts +++ b/src/utils/flattenReference.ts @@ -2,11 +2,14 @@ import { Node } from '../interfaces'; export default function flattenReference(node: Node) { if (node.type === 'Expression') throw new Error('bad'); + const nodes = []; const parts = []; const propEnd = node.end; while (node.type === 'MemberExpression') { if (node.computed) return null; + + nodes.unshift(node.property); parts.unshift(node.property.name); node = node.object; @@ -20,5 +23,7 @@ export default function flattenReference(node: Node) { if (!name) return null; parts.unshift(name); - return { name, parts, keypath: `${name}[✂${propStart}-${propEnd}✂]` }; + nodes.unshift(node); + + return { name, nodes, parts, keypath: `${name}[✂${propStart}-${propEnd}✂]` }; } diff --git a/test/runtime/samples/event-handler-custom-this/_config.js b/test/runtime/samples/event-handler-custom-this/_config.js new file mode 100644 index 0000000000..bd2e47ab50 --- /dev/null +++ b/test/runtime/samples/event-handler-custom-this/_config.js @@ -0,0 +1,22 @@ +export default { + html: ``, + + test(assert, component, target, window) { + const input = target.querySelector('input'); + const event = new window.KeyboardEvent('keydown', { + which: 13 + }); + + let blurred = false; + + input.focus(); + + input.addEventListener('blur', () => { + blurred = true; + }); + + input.dispatchEvent(event); + + assert.ok(blurred); + }, +}; diff --git a/test/runtime/samples/event-handler-custom-this/main.html b/test/runtime/samples/event-handler-custom-this/main.html new file mode 100644 index 0000000000..68140628e3 --- /dev/null +++ b/test/runtime/samples/event-handler-custom-this/main.html @@ -0,0 +1,23 @@ + + + \ No newline at end of file From 0dafc34de08bbe04be78ca9847f7ea88ca0b8c89 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 28 Apr 2018 23:39:36 -0400 Subject: [PATCH 004/319] implement full-state computed properties - fixes #1303 --- src/compile/Compiler.ts | 32 +++++++++++++------ src/compile/dom/index.ts | 21 ++++++++---- src/compile/ssr/index.ts | 3 +- src/validate/js/propValidators/computed.ts | 9 ------ .../samples/computed-state-object/_config.js | 21 ++++++++++++ .../samples/computed-state-object/main.html | 14 ++++++++ 6 files changed, 72 insertions(+), 28 deletions(-) create mode 100644 test/runtime/samples/computed-state-object/_config.js create mode 100644 test/runtime/samples/computed-state-object/main.html diff --git a/src/compile/Compiler.ts b/src/compile/Compiler.ts index da5207de39..ac584816b1 100644 --- a/src/compile/Compiler.ts +++ b/src/compile/Compiler.ts @@ -564,16 +564,29 @@ export default class Compiler { if (templateProperties.computed) { const dependencies = new Map(); + const fullStateComputations = []; + templateProperties.computed.value.properties.forEach((prop: Node) => { const key = getName(prop.key); const value = prop.value; - const deps = value.params[0].properties.map(prop => prop.key.name); - - deps.forEach(dep => { - this.expectedProperties.add(dep); + addDeclaration(key, value, false, 'computed', { + state: true, + changed: true }); - dependencies.set(key, deps); + + const param = value.params[0]; + + if (param.type === 'ObjectPattern') { + const deps = param.properties.map(prop => prop.key.name); + + deps.forEach(dep => { + this.expectedProperties.add(dep); + }); + dependencies.set(key, deps); + } else { + fullStateComputations.push({ key, deps: null }) + } }); const visited = new Set(); @@ -590,16 +603,15 @@ export default class Compiler { computations.push({ key, deps }); const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key); - - addDeclaration(key, prop.value, false, 'computed', { - state: true, - changed: true - }); }; templateProperties.computed.value.properties.forEach((prop: Node) => visit(getName(prop.key)) ); + + if (fullStateComputations.length > 0) { + computations.push(...fullStateComputations); + } } if (templateProperties.data) { diff --git a/src/compile/dom/index.ts b/src/compile/dom/index.ts index 3db1158449..db9b7061d6 100644 --- a/src/compile/dom/index.ts +++ b/src/compile/dom/index.ts @@ -64,10 +64,6 @@ export default function dom( if (computations.length) { computations.forEach(({ key, deps }) => { - deps.forEach(dep => { - computationDeps.add(dep); - }); - if (target.readonly.has(key)) { // bindings throw new Error( @@ -77,11 +73,22 @@ export default function dom( target.readonly.add(key); - const condition = `${deps.map(dep => `changed.${dep}`).join(' || ')}`; + if (deps) { + deps.forEach(dep => { + computationDeps.add(dep); + }); - const statement = `if (this._differs(state.${key}, (state.${key} = %computed-${key}(state)))) changed.${key} = true;`; + const condition = `${deps.map(dep => `changed.${dep}`).join(' || ')}`; + const statement = `if (this._differs(state.${key}, (state.${key} = %computed-${key}(state)))) changed.${key} = true;`; - computationBuilder.addConditional(condition, statement); + computationBuilder.addConditional(condition, statement); + } else { + // computed property depends on entire state object — + // these must go at the end + computationBuilder.addLine( + `if (this._differs(state.${key}, (state.${key} = %computed-${key}(state)))) changed.${key} = true;` + ); + } }); } diff --git a/src/compile/ssr/index.ts b/src/compile/ssr/index.ts index 5f5a410a94..8fccc9b4f2 100644 --- a/src/compile/ssr/index.ts +++ b/src/compile/ssr/index.ts @@ -120,8 +120,7 @@ export default function ssr( ctx = Object.assign(${initialState.join(', ')}); ${computations.map( - ({ key, deps }) => - `ctx.${key} = %computed-${key}(ctx);` + ({ key }) => `ctx.${key} = %computed-${key}(ctx);` )} ${target.bindings.length && diff --git a/src/validate/js/propValidators/computed.ts b/src/validate/js/propValidators/computed.ts index d918fe3e8b..d86f1d2650 100644 --- a/src/validate/js/propValidators/computed.ts +++ b/src/validate/js/propValidators/computed.ts @@ -81,14 +81,5 @@ export default function computed(validator: Validator, prop: Node) { message: `Computed properties must take a single argument` }); } - - const param = params[0]; - if (param.type !== 'ObjectPattern') { - // TODO post-v2, allow the entire object to be passed in - validator.error(computation.value, { - code: `invalid-computed-argument`, - message: `Computed property argument must be a destructured object pattern` - }); - } }); } diff --git a/test/runtime/samples/computed-state-object/_config.js b/test/runtime/samples/computed-state-object/_config.js new file mode 100644 index 0000000000..d9fc4d82ba --- /dev/null +++ b/test/runtime/samples/computed-state-object/_config.js @@ -0,0 +1,21 @@ +export default { + data: { a: 1 }, + + html: ` +

a: 1

+

x: 2

+

y: 4

+

z: 8

+ `, + + test(assert, component, target) { + component.set({ a: 2 }); + + assert.htmlEqual(target.innerHTML, ` +

a: 2

+

x: 4

+

y: 8

+

z: 16

+ `) + }, +}; diff --git a/test/runtime/samples/computed-state-object/main.html b/test/runtime/samples/computed-state-object/main.html new file mode 100644 index 0000000000..c8c313c8fb --- /dev/null +++ b/test/runtime/samples/computed-state-object/main.html @@ -0,0 +1,14 @@ +

a: {a}

+

x: {x}

+

y: {y}

+

z: {z}

+ + From f0bf1ef8d5814e36b4d556753cfe953755e329b4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 28 Apr 2018 23:51:05 -0400 Subject: [PATCH 005/319] recalculate each block values lazily - fixes #1286 --- src/compile/nodes/EachBlock.ts | 4 ++-- test/js/samples/deconflict-builtins/expected-bundle.js | 4 ++-- test/js/samples/deconflict-builtins/expected.js | 4 ++-- test/js/samples/each-block-changed-check/expected-bundle.js | 4 ++-- test/js/samples/each-block-changed-check/expected.js | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts index cb58b31411..f96b330064 100644 --- a/src/compile/nodes/EachBlock.ts +++ b/src/compile/nodes/EachBlock.ts @@ -443,9 +443,9 @@ export default class EachBlock extends Node { `; block.builders.update.addBlock(deindent` - var ${each_block_value} = ${snippet}; - if (${condition}) { + ${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')} diff --git a/test/js/samples/deconflict-builtins/expected-bundle.js b/test/js/samples/deconflict-builtins/expected-bundle.js index 4bc21f60a6..b677e75e29 100644 --- a/test/js/samples/deconflict-builtins/expected-bundle.js +++ b/test/js/samples/deconflict-builtins/expected-bundle.js @@ -186,9 +186,9 @@ function create_main_fragment(component, ctx) { }, p: function update(changed, ctx) { - var each_value = ctx.createElement; - if (changed.createElement) { + each_value = ctx.createElement; + for (var i = 0; i < each_value.length; i += 1) { var each_context = assign(assign({}, ctx), { each_value: each_value, diff --git a/test/js/samples/deconflict-builtins/expected.js b/test/js/samples/deconflict-builtins/expected.js index aa928967c4..ce7e2fa52f 100644 --- a/test/js/samples/deconflict-builtins/expected.js +++ b/test/js/samples/deconflict-builtins/expected.js @@ -34,9 +34,9 @@ function create_main_fragment(component, ctx) { }, p: function update(changed, ctx) { - var each_value = ctx.createElement; - if (changed.createElement) { + each_value = ctx.createElement; + for (var i = 0; i < each_value.length; i += 1) { var each_context = assign(assign({}, ctx), { each_value: each_value, 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 560c0514ae..f4d29ba84a 100644 --- a/test/js/samples/each-block-changed-check/expected-bundle.js +++ b/test/js/samples/each-block-changed-check/expected-bundle.js @@ -192,9 +192,9 @@ function create_main_fragment(component, ctx) { }, p: function update(changed, ctx) { - var each_value = ctx.comments; - if (changed.comments || changed.elapsed || changed.time) { + each_value = ctx.comments; + for (var i = 0; i < each_value.length; i += 1) { var each_context = assign(assign({}, ctx), { each_value: each_value, diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js index 6b15078c79..75dfc20a7d 100644 --- a/test/js/samples/each-block-changed-check/expected.js +++ b/test/js/samples/each-block-changed-check/expected.js @@ -38,9 +38,9 @@ function create_main_fragment(component, ctx) { }, p: function update(changed, ctx) { - var each_value = ctx.comments; - if (changed.comments || changed.elapsed || changed.time) { + each_value = ctx.comments; + for (var i = 0; i < each_value.length; i += 1) { var each_context = assign(assign({}, ctx), { each_value: each_value, From 5fd4965b647db670a16e2c2634b6ed98697c6c9f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 29 Apr 2018 09:48:17 -0400 Subject: [PATCH 006/319] preserve outer context for await blocks - fixes #1251 --- src/compile/nodes/AwaitBlock.ts | 8 ++--- test/runtime/samples/await-in-each/_config.js | 31 +++++++++++++++++++ test/runtime/samples/await-in-each/main.html | 7 +++++ 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 test/runtime/samples/await-in-each/_config.js create mode 100644 test/runtime/samples/await-in-each/main.html diff --git a/src/compile/nodes/AwaitBlock.ts b/src/compile/nodes/AwaitBlock.ts index 87e1f7a850..60ce1ad23d 100644 --- a/src/compile/nodes/AwaitBlock.ts +++ b/src/compile/nodes/AwaitBlock.ts @@ -98,6 +98,8 @@ export default class AwaitBlock extends Node { block.addVariable(promise); block.addVariable(resolved); + block.maintainContext = true; + // the `#component.root.set({})` below is just a cheap way to flush // any oncreate handlers. We could have a dedicated `flush()` method // but it's probably not worth it @@ -119,13 +121,12 @@ export default class AwaitBlock extends Node { } } - function ${handle_promise}(${promise}, ctx) { + function ${handle_promise}(${promise}) { var ${token} = ${await_token} = {}; if (@isPromise(${promise})) { ${promise}.then(function(${value}) { ${this.value ? deindent` - var ctx = #component.get(); ${resolved} = { ${this.value}: ${value} }; ${replace_await_block}(${token}, ${create_then_block}, @assign(@assign({}, ctx), ${resolved})); ` : deindent` @@ -133,7 +134,6 @@ export default class AwaitBlock extends Node { `} }, function (${error}) { ${this.error ? deindent` - var ctx = #component.get(); ${resolved} = { ${this.error}: ${error} }; ${replace_await_block}(${token}, ${create_catch_block}, @assign(@assign({}, ctx), ${resolved})); ` : deindent` @@ -155,7 +155,7 @@ export default class AwaitBlock extends Node { } } - ${handle_promise}(${promise} = ${snippet}, ctx); + ${handle_promise}(${promise} = ${snippet}); `); block.builders.create.addBlock(deindent` diff --git a/test/runtime/samples/await-in-each/_config.js b/test/runtime/samples/await-in-each/_config.js new file mode 100644 index 0000000000..6912341d66 --- /dev/null +++ b/test/runtime/samples/await-in-each/_config.js @@ -0,0 +1,31 @@ +let fulfil; + +let thePromise = new Promise(f => { + fulfil = f; +}); + +const items = [{ + title: 'a title', + data: thePromise +}]; + +export default { + data: { + items + }, + + html: ` +

a title: loading...

+ `, + + test(assert, component, target) { + fulfil(42); + + return thePromise + .then(() => { + assert.htmlEqual(target.innerHTML, ` +

a title: 42

+ `); + }); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/await-in-each/main.html b/test/runtime/samples/await-in-each/main.html new file mode 100644 index 0000000000..a928f933aa --- /dev/null +++ b/test/runtime/samples/await-in-each/main.html @@ -0,0 +1,7 @@ +{#each items as item} + {#await item.data} +

{item.title}: loading...

+ {:then result} +

{item.title}: {result}

+ {/await} +{/each} \ No newline at end of file From dbab1a886d3657d99445e275e7587a016f3d87dc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 29 Apr 2018 10:56:55 -0400 Subject: [PATCH 007/319] -> v2.3.0 --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd71fd4a95..d8010c253e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Svelte changelog +## 2.3.0 + +* Allow computed properties to have entire state object as dependency ([#1303](https://github.com/sveltejs/svelte/issues/1303)) +* Fix `stats` when `options.generate` is `false` ([#1368](https://github.com/sveltejs/svelte/issues/1368)) +* Assign custom methods to custom elements ([#1369](https://github.com/sveltejs/svelte/issues/1369)) +* Fix `this` value in custom event handlers ([#1297](https://github.com/sveltejs/svelte/issues/1297)) +* Re-evaluate `each` values lazily ([#1286](https://github.com/sveltejs/svelte/issues/1286)) +* Preserve outer context in `await` blocks ([#1251](https://github.com/sveltejs/svelte/issues/1251)) + ## 2.2.0 * Internal refactoring ([#1367](https://github.com/sveltejs/svelte/pull/1367)) diff --git a/package.json b/package.json index 539e1933cd..651cda4a81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "2.2.0", + "version": "2.3.0", "description": "The magical disappearing UI framework", "main": "compiler/svelte.js", "files": [ From 8dc17b77db85a8d974c10239676a5e169e01e072 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 29 Apr 2018 11:34:45 -0400 Subject: [PATCH 008/319] 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 009/319] 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 010/319] 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 011/319] 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 012/319] 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 013/319] 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 014/319] 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