diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 72edbe8d0e5c..25813e0c4b0f 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -845,6 +845,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/Attribute.ts b/src/generators/nodes/Attribute.ts index 90031d261e19..904c971c1df3 100644 --- a/src/generators/nodes/Attribute.ts +++ b/src/generators/nodes/Attribute.ts @@ -223,10 +223,9 @@ export default class Attribute { ); } } else { - const value = - this.value === true + const value = this.value === true ? 'true' - : this.value.length === 0 ? `''` : stringify(this.value[0].data); + : this.value.length === 0 ? `""` : stringify(this.value[0].data); const statement = ( isLegacyInputType diff --git a/src/generators/nodes/Component.ts b/src/generators/nodes/Component.ts index 481c7de0e392..a8c013b25897 100644 --- a/src/generators/nodes/Component.ts +++ b/src/generators/nodes/Component.ts @@ -1,21 +1,14 @@ import deindent from '../../utils/deindent'; -import { stringify } from '../../utils/stringify'; import stringifyProps from '../../utils/stringifyProps'; import CodeBuilder from '../../utils/CodeBuilder'; import getTailSnippet from '../../utils/getTailSnippet'; import getObject from '../../utils/getObject'; -import getExpressionPrecedence from '../../utils/getExpressionPrecedence'; -import isValidIdentifier from '../../utils/isValidIdentifier'; -import reservedNames from '../../utils/reservedNames'; +import quoteIfNecessary from '../../utils/quoteIfNecessary'; +import mungeAttribute from './shared/mungeAttribute'; import Node from './shared/Node'; import Block from '../dom/Block'; import Attribute from './Attribute'; -function quoteIfNecessary(name, legacy) { - if (!isValidIdentifier || (legacy && reservedNames.has(name))) return `"${name}"`; - return name; -} - export default class Component extends Node { type: 'Component'; name: string; @@ -89,12 +82,13 @@ export default class Component extends Node { const allContexts = new Set(); const statements: string[] = []; + const name_initial_data = block.getUniqueName(`${name}_initial_data`); + const name_changes = block.getUniqueName(`${name}_changes`); let name_updating: string; - let name_initial_data: string; let beforecreate: string = null; const attributes = this.attributes - .filter(a => a.type === 'Attribute') + .filter(a => a.type === 'Attribute' || a.type === 'Spread') .map(a => mungeAttribute(a, block)); const bindings = this.attributes @@ -110,180 +104,215 @@ export default class Component extends Node { const updates: string[] = []; + const usesSpread = !!attributes.find(a => a.spread); + + const attributeObject = usesSpread + ? '{}' + : stringifyProps( + attributes.map((attribute: Attribute) => `${attribute.name}: ${attribute.value}`) + ); + if (attributes.length || bindings.length) { - const initialProps = attributes - .map((attribute: Attribute) => `${attribute.name}: ${attribute.value}`); - - const initialPropString = stringifyProps(initialProps); - - attributes - .filter((attribute: Attribute) => attribute.dynamic) - .forEach((attribute: Attribute) => { - if (attribute.dependencies.length) { - updates.push(deindent` - if (${attribute.dependencies - .map(dependency => `changed.${dependency}`) - .join(' || ')}) ${name}_changes.${attribute.name} = ${attribute.value}; - `); - } + componentInitProperties.push(`data: ${name_initial_data}`); + } - else { - // TODO this is an odd situation to encounter – I *think* it should only happen with - // each block indices, in which case it may be possible to optimise this - updates.push(`${name}_changes.${attribute.name} = ${attribute.value};`); - } - }); + if ((!usesSpread && attributes.filter(a => a.dynamic).length) || bindings.length) { + updates.push(`var ${name_changes} = {};`); + } + + if (attributes.length) { + if (usesSpread) { + const levels = block.getUniqueName(`${this.var}_spread_levels`); + + const initialProps = []; + const changes = []; - if (bindings.length) { - generator.hasComplexBindings = true; + attributes + .forEach(munged => { + const { spread, name, dynamic, value, dependencies } = munged; - name_updating = block.alias(`${name}_updating`); - name_initial_data = block.getUniqueName(`${name}_initial_data`); + if (spread) { + initialProps.push(value); - block.addVariable(name_updating, '{}'); - statements.push(`var ${name_initial_data} = ${initialPropString};`); + const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || '); + changes.push(condition ? `${condition} && ${value}` : value); + } else { + const obj = `{ ${quoteIfNecessary(name, this.generator.legacy)}: ${value} }`; + initialProps.push(obj); - let hasLocalBindings = false; - let hasStoreBindings = false; + const condition = dependencies && dependencies.map(d => `changed.${d}`).join(' || '); + changes.push(condition ? `${condition} && ${obj}` : obj); + } + }); - const builder = new CodeBuilder(); + statements.push(deindent` + var ${levels} = [ + ${initialProps.join(',\n')} + ]; - bindings.forEach((binding: Binding) => { - let { name: key } = getObject(binding.value); + for (var #i = 0; #i < ${levels}.length; #i += 1) { + ${name_initial_data} = @assign(${name_initial_data}, ${levels}[#i]); + } + `); - binding.contexts.forEach(context => { - allContexts.add(context); + updates.push(deindent` + var ${name_changes} = @getSpreadUpdate(${levels}, [ + ${changes.join(',\n')} + ]); + `); + } else { + attributes + .filter((attribute: Attribute) => attribute.dynamic) + .forEach((attribute: Attribute) => { + if (attribute.dependencies.length) { + updates.push(deindent` + if (${attribute.dependencies + .map(dependency => `changed.${dependency}`) + .join(' || ')}) ${name_changes}.${attribute.name} = ${attribute.value}; + `); + } + + else { + // TODO this is an odd situation to encounter – I *think* it should only happen with + // each block indices, in which case it may be possible to optimise this + updates.push(`${name_changes}.${attribute.name} = ${attribute.value};`); + } }); + } + } - let setFromChild; + if (bindings.length) { + generator.hasComplexBindings = true; - if (block.contexts.has(key)) { - const computed = isComputed(binding.value); - const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : ''; + name_updating = block.alias(`${name}_updating`); + block.addVariable(name_updating, '{}'); - const list = block.listNames.get(key); - const index = block.indexNames.get(key); + let hasLocalBindings = false; + let hasStoreBindings = false; - setFromChild = deindent` - ${list}[${index}]${tail} = childState.${binding.name}; + const builder = new CodeBuilder(); + + bindings.forEach((binding: Binding) => { + let { name: key } = getObject(binding.value); + + binding.contexts.forEach(context => { + allContexts.add(context); + }); + + let setFromChild; - ${binding.dependencies - .map((name: string) => { - const isStoreProp = generator.options.store && name[0] === '$'; - const prop = isStoreProp ? name.slice(1) : name; - const newState = isStoreProp ? 'newStoreState' : 'newState'; + if (block.contexts.has(key)) { + const computed = isComputed(binding.value); + const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : ''; - if (isStoreProp) hasStoreBindings = true; - else hasLocalBindings = true; + const list = block.listNames.get(key); + const index = block.indexNames.get(key); - return `${newState}.${prop} = state.${name};`; - }) - .join('\n')} + setFromChild = deindent` + ${list}[${index}]${tail} = childState.${binding.name}; + + ${binding.dependencies + .map((name: string) => { + const isStoreProp = generator.options.store && name[0] === '$'; + const prop = isStoreProp ? name.slice(1) : name; + const newState = isStoreProp ? 'newStoreState' : 'newState'; + + if (isStoreProp) hasStoreBindings = true; + else hasLocalBindings = true; + + return `${newState}.${prop} = state.${name};`; + })} + `; + } + + else { + const isStoreProp = generator.options.store && key[0] === '$'; + const prop = isStoreProp ? key.slice(1) : key; + const newState = isStoreProp ? 'newStoreState' : 'newState'; + + if (isStoreProp) hasStoreBindings = true; + else hasLocalBindings = true; + + if (binding.value.type === 'MemberExpression') { + setFromChild = deindent` + ${binding.snippet} = childState.${binding.name}; + ${newState}.${prop} = state.${key}; `; } else { - const isStoreProp = generator.options.store && key[0] === '$'; - const prop = isStoreProp ? key.slice(1) : key; - const newState = isStoreProp ? 'newStoreState' : 'newState'; - - if (isStoreProp) hasStoreBindings = true; - else hasLocalBindings = true; - - if (binding.value.type === 'MemberExpression') { - setFromChild = deindent` - ${binding.snippet} = childState.${binding.name}; - ${newState}.${prop} = state.${key}; - `; - } - - else { - setFromChild = `${newState}.${prop} = childState.${binding.name};`; - } + setFromChild = `${newState}.${prop} = childState.${binding.name};`; } + } - statements.push(deindent` - if (${binding.prop} in ${binding.obj}) { - ${name_initial_data}.${binding.name} = ${binding.snippet}; - ${name_updating}.${binding.name} = true; - }` - ); - - builder.addConditional( - `!${name_updating}.${binding.name} && changed.${binding.name}`, - setFromChild - ); - - // TODO could binding.dependencies.length ever be 0? - if (binding.dependencies.length) { - updates.push(deindent` - if (!${name_updating}.${binding.name} && ${binding.dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')}) { - ${name}_changes.${binding.name} = ${binding.snippet}; - ${name_updating}.${binding.name} = true; - } - `); - } - }); + statements.push(deindent` + if (${binding.prop} in ${binding.obj}) { + ${name_initial_data}.${binding.name} = ${binding.snippet}; + ${name_updating}.${binding.name} = true; + }` + ); - componentInitProperties.push(`data: ${name_initial_data}`); - - const initialisers = [ - 'state = #component.get()', - hasLocalBindings && 'newState = {}', - hasStoreBindings && 'newStoreState = {}', - ].filter(Boolean).join(', '); - - componentInitProperties.push(deindent` - _bind: function(changed, childState) { - var ${initialisers}; - ${builder} - ${hasStoreBindings && `#component.store.set(newStoreState);`} - ${hasLocalBindings && `#component._set(newState);`} - ${name_updating} = {}; + builder.addConditional( + `!${name_updating}.${binding.name} && changed.${binding.name}`, + setFromChild + ); + + updates.push(deindent` + if (!${name_updating}.${binding.name} && ${binding.dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')}) { + ${name_changes}.${binding.name} = ${binding.snippet}; + ${name_updating}.${binding.name} = true; } `); + }); - beforecreate = deindent` - #component.root._beforecreate.push(function() { - ${name}._bind({ ${bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get()); - }); - `; - } else if (initialProps.length) { - componentInitProperties.push(`data: ${initialPropString}`); - } - } - - const isDynamicComponent = this.name === ':Component'; + componentInitProperties.push(`data: ${name_initial_data}`); + + const initialisers = [ + 'state = #component.get()', + hasLocalBindings && 'newState = {}', + hasStoreBindings && 'newStoreState = {}', + ].filter(Boolean).join(', '); + + componentInitProperties.push(deindent` + _bind: function(changed, childState) { + var ${initialisers}; + ${builder} + ${hasStoreBindings && `#component.store.set(newStoreState);`} + ${hasLocalBindings && `#component._set(newState);`} + ${name_updating} = {}; + } + `); - const switch_vars = isDynamicComponent && { - value: block.getUniqueName('switch_value'), - props: block.getUniqueName('switch_props') - }; + beforecreate = deindent` + #component.root._beforecreate.push(function() { + ${name}._bind({ ${bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get()); + }); + `; + } - const expression = ( - this.name === ':Self' ? generator.name : - isDynamicComponent ? switch_vars.value : - `%components-${this.name}` - ); + if (this.name === ':Component') { + const switch_value = block.getUniqueName('switch_value'); + const switch_props = block.getUniqueName('switch_props'); - if (isDynamicComponent) { block.contextualise(this.expression); const { dependencies, snippet } = this.metadata; const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes); block.builders.init.addBlock(deindent` - var ${switch_vars.value} = ${snippet}; + var ${switch_value} = ${snippet}; - function ${switch_vars.props}(state) { - ${statements.length > 0 && statements.join('\n')} + function ${switch_props}(state) { + ${(attributes.length || bindings.length) && deindent` + var ${name_initial_data} = ${attributeObject};`} + ${statements} return { ${componentInitProperties.join(',\n')} }; } - if (${switch_vars.value}) { - var ${name} = new ${expression}(${switch_vars.props}(state)); + if (${switch_value}) { + var ${name} = new ${switch_value}(${switch_props}(state)); ${beforecreate} } @@ -317,11 +346,11 @@ export default class Component extends Node { const updateMountNode = this.getUpdateMountNode(anchor); block.builders.update.addBlock(deindent` - if (${switch_vars.value} !== (${switch_vars.value} = ${snippet})) { + if (${switch_value} !== (${switch_value} = ${snippet})) { if (${name}) ${name}.destroy(); - if (${switch_vars.value}) { - ${name} = new ${switch_vars.value}(${switch_vars.props}(state)); + if (${switch_value}) { + ${name} = new ${switch_value}(${switch_props}(state)); ${name}._fragment.c(); ${this.children.map(child => child.remount(name))} @@ -344,9 +373,8 @@ export default class Component extends Node { if (updates.length) { block.builders.update.addBlock(deindent` else { - var ${name}_changes = {}; - ${updates.join('\n')} - ${name}._set(${name}_changes); + ${updates} + ${name}._set(${name_changes}); ${bindings.length && `${name_updating} = {};`} } `); @@ -356,8 +384,14 @@ export default class Component extends Node { block.builders.destroy.addLine(`if (${name}) ${name}.destroy(false);`); } else { + const expression = this.name === ':Self' + ? generator.name + : `%components-${this.name}`; + block.builders.init.addBlock(deindent` - ${statements.join('\n')} + ${(attributes.length || bindings.length) && deindent` + var ${name_initial_data} = ${attributeObject};`} + ${statements} var ${name} = new ${expression}({ ${componentInitProperties.join(',\n')} }); @@ -387,9 +421,9 @@ export default class Component extends Node { if (updates.length) { block.builders.update.addBlock(deindent` - var ${name}_changes = {}; - ${updates.join('\n')} - ${name}._set(${name}_changes); + var ${name_changes} = {}; + ${updates} + ${name}._set(${name_changes}); ${bindings.length && `${name_updating} = {};`} `); } @@ -408,79 +442,6 @@ export default class Component extends Node { } } -function mungeAttribute(attribute: Node, block: Block): Attribute { - if (attribute.value === true) { - // attributes without values, e.g.