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 Node from './shared/Node'; import Block from '../dom/Block'; import Attribute from './Attribute'; export default class Component extends Node { type: 'Component'; name: string; attributes: Attribute[]; children: Node[]; init( block: Block, stripWhitespace: boolean, nextSibling: Node ) { this.cannotUseInnerHTML(); this.attributes.forEach((attribute: Node) => { if (attribute.type === 'Attribute' && attribute.value !== true) { attribute.value.forEach((chunk: Node) => { if (chunk.type !== 'Text') { const dependencies = chunk.metadata.dependencies; block.addDependencies(dependencies); } }); } else { if (attribute.type === 'EventHandler' && attribute.expression) { attribute.expression.arguments.forEach((arg: Node) => { block.addDependencies(arg.metadata.dependencies); }); } else if (attribute.type === 'Binding') { block.addDependencies(attribute.metadata.dependencies); } } }); this.var = block.getUniqueName( ( this.name === ':Self' ? this.generator.name : this.name === ':Component' ? 'switch_instance' : this.name ).toLowerCase() ); if (this.children.length) { this._slots = new Set(['default']); this.children.forEach(child => { child.init(block, stripWhitespace, nextSibling); }); } } build( block: Block, parentNode: string, parentNodes: string ) { const { generator } = this; generator.hasComponents = true; const name = this.var; const componentInitProperties = [`root: #component.root`]; if (this.children.length > 0) { const slots = Array.from(this._slots).map(name => `${name}: @createFragment()`); componentInitProperties.push(`slots: { ${slots.join(', ')} }`); this.children.forEach((child: Node) => { child.build(block, `${this.var}._slotted.default`, 'nodes'); }); } const allContexts = new Set(); const statements: string[] = []; let name_updating: string; let name_initial_data: string; let beforecreate: string = null; const attributes = this.attributes .filter(a => a.type === 'Attribute') .map(a => mungeAttribute(a, block)); const bindings = this.attributes .filter(a => a.type === 'Binding') .map(a => mungeBinding(a, block)); const eventHandlers = this.attributes .filter((a: Node) => a.type === 'EventHandler') .map(a => mungeEventHandler(generator, this, a, block, allContexts)); const ref = this.attributes.find((a: Node) => a.type === 'Ref'); if (ref) generator.usesRefs = true; const updates: string[] = []; 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}; `); } 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 (bindings.length) { generator.hasComplexBindings = true; name_updating = block.alias(`${name}_updating`); name_initial_data = block.getUniqueName(`${name}_initial_data`); block.addVariable(name_updating, '{}'); statements.push(`var ${name_initial_data} = ${initialPropString};`); let hasLocalBindings = false; let hasStoreBindings = false; const builder = new CodeBuilder(); bindings.forEach((binding: Binding) => { let { name: key } = getObject(binding.value); binding.contexts.forEach(context => { allContexts.add(context); }); let setFromChild; if (block.contexts.has(key)) { const computed = isComputed(binding.value); const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : ''; setFromChild = deindent` var list = state.${block.listNames.get(key)}; var index = ${block.indexNames.get(key)}; 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};`; }) .join('\n')} `; } 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};`; } } 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; } `); } }); componentInitProperties.push(`data: ${name_initial_data}`); block.addVariable('__state__TODO', 'state'); block.builders.update.addLine(`__state__TODO = state`); const initialisers = [ 'state = __state__TODO', 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} = {}; } `); 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'; const switch_vars = isDynamicComponent && { value: block.getUniqueName('switch_value'), props: block.getUniqueName('switch_props') }; const expression = ( this.name === ':Self' ? generator.name : isDynamicComponent ? switch_vars.value : `%components-${this.name}` ); 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}; function ${switch_vars.props}(state) { ${statements.length > 0 && statements.join('\n')} return { ${componentInitProperties.join(',\n')} }; } if (${switch_vars.value}) { var ${name} = new ${expression}(${switch_vars.props}(state)); ${beforecreate} } ${eventHandlers.map(handler => deindent` function ${handler.var}(event) { ${handler.body} } if (${name}) ${name}.on("${handler.name}", ${handler.var}); `)} `); block.builders.create.addLine( `if (${name}) ${name}._fragment.c();` ); if (parentNodes) { block.builders.claim.addLine( `if (${name}) ${name}._fragment.l(${parentNodes});` ); } block.builders.mount.addLine( `if (${name}) ${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});` ); const updateMountNode = this.getUpdateMountNode(anchor); block.builders.update.addBlock(deindent` if (${switch_vars.value} !== (${switch_vars.value} = ${snippet})) { if (${name}) ${name}.destroy(); if (${switch_vars.value}) { ${name} = new ${switch_vars.value}(${switch_vars.props}(state)); ${name}._fragment.c(); ${this.children.map(child => remount(generator, child, name))} ${name}._mount(${updateMountNode}, ${anchor}); ${eventHandlers.map(handler => deindent` ${name}.on("${handler.name}", ${handler.var}); `)} ${ref && `#component.refs.${ref.name} = ${name};`} } ${ref && deindent` else if (#component.refs.${ref.name} === ${name}) { #component.refs.${ref.name} = null; }`} } `); if (updates.length) { block.builders.update.addBlock(deindent` else { var ${name}_changes = {}; ${updates.join('\n')} ${name}._set(${name}_changes); ${bindings.length && `${name_updating} = {};`} } `); } if (!parentNode) block.builders.unmount.addLine(`if (${name}) ${name}._unmount();`); block.builders.destroy.addLine(`if (${name}) ${name}.destroy(false);`); } else { block.builders.init.addBlock(deindent` ${statements.join('\n')} var ${name} = new ${expression}({ ${componentInitProperties.join(',\n')} }); ${beforecreate} ${eventHandlers.map(handler => deindent` ${name}.on("${handler.name}", function(event) { ${handler.body} }); `)} ${ref && `#component.refs.${ref.name} = ${name};`} `); block.builders.create.addLine(`${name}._fragment.c();`); if (parentNodes) { block.builders.claim.addLine( `${name}._fragment.l(${parentNodes});` ); } block.builders.mount.addLine( `${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});` ); if (updates.length) { block.builders.update.addBlock(deindent` var ${name}_changes = {}; ${updates.join('\n')} ${name}._set(${name}_changes); ${bindings.length && `${name_updating} = {};`} `); } if (!parentNode) block.builders.unmount.addLine(`${name}._unmount();`); block.builders.destroy.addLine(deindent` ${name}.destroy(false); ${ref && `if (#component.refs.${ref.name} === ${name}) #component.refs.${ref.name} = null;`} `); } } } function mungeAttribute(attribute: Node, block: Block): Attribute { if (attribute.value === true) { // attributes without values, e.g. <textarea readonly> return { name: attribute.name, value: true, dynamic: false }; } if (attribute.value.length === 0) { return { name: attribute.name, value: `''`, dynamic: false }; } if (attribute.value.length === 1) { const value = attribute.value[0]; if (value.type === 'Text') { // static attributes return { name: attribute.name, value: isNaN(value.data) ? stringify(value.data) : value.data, dynamic: false }; } // simple dynamic attributes block.contextualise(value.expression); // TODO remove const { dependencies, snippet } = value.metadata; // TODO only update attributes that have changed return { name: attribute.name, value: snippet, dependencies, dynamic: true }; } // otherwise we're dealing with a complex dynamic attribute const allDependencies = new Set(); const value = (attribute.value[0].type === 'Text' ? '' : `"" + `) + attribute.value .map((chunk: Node) => { if (chunk.type === 'Text') { return stringify(chunk.data); } else { block.contextualise(chunk.expression); // TODO remove const { dependencies, snippet } = chunk.metadata; dependencies.forEach((dependency: string) => { allDependencies.add(dependency); }); return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet; } }) .join(' + '); return { name: attribute.name, value, dependencies: Array.from(allDependencies), dynamic: true }; } function mungeBinding(binding: Node, block: Block): Binding { const { name } = getObject(binding.value); const { contexts } = block.contextualise(binding.value); const { dependencies, snippet } = binding.metadata; const contextual = block.contexts.has(name); let obj; let prop; if (contextual) { obj = `state.${block.listNames.get(name)}`; prop = `${block.indexNames.get(name)}`; } else if (binding.value.type === 'MemberExpression') { prop = `[✂${binding.value.property.start}-${binding.value.property.end}✂]`; if (!binding.value.computed) prop = `'${prop}'`; obj = `[✂${binding.value.object.start}-${binding.value.object.end}✂]`; } else { obj = 'state'; prop = `'${name}'`; } return { name: binding.name, value: binding.value, contexts, snippet, obj, prop, dependencies }; } function mungeEventHandler(generator: DomGenerator, node: Node, handler: Node, block: Block, allContexts: Set<string>) { let body; if (handler.expression) { generator.addSourcemapLocations(handler.expression); generator.code.prependRight( handler.expression.start, `${block.alias('component')}.` ); handler.expression.arguments.forEach((arg: Node) => { const { contexts } = block.contextualise(arg, null, true); contexts.forEach(context => { allContexts.add(context); }); }); body = deindent` [✂${handler.expression.start}-${handler.expression.end}✂]; `; } else { body = deindent` ${block.alias('component')}.fire('${handler.name}', event); `; } return { name: handler.name, var: block.getUniqueName(`${node.var}_${handler.name}`), body }; } function isComputed(node: Node) { while (node.type === 'MemberExpression') { if (node.computed) return true; node = node.object; } return false; } function remount(generator: DomGenerator, node: Node, name: string) { // TODO make this a method of the nodes if (node.type === 'Component') { return `${node.var}._mount(${name}._slotted.default, null);`; } if (node.type === 'Element') { const slot = node.attributes.find(attribute => attribute.name === 'slot'); if (slot) { return `@appendNode(${node.var}, ${name}._slotted.${node.getStaticAttributeValue('slot')});`; } return `@appendNode(${node.var}, ${name}._slotted.default);`; } if (node.type === 'Text' || node.type === 'MustacheTag' || node.type === 'RawMustacheTag') { return `@appendNode(${node.var}, ${name}._slotted.default);`; } if (node.type === 'EachBlock') { // TODO consider keyed blocks return `for (var #i = 0; #i < ${node.iterations}.length; #i += 1) ${node.iterations}[#i].m(${name}._slotted.default, null);`; } return `${node.var}.m(${name}._slotted.default, null);`; }