consolidate everything in Component.ts - much cleaner

pull/772/head
Rich Harris 8 years ago
parent 507d6a4062
commit 6f28f25a68

@ -122,11 +122,16 @@ export default class Generator {
expression: Node, expression: Node,
context: string, context: string,
isEventHandler: boolean isEventHandler: boolean
) { ): {
dependencies: string[],
contexts: Set<string>,
indexes: Set<string>,
snippet: string
} {
this.addSourcemapLocations(expression); this.addSourcemapLocations(expression);
const usedContexts = new Set(); const usedContexts: Set<string> = new Set();
const usedIndexes = new Set(); const usedIndexes: Set<string> = new Set();
const { code, helpers } = this; const { code, helpers } = this;
const { contexts, indexes } = block; const { contexts, indexes } = block;
@ -208,7 +213,7 @@ export default class Generator {
}, },
}); });
const dependencies = new Set(expression._dependencies || []); const dependencies: Set<string> = new Set(expression._dependencies || []);
if (expression._dependencies) { if (expression._dependencies) {
expression._dependencies.forEach((prop: string) => { expression._dependencies.forEach((prop: string) => {

@ -1,14 +1,13 @@
import deindent from '../../../../utils/deindent'; import deindent from '../../../utils/deindent';
import CodeBuilder from '../../../../utils/CodeBuilder'; import CodeBuilder from '../../../utils/CodeBuilder';
import visit from '../../visit'; import visit from '../visit';
import visitAttribute from './Attribute'; import { DomGenerator } from '../index';
import visitBinding from './Binding'; import Block from '../Block';
import { DomGenerator } from '../../index'; import getTailSnippet from '../../../utils/getTailSnippet';
import Block from '../../Block'; import getObject from '../../../utils/getObject';
import getTailSnippet from '../../../../utils/getTailSnippet'; import { stringify } from '../../../utils/stringify';
import getObject from '../../../../utils/getObject'; import { Node } from '../../../interfaces';
import { Node } from '../../../../interfaces'; import { State } from '../interfaces';
import { State } from '../../interfaces';
function stringifyProps(props: string[]) { function stringifyProps(props: string[]) {
if (!props.length) return '{}'; if (!props.length) return '{}';
@ -22,15 +21,22 @@ function stringifyProps(props: string[]) {
return `{ ${joined} }`; return `{ ${joined} }`;
} }
const order = { interface Attribute {
Attribute: 1, name: string;
Binding: 3 value: any;
}; dynamic: boolean;
dependencies?: string[]
}
const visitors = { interface Binding {
Attribute: visitAttribute, name: string;
Binding: visitBinding value: Node;
}; contexts: Set<string>;
snippet: string;
obj: string;
prop: string;
dependencies: string[];
}
export default function visitComponent( export default function visitComponent(
generator: DomGenerator, generator: DomGenerator,
@ -39,47 +45,22 @@ export default function visitComponent(
node: Node, node: Node,
elementStack: Node[] elementStack: Node[]
) { ) {
const hasChildren = node.children.length > 0; generator.hasComponents = true;
const name = block.getUniqueName( const name = block.getUniqueName(
(node.name === ':Self' ? generator.name : node.name).toLowerCase() (node.name === ':Self' ? generator.name : node.name).toLowerCase()
); );
const childState = node._state;
const local = {
allUsedContexts: [],
staticAttributes: [],
dynamicAttributes: [],
bindings: []
};
const isTopLevel = !state.parentNode;
generator.hasComponents = true;
node.attributes
.sort((a, b) => order[a.type] - order[b.type])
.forEach(attribute => {
visitors[attribute.type] && visitors[attribute.type](
generator,
block,
childState,
node,
attribute,
local
);
});
const componentInitProperties = [`_root: #component._root`]; const componentInitProperties = [`_root: #component._root`];
// Component has children, put them in a separate {{yield}} block // Component has children, put them in a separate {{yield}} block
if (hasChildren) { if (node.children.length > 0) {
const params = block.params.join(', '); const params = block.params.join(', ');
const childBlock = node._block; const childBlock = node._block;
node.children.forEach((child: Node) => { node.children.forEach((child: Node) => {
visit(generator, childBlock, childState, child, elementStack); visit(generator, childBlock, node._state, child, elementStack);
}); });
const yield_fragment = block.getUniqueName(`${name}_yield_fragment`); const yield_fragment = block.getUniqueName(`${name}_yield_fragment`);
@ -105,41 +86,51 @@ export default function visitComponent(
componentInitProperties.push(`_yield: ${yield_fragment}`); componentInitProperties.push(`_yield: ${yield_fragment}`);
} }
const allContexts = new Set();
const statements: string[] = []; const statements: string[] = [];
const name_context = block.getUniqueName(`${name}_context`);
let name_updating: string; let name_updating: string;
let name_initial_data: string; let name_initial_data: string;
let beforecreate: string = null; let beforecreate: string = null;
if ( const attributes = node.attributes
local.staticAttributes.length || .filter((a: Node) => a.type === 'Attribute')
local.dynamicAttributes.length || .map((a: Node) => mungeAttribute(a, block));
local.bindings.length
) { const bindings = node.attributes
const initialProps = local.staticAttributes .filter((a: Node) => a.type === 'Binding')
.concat(local.dynamicAttributes) .map((a: Node) => mungeBinding(a, block));
.map(attribute => `${attribute.name}: ${attribute.value}`);
if (attributes.length || bindings.length) {
const initialProps = attributes
.map((attribute: Attribute) => `${attribute.name}: ${attribute.value}`);
const initialPropString = stringifyProps(initialProps); const initialPropString = stringifyProps(initialProps);
const updates: string[] = []; const updates: string[] = [];
local.dynamicAttributes.forEach(attribute => { attributes
if (attribute.dependencies.length) { .filter((attribute: Attribute) => attribute.dynamic)
updates.push(deindent` .forEach((attribute: Attribute) => {
if ( ${attribute.dependencies if (attribute.dependencies.length) {
.map(dependency => `changed.${dependency}`) updates.push(deindent`
.join(' || ')} ) ${name}_changes.${attribute.name} = ${attribute.value}; 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 else {
updates.push(`${name}_changes.${attribute.name} = ${attribute.value};`); // 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;
if (local.bindings.length) {
name_updating = block.alias(`${name}_updating`); name_updating = block.alias(`${name}_updating`);
name_initial_data = block.getUniqueName(`${name}_initial_data`); name_initial_data = block.getUniqueName(`${name}_initial_data`);
@ -149,9 +140,13 @@ export default function visitComponent(
const setParentFromChildOnChange = new CodeBuilder(); const setParentFromChildOnChange = new CodeBuilder();
const setParentFromChildOnInit = new CodeBuilder(); const setParentFromChildOnInit = new CodeBuilder();
local.bindings.forEach(binding => { bindings.forEach((binding: Binding) => {
let setParentFromChild; let setParentFromChild;
binding.contexts.forEach(context => {
allContexts.add(context);
});
const { name: key } = getObject(binding.value); const { name: key } = getObject(binding.value);
if (block.contexts.has(key)) { if (block.contexts.has(key)) {
@ -160,8 +155,8 @@ export default function visitComponent(
const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : ''; const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : '';
setParentFromChild = deindent` setParentFromChild = deindent`
var list = ${name}._context.${block.listNames.get(key)}; var list = ${name_context}.${block.listNames.get(key)};
var index = ${name}._context.${block.indexNames.get(key)}; var index = ${name_context}.${block.indexNames.get(key)};
list[index]${tail} = childState.${binding.name}; list[index]${tail} = childState.${binding.name};
${binding.dependencies ${binding.dependencies
@ -226,7 +221,7 @@ export default function visitComponent(
var state = #component.get(), childState = ${name}.get(), newState = {}; var state = #component.get(), childState = ${name}.get(), newState = {};
if (!childState) return; if (!childState) return;
${setParentFromChildOnInit} ${setParentFromChildOnInit}
${name_updating} = { ${local.bindings.map(binding => `${binding.name}: true`).join(', ')} }; ${name_updating} = { ${bindings.map((binding: Binding) => `${binding.name}: true`).join(', ')} };
#component._set(newState); #component._set(newState);
${name_updating} = {}; ${name_updating} = {};
}); });
@ -239,7 +234,7 @@ export default function visitComponent(
var ${name}_changes = {}; var ${name}_changes = {};
${updates.join('\n')} ${updates.join('\n')}
${name}._set( ${name}_changes ); ${name}._set( ${name}_changes );
${local.bindings.length && `${name_updating} = {};`} ${bindings.length && `${name_updating} = {};`}
`); `);
} }
@ -257,8 +252,7 @@ export default function visitComponent(
${beforecreate} ${beforecreate}
`); `);
if (isTopLevel) if (!state.parentNode) block.builders.unmount.addLine(`${name}._fragment.unmount();`);
block.builders.unmount.addLine(`${name}._fragment.unmount();`);
block.builders.destroy.addLine(`${name}.destroy( false );`); block.builders.destroy.addLine(`${name}.destroy( false );`);
const targetNode = state.parentNode || '#target'; const targetNode = state.parentNode || '#target';
@ -288,20 +282,19 @@ export default function visitComponent(
contexts.forEach(context => { contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context); if (!~usedContexts.indexOf(context)) usedContexts.push(context);
if (!~local.allUsedContexts.indexOf(context)) allContexts.add(context);
local.allUsedContexts.push(context);
}); });
}); });
} }
// TODO hoist event handlers? can do `this.__component.method(...)` // TODO hoist event handlers? can do `this.__component.method(...)`
const declarations = usedContexts.map(name => { const declarations = usedContexts.map(name => {
if (name === 'state') return 'var state = this._context.state;'; if (name === 'state') return `var state = ${name_context}.state;`;
const listName = block.listNames.get(name); const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name); const indexName = block.indexNames.get(name);
return `var ${listName} = this._context.${listName}, ${indexName} = this._context.${indexName}, ${name} = ${listName}[${indexName}]`; return `var ${listName} = ${name_context}.${listName}, ${indexName} = ${name_context}.${indexName}, ${name} = ${listName}[${indexName}]`;
}); });
const handlerBody = const handlerBody =
@ -329,8 +322,10 @@ export default function visitComponent(
}); });
// maintain component context // maintain component context
if (local.allUsedContexts.length) { if (allContexts.size) {
const initialProps = local.allUsedContexts const contexts = Array.from(allContexts);
const initialProps = contexts
.map(contextName => { .map(contextName => {
if (contextName === 'state') return `state: state`; if (contextName === 'state') return `state: state`;
@ -341,19 +336,19 @@ export default function visitComponent(
}) })
.join(',\n'); .join(',\n');
const updates = local.allUsedContexts const updates = contexts
.map(contextName => { .map(contextName => {
if (contextName === 'state') return `${name}._context.state = state;`; if (contextName === 'state') return `${name_context}.state = state;`;
const listName = block.listNames.get(contextName); const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName); const indexName = block.indexNames.get(contextName);
return `${name}._context.${listName} = ${listName};\n${name}._context.${indexName} = ${indexName};`; return `${name_context}.${listName} = ${listName};\n${name_context}.${indexName} = ${indexName};`;
}) })
.join('\n'); .join('\n');
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
${name}._context = { var ${name_context} = {
${initialProps} ${initialProps}
}; };
`); `);
@ -362,6 +357,113 @@ export default function visitComponent(
} }
} }
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
const { dependencies, snippet } = block.contextualise(value.expression);
// 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 {
const { dependencies, snippet } = block.contextualise(
chunk.expression
);
dependencies.forEach(dependency => {
allDependencies.add(dependency);
});
return `( ${snippet} )`; // TODO only parenthesize if necessary
}
})
.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 { snippet, contexts, dependencies } = block.contextualise(
binding.value
);
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 isComputed(node: Node) { function isComputed(node: Node) {
while (node.type === 'MemberExpression') { while (node.type === 'MemberExpression') {
if (node.computed) return true; if (node.computed) return true;

@ -1,77 +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 visitAttribute(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute,
local
) {
if (attribute.value === true) {
// attributes without values, e.g. <textarea readonly>
local.staticAttributes.push({
name: attribute.name,
value: true,
});
} else if (attribute.value.length === 0) {
local.staticAttributes.push({
name: attribute.name,
value: `''`,
});
} else if (attribute.value.length === 1) {
const value = attribute.value[0];
if (value.type === 'Text') {
// static attributes
const result = isNaN(value.data) ? stringify(value.data) : value.data;
local.staticAttributes.push({
name: attribute.name,
value: result,
});
} else {
// simple dynamic attributes
const { dependencies, snippet } = block.contextualise(value.expression);
// TODO only update attributes that have changed
local.dynamicAttributes.push({
name: attribute.name,
value: snippet,
dependencies,
});
}
} else {
// complex dynamic attributes
const allDependencies = [];
const value =
(attribute.value[0].type === 'Text' ? '' : `"" + `) +
attribute.value
.map(chunk => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { dependencies, snippet } = block.contextualise(
chunk.expression
);
dependencies.forEach(dependency => {
if (!~allDependencies.indexOf(dependency))
allDependencies.push(dependency);
});
return `( ${snippet} )`;
}
})
.join(' + ');
local.dynamicAttributes.push({
name: attribute.name,
value,
dependencies: allDependencies,
});
}
}

@ -1,55 +0,0 @@
import deindent from '../../../../utils/deindent';
import flattenReference from '../../../../utils/flattenReference';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
import getObject from '../../../../utils/getObject';
export default function visitBinding(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute,
local
) {
const { name } = getObject(attribute.value);
const { snippet, contexts, dependencies } = block.contextualise(
attribute.value
);
contexts.forEach(context => {
if (!~local.allUsedContexts.indexOf(context))
local.allUsedContexts.push(context);
});
const contextual = block.contexts.has(name);
let obj;
let prop;
if (contextual) {
obj = block.listNames.get(name);
prop = block.indexNames.get(name);
} else if (attribute.value.type === 'MemberExpression') {
prop = `[✂${attribute.value.property.start}-${attribute.value.property
.end}]`;
if (!attribute.value.computed) prop = `'${prop}'`;
obj = `[✂${attribute.value.object.start}-${attribute.value.object.end}✂]`;
} else {
obj = 'state';
prop = `'${name}'`;
}
local.bindings.push({
name: attribute.name,
value: attribute.value,
snippet: snippet,
obj,
prop,
dependencies
});
generator.hasComplexBindings = true;
}

@ -1,6 +1,6 @@
import deindent from '../../../../utils/deindent'; import deindent from '../../../../utils/deindent';
import visit from '../../visit'; import visit from '../../visit';
import visitComponent from '../Component/Component'; import visitComponent from '../Component';
import visitWindow from './meta/Window'; import visitWindow from './meta/Window';
import visitAttribute from './Attribute'; import visitAttribute from './Attribute';
import visitEventHandler from './EventHandler'; import visitEventHandler from './EventHandler';

Loading…
Cancel
Save