svelte/src/generators/nodes/Component.ts

573 lines
16 KiB

7 years ago
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`];
7 years ago
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));
7 years ago
const eventHandlers = this.attributes
.filter((a: Node) => a.type === 'EventHandler')
.map(a => mungeEventHandler(generator, this, a, block, allContexts));
7 years ago
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;
7 years ago
const builder = new CodeBuilder();
7 years ago
bindings.forEach((binding: Binding) => {
let { name: key } = getObject(binding.value);
7 years ago
binding.contexts.forEach(context => {
allContexts.add(context);
});
let setFromChild;
7 years ago
if (block.contexts.has(key)) {
7 years ago
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)};
7 years ago
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';
7 years ago
if (isStoreProp) hasStoreBindings = true;
else hasLocalBindings = true;
return `${newState}.${prop} = state.${name};`;
})
7 years ago
.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};`;
}
7 years ago
}
statements.push(deindent`
if (${binding.prop} in ${binding.obj}) {
${name_initial_data}.${binding.name} = ${binding.snippet};
${name_updating}.${binding.name} = true;
}`
);
builder.addConditional(
7 years ago
`!${name_updating}.${binding.name} && changed.${binding.name}`,
setFromChild
7 years ago
);
// 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(', ');
7 years ago
componentInitProperties.push(deindent`
_bind: function(changed, childState) {
var ${initialisers};
${builder}
${hasStoreBindings && `#component.store.set(newStoreState);`}
${hasLocalBindings && `#component._set(newState);`}
7 years ago
${name_updating} = {};
}
`);
beforecreate = deindent`
#component.root._beforecreate.push(function() {
${name}._bind({ ${bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get());
7 years ago
});
`;
} 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);
7 years ago
block.builders.init.addBlock(deindent`
var ${switch_vars.value} = ${snippet};
function ${switch_vars.props}(state) {
${statements.length > 0 && statements.join('\n')}
7 years ago
return {
${componentInitProperties.join(',\n')}
};
}
if (${switch_vars.value}) {
var ${name} = new ${expression}(${switch_vars.props}(state));
7 years ago
${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});`
);
}
7 years ago
block.builders.mount.addLine(
`if (${name}) ${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});`
);
const updateMountNode = this.getUpdateMountNode(anchor);
7 years ago
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));
7 years ago
${name}._fragment.c();
${this.children.map(child => remount(generator, child, name))}
${name}._mount(${updateMountNode}, ${anchor});
7 years ago
${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});`
);
}
7 years ago
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 {
7 years ago
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)}`;
7 years ago
} 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>) {
7 years ago
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);`;
}