move some code around

pull/992/head
Rich Harris 8 years ago
parent 8e9e323b13
commit c6d49b7295

@ -12,6 +12,7 @@ export default function visit(
elementStack: Node[], elementStack: Node[],
componentStack: Node[] componentStack: Node[]
) { ) {
throw new Error('do not use visit')
const visitor = visitors[node.type]; const visitor = visitors[node.type];
visitor(generator, block, state, node, elementStack, componentStack); visitor(generator, block, state, node, elementStack, componentStack);
} }

@ -1,159 +0,0 @@
import deindent from '../../../utils/deindent';
import visit from '../visit';
import { DomGenerator } from '../index';
import Block from '../Block';
import isDomNode from './shared/isDomNode';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
export default function visitAwaitBlock(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
elementStack: Node[],
componentStack: Node[]
) {
const name = node.var;
const needsAnchor = node.next ? !isDomNode(node.next, generator) : !state.parentNode || !isDomNode(node.parent, generator);
const anchor = needsAnchor
? block.getUniqueName(`${name}_anchor`)
: (node.next && node.next.var) || 'null';
const params = block.params.join(', ');
block.contextualise(node.expression);
const { snippet } = node.metadata;
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
`@createComment()`,
state.parentNode
);
}
const promise = block.getUniqueName(`promise`);
const resolved = block.getUniqueName(`resolved`);
const await_block = block.getUniqueName(`await_block`);
const await_block_type = block.getUniqueName(`await_block_type`);
const token = block.getUniqueName(`token`);
const await_token = block.getUniqueName(`await_token`);
const handle_promise = block.getUniqueName(`handle_promise`);
const replace_await_block = block.getUniqueName(`replace_await_block`);
const old_block = block.getUniqueName(`old_block`);
const value = block.getUniqueName(`value`);
const error = block.getUniqueName(`error`);
const create_pending_block = node.pending._block.name;
const create_then_block = node.then._block.name;
const create_catch_block = node.catch._block.name;
block.addVariable(await_block);
block.addVariable(await_block_type);
block.addVariable(await_token);
block.addVariable(promise);
block.addVariable(resolved);
block.builders.init.addBlock(deindent`
function ${replace_await_block}(${token}, type, ${value}, ${params}) {
if (${token} !== ${await_token}) return;
var ${old_block} = ${await_block};
${await_block} = (${await_block_type} = type)(${params}, ${resolved} = ${value}, #component);
if (${old_block}) {
${old_block}.u();
${old_block}.d();
${await_block}.c();
${await_block}.m(${state.parentNode || `${anchor}.parentNode`}, ${anchor});
}
}
function ${handle_promise}(${promise}, ${params}) {
var ${token} = ${await_token} = {};
if (@isPromise(${promise})) {
${promise}.then(function(${value}) {
${replace_await_block}(${token}, ${create_then_block}, ${value}, ${params});
}, function (${error}) {
${replace_await_block}(${token}, ${create_catch_block}, ${error}, ${params});
});
// if we previously had a then/catch block, destroy it
if (${await_block_type} !== ${create_pending_block}) {
${replace_await_block}(${token}, ${create_pending_block}, null, ${params});
return true;
}
} else {
${resolved} = ${promise};
if (${await_block_type} !== ${create_then_block}) {
${replace_await_block}(${token}, ${create_then_block}, ${resolved}, ${params});
return true;
}
}
}
${handle_promise}(${promise} = ${snippet}, ${params});
`);
block.builders.create.addBlock(deindent`
${await_block}.c();
`);
block.builders.claim.addBlock(deindent`
${await_block}.l(${state.parentNodes});
`);
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.mount.addBlock(deindent`
${await_block}.m(${targetNode}, ${anchorNode});
`);
const conditions = [];
if (node.metadata.dependencies) {
conditions.push(
`(${node.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})`
);
}
conditions.push(
`${promise} !== (${promise} = ${snippet})`,
`${handle_promise}(${promise}, ${params})`
);
if (node.pending._block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (${conditions.join(' && ')}) {
// nothing
} else {
${await_block}.p(changed, ${params}, ${resolved});
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${conditions.join(' && ')}) {
${await_block}.c();
${await_block}.m(${anchor}.parentNode, ${anchor});
}
`);
}
block.builders.unmount.addBlock(deindent`
${await_block}.u();
`);
block.builders.destroy.addBlock(deindent`
${await_token} = null;
${await_block}.d();
`);
[node.pending, node.then, node.catch].forEach(status => {
status.children.forEach(child => {
visit(generator, status._block, status._state, child, elementStack, componentStack);
});
});
}

@ -1,588 +0,0 @@
import deindent from '../../../utils/deindent';
import CodeBuilder from '../../../utils/CodeBuilder';
import visit from '../visit';
import { DomGenerator } from '../index';
import Block from '../Block';
import isDomNode from './shared/isDomNode';
import getTailSnippet from '../../../utils/getTailSnippet';
import getObject from '../../../utils/getObject';
import getExpressionPrecedence from '../../../utils/getExpressionPrecedence';
import getStaticAttributeValue from '../../../utils/getStaticAttributeValue';
import { stringify } from '../../../utils/stringify';
import stringifyProps from '../../../utils/stringifyProps';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
interface Attribute {
name: string;
value: any;
dynamic: boolean;
dependencies?: string[]
}
interface Binding {
name: string;
value: Node;
contexts: Set<string>;
snippet: string;
obj: string;
prop: string;
dependencies: string[];
}
export default function visitComponent(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
elementStack: Node[],
componentStack: Node[]
) {
generator.hasComponents = true;
const name = node.var;
const componentInitProperties = [`_root: #component._root`];
if (node.children.length > 0) {
const slots = Array.from(node._slots).map(name => `${name}: @createFragment()`);
componentInitProperties.push(`slots: { ${slots.join(', ')} }`);
node.children.forEach((child: Node) => {
visit(generator, block, node._state, child, elementStack, componentStack.concat(node));
});
}
const allContexts = new Set();
const statements: string[] = [];
const name_context = block.getUniqueName(`${name}_context`);
let name_updating: string;
let name_initial_data: string;
let beforecreate: string = null;
const attributes = node.attributes
.filter(a => a.type === 'Attribute')
.map(a => mungeAttribute(a, block));
const bindings = node.attributes
.filter(a => a.type === 'Binding')
.map(a => mungeBinding(a, block));
const eventHandlers = node.attributes
.filter((a: Node) => a.type === 'EventHandler')
.map(a => mungeEventHandler(generator, node, a, block, name_context, allContexts));
const ref = node.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};`);
const setParentFromChildOnChange = new CodeBuilder();
const setParentFromChildOnInit = new CodeBuilder();
bindings.forEach((binding: Binding) => {
let setParentFromChild;
binding.contexts.forEach(context => {
allContexts.add(context);
});
const { name: key } = getObject(binding.value);
if (block.contexts.has(key)) {
const prop = binding.dependencies[0];
const computed = isComputed(binding.value);
const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : '';
setParentFromChild = deindent`
var list = ${name_context}.${block.listNames.get(key)};
var index = ${name_context}.${block.indexNames.get(key)};
list[index]${tail} = childState.${binding.name};
${binding.dependencies
.map((prop: string) => `newState.${prop} = state.${prop};`)
.join('\n')}
`;
}
else if (binding.value.type === 'MemberExpression') {
setParentFromChild = deindent`
${binding.snippet} = childState.${binding.name};
${binding.dependencies.map((prop: string) => `newState.${prop} = state.${prop};`).join('\n')}
`;
}
else {
setParentFromChild = `newState.${binding.value.name} = childState.${binding.name};`;
}
statements.push(deindent`
if (${binding.prop} in ${binding.obj}) {
${name_initial_data}.${binding.name} = ${binding.snippet};
${name_updating}.${binding.name} = true;
}`
);
setParentFromChildOnChange.addConditional(
`!${name_updating}.${binding.name} && changed.${binding.name}`,
setParentFromChild
);
setParentFromChildOnInit.addConditional(
`!${name_updating}.${binding.name}`,
setParentFromChild
);
// 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}`);
componentInitProperties.push(deindent`
_bind: function(changed, childState) {
var state = #component.get(), newState = {};
${setParentFromChildOnChange}
${name_updating} = @assign({}, changed);
#component._set(newState);
${name_updating} = {};
}
`);
beforecreate = deindent`
#component._root._beforecreate.push(function() {
var state = #component.get(), childState = ${name}.get(), newState = {};
if (!childState) return;
${setParentFromChildOnInit}
${name_updating} = { ${bindings.map((binding: Binding) => `${binding.name}: true`).join(', ')} };
#component._set(newState);
${name_updating} = {};
});
`;
} else if (initialProps.length) {
componentInitProperties.push(`data: ${initialPropString}`);
}
}
const isDynamicComponent = node.name === ':Component';
const switch_vars = isDynamicComponent && {
value: block.getUniqueName('switch_value'),
props: block.getUniqueName('switch_props')
};
const expression = (
node.name === ':Self' ? generator.name :
isDynamicComponent ? switch_vars.value :
`%components-${node.name}`
);
if (isDynamicComponent) {
block.contextualise(node.expression);
const { dependencies, snippet } = node.metadata;
const needsAnchor = node.next ? !isDomNode(node.next, generator) : !state.parentNode || !isDomNode(node.parent, generator);
const anchor = needsAnchor
? block.getUniqueName(`${name}_anchor`)
: (node.next && node.next.var) || 'null';
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
`@createComment()`,
state.parentNode
);
}
const params = block.params.join(', ');
block.builders.init.addBlock(deindent`
var ${switch_vars.value} = ${snippet};
function ${switch_vars.props}(${params}) {
return {
${componentInitProperties.join(',\n')}
};
}
if (${switch_vars.value}) {
${statements.length > 0 && statements.join('\n')}
var ${name} = new ${expression}(${switch_vars.props}(${params}));
${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();`
);
block.builders.claim.addLine(
`if (${name}) ${name}._fragment.l(${state.parentNodes});`
);
block.builders.mount.addLine(
`if (${name}) ${name}._mount(${state.parentNode || '#target'}, ${state.parentNode ? 'null' : '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}(${params}));
${name}._fragment.c();
${node.children.map(child => remount(generator, child, name))}
${name}._mount(${anchor}.parentNode, ${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 (!state.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();`);
block.builders.claim.addLine(
`${name}._fragment.l(${state.parentNodes});`
);
block.builders.mount.addLine(
`${name}._mount(${state.parentNode || '#target'}, ${state.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 (!state.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;`}
`);
}
// maintain component context
if (allContexts.size) {
const contexts = Array.from(allContexts);
const initialProps = contexts
.map(contextName => {
if (contextName === 'state') return `state: state`;
const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName);
return `${listName}: ${listName},\n${indexName}: ${indexName}`;
})
.join(',\n');
const updates = contexts
.map(contextName => {
if (contextName === 'state') return `${name_context}.state = state;`;
const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName);
return `${name_context}.${listName} = ${listName};\n${name_context}.${indexName} = ${indexName};`;
})
.join('\n');
block.builders.init.addBlock(deindent`
var ${name_context} = {
${initialProps}
};
`);
block.builders.update.addBlock(updates);
}
}
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 = 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, name_context: string, allContexts: Set<string>) {
let body;
if (handler.expression) {
generator.addSourcemapLocations(handler.expression);
generator.code.prependRight(
handler.expression.start,
`${block.alias('component')}.`
);
const usedContexts: string[] = [];
handler.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, null, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
allContexts.add(context);
});
});
// TODO hoist event handlers? can do `this.__component.method(...)`
const declarations = usedContexts.map(name => {
if (name === 'state') return `var state = ${name_context}.state;`;
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
return `var ${listName} = ${name_context}.${listName}, ${indexName} = ${name_context}.${indexName}, ${name} = ${listName}[${indexName}]`;
});
body = deindent`
${declarations}
[${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.${getStaticAttributeValue(node, '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);`;
}

@ -1,491 +0,0 @@
import deindent from '../../../utils/deindent';
import visit from '../visit';
import { DomGenerator } from '../index';
import Block from '../Block';
import isDomNode from './shared/isDomNode';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
export default function visitEachBlock(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
elementStack: Node[],
componentStack: Node[]
) {
const each = node.var;
const create_each_block = node._block.name;
const each_block_value = node._block.listName;
const iterations = node.iterations;
const params = block.params.join(', ');
const needsAnchor = node.next ? !isDomNode(node.next, generator) : !state.parentNode || !isDomNode(node.parent, generator);
const anchor = needsAnchor
? block.getUniqueName(`${each}_anchor`)
: (node.next && node.next.var) || 'null';
// hack the sourcemap, so that if data is missing the bug
// is easy to find
let c = node.start + 3;
while (generator.source[c] !== 'e') c += 1;
generator.code.overwrite(c, c + 4, 'length');
const length = `[✂${c}-${c+4}✂]`;
const mountOrIntro = node._block.hasIntroMethod ? 'i' : 'm';
const vars = {
each,
create_each_block,
each_block_value,
length,
iterations,
params,
anchor,
mountOrIntro,
};
block.contextualise(node.expression);
const { snippet } = node.metadata;
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
if (node.key) {
keyed(generator, block, state, node, snippet, vars);
} else {
unkeyed(generator, block, state, node, snippet, vars);
}
const isToplevel = !state.parentNode;
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
`@createComment()`,
state.parentNode
);
}
if (node.else) {
const each_block_else = generator.getUniqueName(`${each}_else`);
block.builders.init.addLine(`var ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block
block.builders.init.addBlock(deindent`
if (!${each_block_value}.${length}) {
${each_block_else} = ${node.else._block.name}(${params}, #component);
${each_block_else}.c();
}
`);
block.builders.mount.addBlock(deindent`
if (${each_block_else}) {
${each_block_else}.${mountOrIntro}(${state.parentNode || '#target'}, null);
}
`);
const parentNode = state.parentNode || `${anchor}.parentNode`;
if (node.else._block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (!${each_block_value}.${length} && ${each_block_else}) {
${each_block_else}.p( changed, ${params} );
} else if (!${each_block_value}.${length}) {
${each_block_else} = ${node.else._block.name}(${params}, #component);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${parentNode}, ${anchor});
} else if (${each_block_else}) {
${each_block_else}.u();
${each_block_else}.d();
${each_block_else} = null;
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${each_block_value}.${length}) {
if (${each_block_else}) {
${each_block_else}.u();
${each_block_else}.d();
${each_block_else} = null;
}
} else if (!${each_block_else}) {
${each_block_else} = ${node.else._block.name}(${params}, #component);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${parentNode}, ${anchor});
}
`);
}
block.builders.unmount.addLine(
`if (${each_block_else}) ${each_block_else}.u()`
);
block.builders.destroy.addBlock(deindent`
if (${each_block_else}) ${each_block_else}.d();
`);
}
node.children.forEach((child: Node) => {
visit(generator, node._block, node._state, child, elementStack, componentStack);
});
if (node.else) {
node.else.children.forEach((child: Node) => {
visit(generator, node.else._block, node.else._state, child, elementStack, componentStack);
});
}
}
function keyed(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
snippet: string,
{
each,
create_each_block,
each_block_value,
length,
params,
anchor,
mountOrIntro,
}
) {
const key = block.getUniqueName('key');
const lookup = block.getUniqueName(`${each}_lookup`);
const iteration = block.getUniqueName(`${each}_iteration`);
const head = block.getUniqueName(`${each}_head`);
const last = block.getUniqueName(`${each}_last`);
const expected = block.getUniqueName(`${each}_expected`);
block.addVariable(lookup, `@blankObject()`);
block.addVariable(head);
block.addVariable(last);
if (node.children[0] && node.children[0].type === 'Element' && !generator.components.has(node.children[0].name)) {
// TODO or text/tag/raw
node._block.first = node.children[0]._state.parentNode; // TODO this is highly confusing
} else {
node._block.first = node._block.getUniqueName('first');
node._block.addElement(
node._block.first,
`@createComment()`,
`@createComment()`,
null
);
}
block.builders.init.addBlock(deindent`
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
var ${key} = ${each_block_value}[#i].${node.key};
var ${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
if (${last}) ${last}.next = ${iteration};
${iteration}.last = ${last};
${last} = ${iteration};
if (#i === 0) ${head} = ${iteration};
}
`);
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.create.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.c();
${iteration} = ${iteration}.next;
}
`);
block.builders.claim.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.l(${state.parentNodes});
${iteration} = ${iteration}.next;
}
`);
block.builders.mount.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.${mountOrIntro}(${targetNode}, ${anchorNode});
${iteration} = ${iteration}.next;
}
`);
const dynamic = node._block.hasUpdateMethod;
const parentNode = isDomNode(node.parent, generator) ? node.parent.var : `${anchor}.parentNode`;
let destroy;
if (node._block.hasOutroMethod) {
const fn = block.getUniqueName(`${each}_outro`);
block.builders.init.addBlock(deindent`
function ${fn}(iteration) {
iteration.o(function() {
iteration.u();
iteration.d();
${lookup}[iteration.key] = null;
});
}
`);
destroy = deindent`
while (${expected}) {
${fn}(${expected});
${expected} = ${expected}.next;
}
for (#i = 0; #i < discard_pile.length; #i += 1) {
if (discard_pile[#i].discard) {
${fn}(discard_pile[#i]);
}
}
`;
} else {
const fn = block.getUniqueName(`${each}_destroy`);
block.builders.init.addBlock(deindent`
function ${fn}(iteration) {
iteration.u();
iteration.d();
${lookup}[iteration.key] = null;
}
`);
destroy = deindent`
while (${expected}) {
${fn}(${expected});
${expected} = ${expected}.next;
}
for (#i = 0; #i < discard_pile.length; #i += 1) {
var ${iteration} = discard_pile[#i];
if (${iteration}.discard) {
${fn}(${iteration});
}
}
`;
}
block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet};
var ${expected} = ${head};
var ${last} = null;
var discard_pile = [];
for (#i = 0; #i < ${each_block_value}.${length}; #i += 1) {
var ${key} = ${each_block_value}[#i].${node.key};
var ${iteration} = ${lookup}[${key}];
${dynamic &&
`if (${iteration}) ${iteration}.p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);`}
if (${expected}) {
if (${key} === ${expected}.key) {
${expected} = ${expected}.next;
} else {
if (${iteration}) {
// probably a deletion
while (${expected} && ${expected}.key !== ${key}) {
${expected}.discard = true;
discard_pile.push(${expected});
${expected} = ${expected}.next;
};
${expected} = ${expected} && ${expected}.next;
${iteration}.discard = false;
${iteration}.last = ${last};
if (!${expected}) ${iteration}.m(${parentNode}, ${anchor});
} else {
// key is being inserted
${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
${iteration}.c();
${iteration}.${mountOrIntro}(${parentNode}, ${expected}.first);
${expected}.last = ${iteration};
${iteration}.next = ${expected};
}
}
} else {
// we're appending from this point forward
if (${iteration}) {
${iteration}.discard = false;
${iteration}.next = null;
${iteration}.m(${parentNode}, ${anchor});
} else {
${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
${iteration}.c();
${iteration}.${mountOrIntro}(${parentNode}, ${anchor});
}
}
if (${last}) ${last}.next = ${iteration};
${iteration}.last = ${last};
${node._block.hasIntroMethod && `${iteration}.i(${parentNode}, ${anchor});`}
${last} = ${iteration};
}
if (${last}) ${last}.next = null;
${destroy}
${head} = ${lookup}[${each_block_value}[0] && ${each_block_value}[0].${node.key}];
`);
if (!state.parentNode) {
block.builders.unmount.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.u();
${iteration} = ${iteration}.next;
}
`);
}
block.builders.destroy.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.d();
${iteration} = ${iteration}.next;
}
`);
}
function unkeyed(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
snippet: string,
{
create_each_block,
each_block_value,
length,
iterations,
params,
anchor,
mountOrIntro,
}
) {
block.builders.init.addBlock(deindent`
var ${iterations} = [];
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
}
`);
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.create.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].c();
}
`);
block.builders.claim.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].l(${state.parentNodes});
}
`);
block.builders.mount.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].${mountOrIntro}(${targetNode}, ${anchorNode});
}
`);
const allDependencies = new Set(node._block.dependencies);
const { dependencies } = node.metadata;
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
// TODO do this for keyed blocks as well
const condition = Array.from(allDependencies)
.map(dependency => `changed.${dependency}`)
.join(' || ');
const parentNode = isDomNode(node.parent, generator) ? node.parent.var : `${anchor}.parentNode`;
if (condition !== '') {
const forLoopBody = node._block.hasUpdateMethod
? node._block.hasIntroMethod
? deindent`
if (${iterations}[#i]) {
${iterations}[#i].p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);
} else {
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
${iterations}[#i].c();
}
${iterations}[#i].i(${parentNode}, ${anchor});
`
: deindent`
if (${iterations}[#i]) {
${iterations}[#i].p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);
} else {
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
${iterations}[#i].c();
${iterations}[#i].m(${parentNode}, ${anchor});
}
`
: deindent`
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
${iterations}[#i].c();
${iterations}[#i].${mountOrIntro}(${parentNode}, ${anchor});
`;
const start = node._block.hasUpdateMethod ? '0' : `${iterations}.length`;
const outro = block.getUniqueName('outro');
const destroy = node._block.hasOutroMethod
? deindent`
function ${outro}(i) {
if (${iterations}[i]) {
${iterations}[i].o(function() {
${iterations}[i].u();
${iterations}[i].d();
${iterations}[i] = null;
});
}
}
for (; #i < ${iterations}.length; #i += 1) ${outro}(#i);
`
: deindent`
for (; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].u();
${iterations}[#i].d();
}
${iterations}.length = ${each_block_value}.${length};
`;
block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet};
if (${condition}) {
for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) {
${forLoopBody}
}
${destroy}
}
`);
}
block.builders.unmount.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].u();
}
`);
block.builders.destroy.addBlock(`@destroyEach(${iterations});`);
}

@ -41,7 +41,7 @@ export default function visitSlot(
block.builders.destroy.pushCondition(`!${content_name}`); block.builders.destroy.pushCondition(`!${content_name}`);
node.children.forEach((child: Node) => { node.children.forEach((child: Node) => {
visit(generator, block, state, child, elementStack, componentStack); child.build(block, state, elementStack, componentStack);
}); });
block.builders.create.popCondition(); block.builders.create.popCondition();

@ -1,21 +0,0 @@
import { DomGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
import { stringify } from '../../../utils/stringify';
export default function visitText(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
if (node.shouldSkip) return;
block.addElement(
node.var,
`@createText(${stringify(node.data)})`,
`@claimText(${state.parentNodes}, ${stringify(node.data)})`,
state.parentNode
);
}

@ -1,22 +1,14 @@
import AwaitBlock from './AwaitBlock';
import Component from './Component';
import EachBlock from './EachBlock';
import Element from './Element/Element'; import Element from './Element/Element';
import IfBlock from './IfBlock'; import IfBlock from './IfBlock';
import MustacheTag from './MustacheTag'; import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag'; import RawMustacheTag from './RawMustacheTag';
import Text from './Text';
import { Visitor } from '../interfaces'; import { Visitor } from '../interfaces';
const visitors: Record<string, Visitor> = { const visitors: Record<string, Visitor> = {
AwaitBlock,
Component,
EachBlock,
Element, Element,
IfBlock, IfBlock,
MustacheTag, MustacheTag,
RawMustacheTag, RawMustacheTag
Text
}; };
export default visitors; export default visitors;

@ -1,3 +1,4 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node'; import Node from './shared/Node';
import { DomGenerator } from '../dom/index'; import { DomGenerator } from '../dom/index';
import Block from '../dom/Block'; import Block from '../dom/Block';
@ -73,6 +74,147 @@ export default class AwaitBlock extends Node {
elementStack: Node[], elementStack: Node[],
componentStack: Node[] componentStack: Node[]
) { ) {
visitAwaitBlock(this.generator, block, state, this, elementStack, componentStack); const name = this.var;
const needsAnchor = this.next ? !this.next.isDomNode() : !state.parentNode || !this.parent.isDomNode();
const anchor = needsAnchor
? block.getUniqueName(`${name}_anchor`)
: (this.next && this.next.var) || 'null';
const params = block.params.join(', ');
block.contextualise(this.expression);
const { snippet } = this.metadata;
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
`@createComment()`,
state.parentNode
);
}
const promise = block.getUniqueName(`promise`);
const resolved = block.getUniqueName(`resolved`);
const await_block = block.getUniqueName(`await_block`);
const await_block_type = block.getUniqueName(`await_block_type`);
const token = block.getUniqueName(`token`);
const await_token = block.getUniqueName(`await_token`);
const handle_promise = block.getUniqueName(`handle_promise`);
const replace_await_block = block.getUniqueName(`replace_await_block`);
const old_block = block.getUniqueName(`old_block`);
const value = block.getUniqueName(`value`);
const error = block.getUniqueName(`error`);
const create_pending_block = this.pending._block.name;
const create_then_block = this.then._block.name;
const create_catch_block = this.catch._block.name;
block.addVariable(await_block);
block.addVariable(await_block_type);
block.addVariable(await_token);
block.addVariable(promise);
block.addVariable(resolved);
block.builders.init.addBlock(deindent`
function ${replace_await_block}(${token}, type, ${value}, ${params}) {
if (${token} !== ${await_token}) return;
var ${old_block} = ${await_block};
${await_block} = (${await_block_type} = type)(${params}, ${resolved} = ${value}, #component);
if (${old_block}) {
${old_block}.u();
${old_block}.d();
${await_block}.c();
${await_block}.m(${state.parentNode || `${anchor}.parentNode`}, ${anchor});
}
}
function ${handle_promise}(${promise}, ${params}) {
var ${token} = ${await_token} = {};
if (@isPromise(${promise})) {
${promise}.then(function(${value}) {
${replace_await_block}(${token}, ${create_then_block}, ${value}, ${params});
}, function (${error}) {
${replace_await_block}(${token}, ${create_catch_block}, ${error}, ${params});
});
// if we previously had a then/catch block, destroy it
if (${await_block_type} !== ${create_pending_block}) {
${replace_await_block}(${token}, ${create_pending_block}, null, ${params});
return true;
}
} else {
${resolved} = ${promise};
if (${await_block_type} !== ${create_then_block}) {
${replace_await_block}(${token}, ${create_then_block}, ${resolved}, ${params});
return true;
}
}
}
${handle_promise}(${promise} = ${snippet}, ${params});
`);
block.builders.create.addBlock(deindent`
${await_block}.c();
`);
block.builders.claim.addBlock(deindent`
${await_block}.l(${state.parentNodes});
`);
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.mount.addBlock(deindent`
${await_block}.m(${targetNode}, ${anchorNode});
`);
const conditions = [];
if (this.metadata.dependencies) {
conditions.push(
`(${this.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})`
);
}
conditions.push(
`${promise} !== (${promise} = ${snippet})`,
`${handle_promise}(${promise}, ${params})`
);
if (this.pending._block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (${conditions.join(' && ')}) {
// nothing
} else {
${await_block}.p(changed, ${params}, ${resolved});
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${conditions.join(' && ')}) {
${await_block}.c();
${await_block}.m(${anchor}.parentNode, ${anchor});
}
`);
}
block.builders.unmount.addBlock(deindent`
${await_block}.u();
`);
block.builders.destroy.addBlock(deindent`
${await_token} = null;
${await_block}.d();
`);
[this.pending, this.then, this.catch].forEach(status => {
status.children.forEach(child => {
child.build(status._block, status._state, elementStack, componentStack);
});
});
} }
} }

@ -1,6 +1,8 @@
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import State from '../dom/State';
export default class CatchBlock extends Node { export default class CatchBlock extends Node {
_block: Block; _block: Block;
_state: State;
} }

@ -1,12 +1,20 @@
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 Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import State from '../dom/State'; import State from '../dom/State';
import Attribute from './Attribute';
import visitComponent from '../dom/visitors/Component'; import visitComponent from '../dom/visitors/Component';
export default class Component extends Node { export default class Component extends Node {
type: 'Component'; // TODO fix this? type: 'Component'; // TODO fix this?
name: string; name: string;
attributes: Node[]; // TODO have more specific Attribute type attributes: Attribute[];
children: Node[]; children: Node[];
init( init(
@ -66,6 +74,553 @@ export default class Component extends Node {
elementStack: Node[], elementStack: Node[],
componentStack: Node[] componentStack: Node[]
) { ) {
visitComponent(this.generator, block, state, this, elementStack, componentStack); 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._state, elementStack, componentStack.concat(this));
});
}
const allContexts = new Set();
const statements: string[] = [];
const name_context = block.getUniqueName(`${name}_context`);
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, name_context, 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};`);
const setParentFromChildOnChange = new CodeBuilder();
const setParentFromChildOnInit = new CodeBuilder();
bindings.forEach((binding: Binding) => {
let setParentFromChild;
binding.contexts.forEach(context => {
allContexts.add(context);
});
const { name: key } = getObject(binding.value);
if (block.contexts.has(key)) {
const prop = binding.dependencies[0];
const computed = isComputed(binding.value);
const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : '';
setParentFromChild = deindent`
var list = ${name_context}.${block.listNames.get(key)};
var index = ${name_context}.${block.indexNames.get(key)};
list[index]${tail} = childState.${binding.name};
${binding.dependencies
.map((prop: string) => `newState.${prop} = state.${prop};`)
.join('\n')}
`;
}
else if (binding.value.type === 'MemberExpression') {
setParentFromChild = deindent`
${binding.snippet} = childState.${binding.name};
${binding.dependencies.map((prop: string) => `newState.${prop} = state.${prop};`).join('\n')}
`;
}
else {
setParentFromChild = `newState.${binding.value.name} = childState.${binding.name};`;
}
statements.push(deindent`
if (${binding.prop} in ${binding.obj}) {
${name_initial_data}.${binding.name} = ${binding.snippet};
${name_updating}.${binding.name} = true;
}`
);
setParentFromChildOnChange.addConditional(
`!${name_updating}.${binding.name} && changed.${binding.name}`,
setParentFromChild
);
setParentFromChildOnInit.addConditional(
`!${name_updating}.${binding.name}`,
setParentFromChild
);
// 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}`);
componentInitProperties.push(deindent`
_bind: function(changed, childState) {
var state = #component.get(), newState = {};
${setParentFromChildOnChange}
${name_updating} = @assign({}, changed);
#component._set(newState);
${name_updating} = {};
}
`);
beforecreate = deindent`
#component._root._beforecreate.push(function() {
var state = #component.get(), childState = ${name}.get(), newState = {};
if (!childState) return;
${setParentFromChildOnInit}
${name_updating} = { ${bindings.map((binding: Binding) => `${binding.name}: true`).join(', ')} };
#component._set(newState);
${name_updating} = {};
});
`;
} 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 needsAnchor = this.next ? !this.next.isDomNode() : !state.parentNode || !this.parent.isDomNode();
const anchor = needsAnchor
? block.getUniqueName(`${name}_anchor`)
: (this.next && this.next.var) || 'null';
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
`@createComment()`,
state.parentNode
);
}
const params = block.params.join(', ');
block.builders.init.addBlock(deindent`
var ${switch_vars.value} = ${snippet};
function ${switch_vars.props}(${params}) {
return {
${componentInitProperties.join(',\n')}
};
}
if (${switch_vars.value}) {
${statements.length > 0 && statements.join('\n')}
var ${name} = new ${expression}(${switch_vars.props}(${params}));
${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();`
);
block.builders.claim.addLine(
`if (${name}) ${name}._fragment.l(${state.parentNodes});`
);
block.builders.mount.addLine(
`if (${name}) ${name}._mount(${state.parentNode || '#target'}, ${state.parentNode ? 'null' : '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}(${params}));
${name}._fragment.c();
${this.children.map(child => remount(generator, child, name))}
${name}._mount(${anchor}.parentNode, ${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 (!state.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();`);
block.builders.claim.addLine(
`${name}._fragment.l(${state.parentNodes});`
);
block.builders.mount.addLine(
`${name}._mount(${state.parentNode || '#target'}, ${state.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 (!state.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;`}
`);
}
// maintain component context
if (allContexts.size) {
const contexts = Array.from(allContexts);
const initialProps = contexts
.map(contextName => {
if (contextName === 'state') return `state: state`;
const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName);
return `${listName}: ${listName},\n${indexName}: ${indexName}`;
})
.join(',\n');
const updates = contexts
.map(contextName => {
if (contextName === 'state') return `${name_context}.state = state;`;
const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName);
return `${name_context}.${listName} = ${listName};\n${name_context}.${indexName} = ${indexName};`;
})
.join('\n');
block.builders.init.addBlock(deindent`
var ${name_context} = {
${initialProps}
};
`);
block.builders.update.addBlock(updates);
}
}
}
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 = 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, name_context: string, allContexts: Set<string>) {
let body;
if (handler.expression) {
generator.addSourcemapLocations(handler.expression);
generator.code.prependRight(
handler.expression.start,
`${block.alias('component')}.`
);
const usedContexts: string[] = [];
handler.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, null, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
allContexts.add(context);
});
});
// TODO hoist event handlers? can do `this.__component.method(...)`
const declarations = usedContexts.map(name => {
if (name === 'state') return `var state = ${name_context}.state;`;
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
return `var ${listName} = ${name_context}.${listName}, ${indexName} = ${name_context}.${indexName}, ${name} = ${listName}[${indexName}]`;
});
body = deindent`
${declarations}
[${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);`;
} }

@ -1,3 +1,4 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node'; import Node from './shared/Node';
import ElseBlock from './ElseBlock'; import ElseBlock from './ElseBlock';
import { DomGenerator } from '../dom/index'; import { DomGenerator } from '../dom/index';
@ -17,6 +18,7 @@ export default class EachBlock extends Node {
key: string; key: string;
destructuredContexts: string[]; destructuredContexts: string[];
children: Node[];
else?: ElseBlock; else?: ElseBlock;
init( init(
@ -120,6 +122,481 @@ export default class EachBlock extends Node {
elementStack: Node[], elementStack: Node[],
componentStack: Node[] componentStack: Node[]
) { ) {
visitEachBlock(this.generator, block, state, this, elementStack, componentStack); const { generator } = this;
const each = this.var;
const create_each_block = this._block.name;
const each_block_value = this._block.listName;
const iterations = this.iterations;
const params = block.params.join(', ');
const needsAnchor = this.next ? !this.next.isDomNode() : !state.parentNode || !this.parent.isDomNode();
const anchor = needsAnchor
? block.getUniqueName(`${each}_anchor`)
: (this.next && this.next.var) || 'null';
// hack the sourcemap, so that if data is missing the bug
// is easy to find
let c = this.start + 3;
while (generator.source[c] !== 'e') c += 1;
generator.code.overwrite(c, c + 4, 'length');
const length = `[✂${c}-${c+4}✂]`;
const mountOrIntro = this._block.hasIntroMethod ? 'i' : 'm';
const vars = {
each,
create_each_block,
each_block_value,
length,
iterations,
params,
anchor,
mountOrIntro,
};
block.contextualise(this.expression);
const { snippet } = this.metadata;
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
if (this.key) {
keyed(generator, block, state, this, snippet, vars);
} else {
unkeyed(generator, block, state, this, snippet, vars);
}
const isToplevel = !state.parentNode;
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
`@createComment()`,
state.parentNode
);
}
if (this.else) {
const each_block_else = generator.getUniqueName(`${each}_else`);
block.builders.init.addLine(`var ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block
block.builders.init.addBlock(deindent`
if (!${each_block_value}.${length}) {
${each_block_else} = ${this.else._block.name}(${params}, #component);
${each_block_else}.c();
}
`);
block.builders.mount.addBlock(deindent`
if (${each_block_else}) {
${each_block_else}.${mountOrIntro}(${state.parentNode || '#target'}, null);
}
`);
const parentNode = state.parentNode || `${anchor}.parentNode`;
if (this.else._block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (!${each_block_value}.${length} && ${each_block_else}) {
${each_block_else}.p( changed, ${params} );
} else if (!${each_block_value}.${length}) {
${each_block_else} = ${this.else._block.name}(${params}, #component);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${parentNode}, ${anchor});
} else if (${each_block_else}) {
${each_block_else}.u();
${each_block_else}.d();
${each_block_else} = null;
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${each_block_value}.${length}) {
if (${each_block_else}) {
${each_block_else}.u();
${each_block_else}.d();
${each_block_else} = null;
}
} else if (!${each_block_else}) {
${each_block_else} = ${this.else._block.name}(${params}, #component);
${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${parentNode}, ${anchor});
}
`);
}
block.builders.unmount.addLine(
`if (${each_block_else}) ${each_block_else}.u()`
);
block.builders.destroy.addBlock(deindent`
if (${each_block_else}) ${each_block_else}.d();
`);
}
this.children.forEach((child: Node) => {
child.build(this._block, this._state, elementStack, componentStack);
});
if (this.else) {
this.else.children.forEach((child: Node) => {
child.build(this.else._block, this.else._state, elementStack, componentStack);
});
}
}
}
function keyed(
generator: DomGenerator,
block: Block,
state: State,
node: EachBlock,
snippet: string,
{
each,
create_each_block,
each_block_value,
length,
params,
anchor,
mountOrIntro,
}
) {
const key = block.getUniqueName('key');
const lookup = block.getUniqueName(`${each}_lookup`);
const iteration = block.getUniqueName(`${each}_iteration`);
const head = block.getUniqueName(`${each}_head`);
const last = block.getUniqueName(`${each}_last`);
const expected = block.getUniqueName(`${each}_expected`);
block.addVariable(lookup, `@blankObject()`);
block.addVariable(head);
block.addVariable(last);
if (node.children[0] && node.children[0].type === 'Element' && !generator.components.has(node.children[0].name)) {
// TODO or text/tag/raw
node._block.first = node.children[0]._state.parentNode; // TODO this is highly confusing
} else {
node._block.first = node._block.getUniqueName('first');
node._block.addElement(
node._block.first,
`@createComment()`,
`@createComment()`,
null
);
} }
block.builders.init.addBlock(deindent`
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
var ${key} = ${each_block_value}[#i].${node.key};
var ${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
if (${last}) ${last}.next = ${iteration};
${iteration}.last = ${last};
${last} = ${iteration};
if (#i === 0) ${head} = ${iteration};
}
`);
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.create.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.c();
${iteration} = ${iteration}.next;
}
`);
block.builders.claim.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.l(${state.parentNodes});
${iteration} = ${iteration}.next;
}
`);
block.builders.mount.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.${mountOrIntro}(${targetNode}, ${anchorNode});
${iteration} = ${iteration}.next;
}
`);
const dynamic = node._block.hasUpdateMethod;
const parentNode = node.parent.isDomNode() ? node.parent.var : `${anchor}.parentNode`;
let destroy;
if (node._block.hasOutroMethod) {
const fn = block.getUniqueName(`${each}_outro`);
block.builders.init.addBlock(deindent`
function ${fn}(iteration) {
iteration.o(function() {
iteration.u();
iteration.d();
${lookup}[iteration.key] = null;
});
}
`);
destroy = deindent`
while (${expected}) {
${fn}(${expected});
${expected} = ${expected}.next;
}
for (#i = 0; #i < discard_pile.length; #i += 1) {
if (discard_pile[#i].discard) {
${fn}(discard_pile[#i]);
}
}
`;
} else {
const fn = block.getUniqueName(`${each}_destroy`);
block.builders.init.addBlock(deindent`
function ${fn}(iteration) {
iteration.u();
iteration.d();
${lookup}[iteration.key] = null;
}
`);
destroy = deindent`
while (${expected}) {
${fn}(${expected});
${expected} = ${expected}.next;
}
for (#i = 0; #i < discard_pile.length; #i += 1) {
var ${iteration} = discard_pile[#i];
if (${iteration}.discard) {
${fn}(${iteration});
}
}
`;
}
block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet};
var ${expected} = ${head};
var ${last} = null;
var discard_pile = [];
for (#i = 0; #i < ${each_block_value}.${length}; #i += 1) {
var ${key} = ${each_block_value}[#i].${node.key};
var ${iteration} = ${lookup}[${key}];
${dynamic &&
`if (${iteration}) ${iteration}.p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);`}
if (${expected}) {
if (${key} === ${expected}.key) {
${expected} = ${expected}.next;
} else {
if (${iteration}) {
// probably a deletion
while (${expected} && ${expected}.key !== ${key}) {
${expected}.discard = true;
discard_pile.push(${expected});
${expected} = ${expected}.next;
};
${expected} = ${expected} && ${expected}.next;
${iteration}.discard = false;
${iteration}.last = ${last};
if (!${expected}) ${iteration}.m(${parentNode}, ${anchor});
} else {
// key is being inserted
${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
${iteration}.c();
${iteration}.${mountOrIntro}(${parentNode}, ${expected}.first);
${expected}.last = ${iteration};
${iteration}.next = ${expected};
}
}
} else {
// we're appending from this point forward
if (${iteration}) {
${iteration}.discard = false;
${iteration}.next = null;
${iteration}.m(${parentNode}, ${anchor});
} else {
${iteration} = ${lookup}[${key}] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key});
${iteration}.c();
${iteration}.${mountOrIntro}(${parentNode}, ${anchor});
}
}
if (${last}) ${last}.next = ${iteration};
${iteration}.last = ${last};
${node._block.hasIntroMethod && `${iteration}.i(${parentNode}, ${anchor});`}
${last} = ${iteration};
}
if (${last}) ${last}.next = null;
${destroy}
${head} = ${lookup}[${each_block_value}[0] && ${each_block_value}[0].${node.key}];
`);
if (!state.parentNode) {
block.builders.unmount.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.u();
${iteration} = ${iteration}.next;
}
`);
}
block.builders.destroy.addBlock(deindent`
var ${iteration} = ${head};
while (${iteration}) {
${iteration}.d();
${iteration} = ${iteration}.next;
}
`);
}
function unkeyed(
generator: DomGenerator,
block: Block,
state: State,
node: EachBlock,
snippet: string,
{
create_each_block,
each_block_value,
length,
iterations,
params,
anchor,
mountOrIntro,
}
) {
block.builders.init.addBlock(deindent`
var ${iterations} = [];
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
}
`);
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.create.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].c();
}
`);
block.builders.claim.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].l(${state.parentNodes});
}
`);
block.builders.mount.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].${mountOrIntro}(${targetNode}, ${anchorNode});
}
`);
const allDependencies = new Set(node._block.dependencies);
const { dependencies } = node.metadata;
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
// TODO do this for keyed blocks as well
const condition = Array.from(allDependencies)
.map(dependency => `changed.${dependency}`)
.join(' || ');
const parentNode = node.parent.isDomNode() ? node.parent.var : `${anchor}.parentNode`;
if (condition !== '') {
const forLoopBody = node._block.hasUpdateMethod
? node._block.hasIntroMethod
? deindent`
if (${iterations}[#i]) {
${iterations}[#i].p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);
} else {
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
${iterations}[#i].c();
}
${iterations}[#i].i(${parentNode}, ${anchor});
`
: deindent`
if (${iterations}[#i]) {
${iterations}[#i].p(changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i);
} else {
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
${iterations}[#i].c();
${iterations}[#i].m(${parentNode}, ${anchor});
}
`
: deindent`
${iterations}[#i] = ${create_each_block}(${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component);
${iterations}[#i].c();
${iterations}[#i].${mountOrIntro}(${parentNode}, ${anchor});
`;
const start = node._block.hasUpdateMethod ? '0' : `${iterations}.length`;
const outro = block.getUniqueName('outro');
const destroy = node._block.hasOutroMethod
? deindent`
function ${outro}(i) {
if (${iterations}[i]) {
${iterations}[i].o(function() {
${iterations}[i].u();
${iterations}[i].d();
${iterations}[i] = null;
});
}
}
for (; #i < ${iterations}.length; #i += 1) ${outro}(#i);
`
: deindent`
for (; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].u();
${iterations}[#i].d();
}
${iterations}.length = ${each_block_value}.${length};
`;
block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet};
if (${condition}) {
for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) {
${forLoopBody}
}
${destroy}
}
`);
}
block.builders.unmount.addBlock(deindent`
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
${iterations}[#i].u();
}
`);
block.builders.destroy.addBlock(`@destroyEach(${iterations});`);
} }

@ -8,6 +8,7 @@ import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import State from '../dom/State'; import State from '../dom/State';
import Attribute from './Attribute'; import Attribute from './Attribute';
import Text from './Text';
import * as namespaces from '../../utils/namespaces'; import * as namespaces from '../../utils/namespaces';
// temp - move this logic in here // temp - move this logic in here
@ -299,7 +300,7 @@ export default class Element extends Node {
// get a name for the event handler that is globally unique // get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise // if hoisted, locally unique otherwise
const handlerName = (shouldHoist ? this.generator : block).getUniqueName( const handlerName = (shouldHoist ? generator : block).getUniqueName(
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler` `${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
); );
@ -333,7 +334,7 @@ export default class Element extends Node {
`; `;
if (shouldHoist) { if (shouldHoist) {
this.generator.blocks.push(handler); generator.blocks.push(handler);
} else { } else {
block.builders.init.addBlock(handler); block.builders.init.addBlock(handler);
} }
@ -360,10 +361,10 @@ export default class Element extends Node {
`if (${ref} === ${name}) ${ref} = null;` `if (${ref} === ${name}) ${ref} = null;`
); );
this.generator.usesRefs = true; // so component.refs object is created generator.usesRefs = true; // so component.refs object is created
}); });
addTransitions(this.generator, block, childState, this); addTransitions(generator, block, childState, this);
if (childState.allUsedContexts.length || childState.usesComponent) { if (childState.allUsedContexts.length || childState.usesComponent) {
const initialProps: string[] = []; const initialProps: string[] = [];
@ -408,7 +409,7 @@ export default class Element extends Node {
`${childState.parentNodes}.forEach(@detachNode);` `${childState.parentNodes}.forEach(@detachNode);`
); );
function toHTML(node: Node) { function toHTML(node: Element | Text) {
if (node.type === 'Text') return node.data; if (node.type === 'Text') return node.data;
let open = `<${node.name}`; let open = `<${node.name}`;

@ -1,6 +1,8 @@
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import State from '../dom/State';
export default class PendingBlock extends Node { export default class PendingBlock extends Node {
_block: Block; _block: Block;
_state: State;
} }

@ -18,6 +18,7 @@ const elementsWithoutText = new Set([
]); ]);
export default class Text extends Node { export default class Text extends Node {
type: 'Text';
data: string; data: string;
shouldSkip: boolean; shouldSkip: boolean;

@ -1,6 +1,8 @@
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import State from '../dom/State';
export default class ThenBlock extends Node { export default class ThenBlock extends Node {
_block: Block; _block: Block;
_state: State;
} }

@ -4,11 +4,14 @@ import State from '../../dom/State';
import { trimStart, trimEnd } from '../../../utils/trim'; import { trimStart, trimEnd } from '../../../utils/trim';
export default class Node { export default class Node {
type: string;
start: number;
end: number;
metadata: { metadata: {
dependencies: string[]; dependencies: string[];
}; };
type: string;
parent: Node; parent: Node;
prev?: Node; prev?: Node;
next?: Node; next?: Node;

Loading…
Cancel
Save