From b5102f4f1badb39d67ab475ce91ced6927fd1ffa Mon Sep 17 00:00:00 2001 From: mrkishi Date: Fri, 16 Mar 2018 16:04:28 -0300 Subject: [PATCH 01/15] Add spread -- rough idea --- src/generators/Generator.ts | 4 + src/generators/nodes/Component.ts | 22 ++- src/generators/nodes/Element.ts | 8 + src/generators/nodes/Spread.ts | 173 ++++++++++++++++++ src/generators/nodes/index.ts | 4 +- src/parse/state/tag.ts | 26 +++ src/shared/dom.js | 6 +- test/parser/samples/spread/input.html | 1 + test/parser/samples/spread/output.json | 31 ++++ .../samples/spread-component/Widget.html | 4 + .../samples/spread-component/_config.js | 27 +++ .../samples/spread-component/main.html | 11 ++ .../runtime/samples/spread-element/_config.js | 19 ++ test/runtime/samples/spread-element/main.html | 12 ++ 14 files changed, 343 insertions(+), 5 deletions(-) create mode 100644 src/generators/nodes/Spread.ts create mode 100644 test/parser/samples/spread/input.html create mode 100644 test/parser/samples/spread/output.json create mode 100644 test/runtime/samples/spread-component/Widget.html create mode 100644 test/runtime/samples/spread-component/_config.js create mode 100644 test/runtime/samples/spread-component/main.html create mode 100644 test/runtime/samples/spread-element/_config.js create mode 100644 test/runtime/samples/spread-element/main.html diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index b53851cec9ae..c3d3b428d2fb 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -827,6 +827,10 @@ export default class Generator { if (node.type === 'Component' && node.name === ':Component') { node.metadata = contextualise(node.expression, contextDependencies, indexes, false); } + + if (node.type === 'Spread') { + node.metadata = contextualise(node.expression, contextDependencies, indexes, false); + } }, leave(node: Node, parent: Node) { diff --git a/src/generators/nodes/Component.ts b/src/generators/nodes/Component.ts index 566b1c83a1c3..22683e3429db 100644 --- a/src/generators/nodes/Component.ts +++ b/src/generators/nodes/Component.ts @@ -48,6 +48,10 @@ export default class Component extends Node { } }); + if (this.spread) { + block.addDependencies(this.spread.metadata.dependencies); + } + this.var = block.getUniqueName( ( this.name === ':Self' ? this.generator.name : @@ -76,6 +80,7 @@ export default class Component extends Node { const name = this.var; const componentInitProperties = [`root: #component.root`]; + let componentInitialData = null; if (this.children.length > 0) { const slots = Array.from(this._slots).map(name => `${quoteIfNecessary(name, generator.legacy)}: @createFragment()`); @@ -224,7 +229,7 @@ export default class Component extends Node { } }); - componentInitProperties.push(`data: ${name_initial_data}`); + componentInitialData = name_initial_data; const initialisers = [ 'state = #component.get()', @@ -248,10 +253,21 @@ export default class Component extends Node { }); `; } else if (initialProps.length) { - componentInitProperties.push(`data: ${initialPropString}`); + componentInitialData = initialPropString; } } + if (this.spread) { + const initialData = this.spread.renderForComponent(block, updates); + componentInitialData = componentInitialData ? + `@assign({}, ${initialData}, ${componentInitialData})` : + initialData; + } + + if (componentInitialData) { + componentInitProperties.push(`data: ${componentInitialData}`); + } + const isDynamicComponent = this.name === ':Component'; const switch_vars = isDynamicComponent && { @@ -551,4 +567,4 @@ function isComputed(node: Node) { } return false; -} \ No newline at end of file +} diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index 42db165f9daf..857fe2bbc138 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -138,6 +138,10 @@ export default class Element extends Node { component._slots.add(slot); } + if (this.spread) { + block.addDependencies(this.spread.metadata.dependencies); + } + this.var = block.getUniqueName( this.name.replace(/[^a-zA-Z0-9_$]/g, '_') ); @@ -240,6 +244,10 @@ export default class Element extends Node { attribute.render(block); }); + if (this.spread) { + this.spread.renderForElement(block); + } + // event handlers let eventHandlerUsesComponent = false; diff --git a/src/generators/nodes/Spread.ts b/src/generators/nodes/Spread.ts new file mode 100644 index 000000000000..984af4a95815 --- /dev/null +++ b/src/generators/nodes/Spread.ts @@ -0,0 +1,173 @@ +import deindent from '../../utils/deindent'; +import { DomGenerator } from '../dom/index'; +import Node from './shared/Node'; +import Element from './Element'; +import Block from '../dom/Block'; + +export default class Spread { + type: 'Spread'; + start: number; + end: number; + + generator: DomGenerator; + parent: Element; + expression: Node; + + metadata: { + dependencies: string[]; + snippet: string; + }; + + constructor({ + generator, + expression, + parent + }: { + generator: DomGenerator, + expression: Node, + parent: Element + }) { + this.type = 'Spread'; + this.generator = generator; + this.parent = parent; + + this.expression = expression; + } + + renderForElement(block: Block) { + const node = this.parent; + + const { expression } = this; + const { indexes } = block.contextualise(expression); + const { dependencies, snippet } = this.metadata; + + const value = snippet; + + const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index)); + + const shouldCache = ( + expression.type !== 'Identifier' || + block.contexts.has(expression.name) || + hasChangeableIndex + ); + + const last = shouldCache && block.getUniqueName(`${node.var}_spread_value`); + + if (shouldCache) block.addVariable(last); + + const init = shouldCache ? `${last} = ${value}` : value; + + const activeKeys = block.getUniqueName(`${node.var}_spread_keys`); + block.addVariable(activeKeys, '{}'); + + const changes = block.getUniqueName(`${node.var}_spread_changes`); + + block.builders.hydrate.addBlock(deindent` + var ${changes} = ${init}; + for (var key in ${changes}) { + @setAttribute(${node.var}, key, ${changes}[key]); + ${activeKeys}[key] = true; + } + `); + + if (dependencies.length || hasChangeableIndex) { + const changedCheck = ( + ( block.hasOutroMethod ? `#outroing || ` : '' ) + + dependencies.map(dependency => `changed.${dependency}`).join(' || ') + ); + + const updateCachedValue = `${last} !== (${last} = ${value})`; + + const condition = shouldCache ? + ( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) : + changedCheck; + + const oldKeys = block.getUniqueName(`${node.var}_spread_keys_old`); + + const updater = deindent` + var ${oldKeys} = ${activeKeys}; + ${activeKeys} = {}; + + var ${changes} = ${shouldCache ? last : value}; + for (var key in ${changes}) { + ${activeKeys}[key] = true; + delete ${oldKeys}[key]; + @setAttribute(${node.var}, key, ${changes}[key]); + } + + for (var key in ${oldKeys}) { + @removeAttribute(${node.var}, key); + } + `; + + block.builders.update.addConditional( + condition, + updater + ); + } + } + + renderForComponent(block: Block, updates: string[]) { + const node = this.parent; + + const { expression } = this; + const { indexes } = block.contextualise(expression); + const { dependencies, snippet } = this.metadata; + + const value = snippet; + + const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index)); + + const shouldCache = ( + expression.type !== 'Identifier' || + block.contexts.has(expression.name) || + hasChangeableIndex + ); + + const last = shouldCache && block.getUniqueName(`${node.var}_spread_value`); + + if (shouldCache) block.addVariable(last); + + const init = shouldCache ? `${last} = ${value}` : value; + + const activeKeys = block.getUniqueName(`${node.var}_spread_keys`); + block.addVariable(activeKeys, '{}'); + + const changes = block.getUniqueName(`${node.var}_spread_changes`); + + if (dependencies.length || hasChangeableIndex) { + const changedCheck = ( + ( block.hasOutroMethod ? `#outroing || ` : '' ) + + dependencies.map(dependency => `changed.${dependency}`).join(' || ') + ); + + const updateCachedValue = `${last} !== (${last} = ${value})`; + + const condition = shouldCache ? + ( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) : + changedCheck; + + const oldKeys = block.getUniqueName(`${node.var}_spread_keys_old`); + + updates.push(deindent` + if (${condition}) { + var ${oldKeys} = ${activeKeys}; + ${activeKeys} = {}; + + var ${changes} = ${shouldCache ? last : value}; + for (var key in ${changes}) { + ${activeKeys}[key] = true; + delete ${oldKeys}[key]; + ${node.var}_changes[key] = ${changes}[key]; + } + + for (var key in ${oldKeys}) { + ${node.var}_changes[key] = undefined; + } + } + `); + } + + return value; + } +} diff --git a/src/generators/nodes/index.ts b/src/generators/nodes/index.ts index c9602455f317..5701ed3666a3 100644 --- a/src/generators/nodes/index.ts +++ b/src/generators/nodes/index.ts @@ -17,6 +17,7 @@ import PendingBlock from './PendingBlock'; import RawMustacheTag from './RawMustacheTag'; import Ref from './Ref'; import Slot from './Slot'; +import Spread from './Spread'; import Text from './Text'; import ThenBlock from './ThenBlock'; import Title from './Title'; @@ -42,6 +43,7 @@ const nodes: Record = { RawMustacheTag, Ref, Slot, + Spread, Text, ThenBlock, Title, @@ -49,4 +51,4 @@ const nodes: Record = { Window }; -export default nodes; \ No newline at end of file +export default nodes; diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index 79aa289f20ea..897478c6dddf 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -178,6 +178,8 @@ export default function tag(parser: Parser) { parser.allowWhitespace(); } + element.spread = readSpread(parser); + const uniqueNames = new Set(); let attribute; @@ -384,3 +386,27 @@ function readSequence(parser: Parser, done: () => boolean) { parser.error(`Unexpected end of input`); } + +function readSpread(parser: Parser) { + const start = parser.index; + + if (parser.eat('{{...')) { + const expression = readExpression(parser); + parser.allowWhitespace(); + + if (!parser.eat('}}')) { + parser.error(`Expected }}`); + } + + parser.allowWhitespace(); + + return { + start, + end: parser.index, + type: 'Spread', + expression, + }; + } + + return null; +} diff --git a/src/shared/dom.js b/src/shared/dom.js index 9003faee3e14..8cde378ad698 100644 --- a/src/shared/dom.js +++ b/src/shared/dom.js @@ -85,6 +85,10 @@ export function setAttribute(node, attribute, value) { node.setAttribute(attribute, value); } +export function removeAttribute(node, attribute) { + node.removeAttribute(attribute); +} + export function setXlinkAttribute(node, attribute, value) { node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value); } @@ -177,4 +181,4 @@ export function selectMultipleValue(select) { return [].map.call(select.querySelectorAll(':checked'), function(option) { return option.__value; }); -} \ No newline at end of file +} diff --git a/test/parser/samples/spread/input.html b/test/parser/samples/spread/input.html new file mode 100644 index 000000000000..b5a2c7960057 --- /dev/null +++ b/test/parser/samples/spread/input.html @@ -0,0 +1 @@ +
diff --git a/test/parser/samples/spread/output.json b/test/parser/samples/spread/output.json new file mode 100644 index 000000000000..b4ae6a7e01f7 --- /dev/null +++ b/test/parser/samples/spread/output.json @@ -0,0 +1,31 @@ +{ + "hash": "phg0l6", + "html": { + "start": 0, + "end": 24, + "type": "Fragment", + "children": [ + { + "start": 0, + "end": 24, + "type": "Element", + "name": "div", + "attributes": [], + "children": [], + "spread": { + "start": 5, + "end": 17, + "type": "Spread", + "expression": { + "type": "Identifier", + "start": 10, + "end": 15, + "name": "props" + } + } + } + ] + }, + "css": null, + "js": null +} diff --git a/test/runtime/samples/spread-component/Widget.html b/test/runtime/samples/spread-component/Widget.html new file mode 100644 index 000000000000..43184b9ad896 --- /dev/null +++ b/test/runtime/samples/spread-component/Widget.html @@ -0,0 +1,4 @@ +

foo: {{foo}}

+

baz: {{baz}} ({{typeof baz}})

+

qux: {{qux}}

+

quux: {{quux}}

diff --git a/test/runtime/samples/spread-component/_config.js b/test/runtime/samples/spread-component/_config.js new file mode 100644 index 000000000000..d08c6576ff58 --- /dev/null +++ b/test/runtime/samples/spread-component/_config.js @@ -0,0 +1,27 @@ +export default { + solo: true, + + data: { + props: { + foo: 'lol', + baz: 40 + 2, + qux: `this is a ${'piece of'} string`, + quux: 'core' + } + }, + + html: `

foo: lol

\n

baz: 42 (number)

\n

qux: this is a piece of string

\n

quux: core

`, + + test ( assert, component, target ) { + component.set({ + props: { + foo: 'wut', + baz: 40 + 3, + qux: `this is a ${'rather boring'} string`, + quux: 'heart' + } + }); + + assert.equal( target.innerHTML, `

foo: wut

\n

baz: 43 (number)

\n

qux: this is a rather boring string

\n

quux: heart

` ); + } +}; diff --git a/test/runtime/samples/spread-component/main.html b/test/runtime/samples/spread-component/main.html new file mode 100644 index 000000000000..ac5a7c8f7306 --- /dev/null +++ b/test/runtime/samples/spread-component/main.html @@ -0,0 +1,11 @@ +
+ +
+ + diff --git a/test/runtime/samples/spread-element/_config.js b/test/runtime/samples/spread-element/_config.js new file mode 100644 index 000000000000..e3d3abd1f2a0 --- /dev/null +++ b/test/runtime/samples/spread-element/_config.js @@ -0,0 +1,19 @@ +export default { + solo: true, + + html: `
red
`, + + test ( assert, component, target ) { + const div = target.querySelector( 'div' ); + + assert.equal( div.dataset.foo, 'bar' ); + + component.set({ color: 'blue', props: { 'data-foo': 'baz' } }); + assert.equal( target.innerHTML, `
blue
` ); + assert.equal( div.dataset.foo, 'baz' ); + + component.set({ color: 'blue', props: {} }); + assert.equal( target.innerHTML, `
blue
` ); + assert.equal( div.dataset.foo, undefined ); + } +}; diff --git a/test/runtime/samples/spread-element/main.html b/test/runtime/samples/spread-element/main.html new file mode 100644 index 000000000000..381bfdfade70 --- /dev/null +++ b/test/runtime/samples/spread-element/main.html @@ -0,0 +1,12 @@ +
{{color}}
+ + From 9b80eee51af86a317e485952d15fb37b62416b58 Mon Sep 17 00:00:00 2001 From: mrkishi Date: Fri, 16 Mar 2018 16:32:23 -0300 Subject: [PATCH 02/15] Prioritize named over spread attributes --- src/generators/nodes/Spread.ts | 22 +++++++++++++++++-- .../samples/spread-component/_config.js | 4 ++-- .../samples/spread-component/main.html | 2 +- .../runtime/samples/spread-element/_config.js | 10 +++++---- test/runtime/samples/spread-element/main.html | 3 ++- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/generators/nodes/Spread.ts b/src/generators/nodes/Spread.ts index 984af4a95815..d07cc40ac843 100644 --- a/src/generators/nodes/Spread.ts +++ b/src/generators/nodes/Spread.ts @@ -62,9 +62,16 @@ export default class Spread { const changes = block.getUniqueName(`${node.var}_spread_changes`); + const namedAttributes = block.getUniqueName(`${node.var}_attributes`); + block.builders.init.addBlock(deindent` + var ${namedAttributes} = [${node.attributes.map(attr => `'${attr.name}'`).join(', ')}]; + `) + block.builders.hydrate.addBlock(deindent` var ${changes} = ${init}; for (var key in ${changes}) { + if (${namedAttributes}.indexOf(key) !== -1) continue; + @setAttribute(${node.var}, key, ${changes}[key]); ${activeKeys}[key] = true; } @@ -90,9 +97,12 @@ export default class Spread { var ${changes} = ${shouldCache ? last : value}; for (var key in ${changes}) { + if (${namedAttributes}.indexOf(key) !== -1) continue; + + @setAttribute(${node.var}, key, ${changes}[key]); + ${activeKeys}[key] = true; delete ${oldKeys}[key]; - @setAttribute(${node.var}, key, ${changes}[key]); } for (var key in ${oldKeys}) { @@ -135,6 +145,11 @@ export default class Spread { const changes = block.getUniqueName(`${node.var}_spread_changes`); + const namedAttributes = block.getUniqueName(`${node.var}_attributes`); + block.builders.init.addBlock(deindent` + var ${namedAttributes} = [${node.attributes.map(attr => `'${attr.name}'`).join(', ')}]; + `) + if (dependencies.length || hasChangeableIndex) { const changedCheck = ( ( block.hasOutroMethod ? `#outroing || ` : '' ) + @@ -156,9 +171,12 @@ export default class Spread { var ${changes} = ${shouldCache ? last : value}; for (var key in ${changes}) { + if (${namedAttributes}.indexOf(key) !== -1) continue; + + ${node.var}_changes[key] = ${changes}[key]; + ${activeKeys}[key] = true; delete ${oldKeys}[key]; - ${node.var}_changes[key] = ${changes}[key]; } for (var key in ${oldKeys}) { diff --git a/test/runtime/samples/spread-component/_config.js b/test/runtime/samples/spread-component/_config.js index d08c6576ff58..66df4c04e750 100644 --- a/test/runtime/samples/spread-component/_config.js +++ b/test/runtime/samples/spread-component/_config.js @@ -10,7 +10,7 @@ export default { } }, - html: `

foo: lol

\n

baz: 42 (number)

\n

qux: this is a piece of string

\n

quux: core

`, + html: `

foo: lol

\n

baz: 42 (number)

\n

qux: named

\n

quux: core

`, test ( assert, component, target ) { component.set({ @@ -22,6 +22,6 @@ export default { } }); - assert.equal( target.innerHTML, `

foo: wut

\n

baz: 43 (number)

\n

qux: this is a rather boring string

\n

quux: heart

` ); + assert.equal( target.innerHTML, `

foo: wut

\n

baz: 43 (number)

\n

qux: named

\n

quux: heart

` ); } }; diff --git a/test/runtime/samples/spread-component/main.html b/test/runtime/samples/spread-component/main.html index ac5a7c8f7306..e6b16a44296f 100644 --- a/test/runtime/samples/spread-component/main.html +++ b/test/runtime/samples/spread-component/main.html @@ -1,5 +1,5 @@
- +