|
|
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);`;
|
|
|
} |