nested each blocks

pull/31/head
Rich Harris 8 years ago
parent ba9238b864
commit 8f892bf65b

@ -1,332 +1,274 @@
import { getLocator } from 'locate-character'; import { getLocator } from 'locate-character';
import deindent from './utils/deindent.js'; import deindent from './utils/deindent.js';
import walkHtml from './utils/walkHtml.js'; import walkHtml from './utils/walkHtml.js';
import flattenReference from './utils/flattenReference.js';
const ROOT = 'options.target'; function createRenderer ( fragment ) {
return deindent`
function ${fragment.name} ( target${fragment.useAnchor ? ', anchor' : ''} ) {
${fragment.initStatements.join( '\n\n' )}
return {
update: function ( ${fragment.contextChain.join( ', ' )} ) {
${fragment.updateStatements.join( '\n\n' )}
},
teardown: function () {
${fragment.teardownStatements.join( '\n\n' )}
}
}
}
`;
}
export default function generate ( parsed, template ) { export default function generate ( parsed, template ) {
const locator = getLocator( template );
const renderers = [];
const counters = { const counters = {
fragment: 0,
element: 0,
text: 0,
anchor: 0,
if: 0, if: 0,
each: 0, each: 0
loop: 0
}; };
const initStatements = [];
const setStatements = [ deindent`
const oldState = state;
state = Object.assign( {}, oldState, newState );
` ];
const teardownStatements = [];
// TODO add contents of <script> tag, with `export default` replaced with `var template =` // TODO add contents of <script> tag, with `export default` replaced with `var template =`
// TODO css // TODO css
const locator = getLocator( template ); let current = {
useAnchor: false,
name: 'renderMainFragment',
target: 'target',
parsed.html.children.forEach( child => { initStatements: [],
const declarations = []; updateStatements: [],
teardownStatements: [],
let current = { contexts: {},
target: ROOT, contextChain: [ 'context' ],
conditions: [],
children: [], counters: {
renderBlocks: [], element: 0,
removeBlocks: [], text: 0,
anchor: null, anchor: 0
renderImmediately: true },
parent: null
}; };
const stack = [ current ]; function flattenExpression ( node, contexts ) {
const flattened = flattenReference( node );
if ( flattened ) {
if ( flattened.name in contexts ) return flattened.keypath;
// TODO handle globals, e.g. {{Math.round(foo)}}
return `context.${flattened.keypath}`;
}
throw new Error( 'TODO expressions' );
}
parsed.html.children.forEach( child => {
walkHtml( child, { walkHtml( child, {
Element: { Element: {
enter ( node ) { enter ( node ) {
const target = `element_${counters.element++}`; const name = `element_${current.counters.element++}`;
stack.push( current ); current.initStatements.push( deindent`
var ${name} = document.createElement( '${node.name}' );
declarations.push( `var ${target};` );
if ( current.renderImmediately ) {
current.renderBlocks.push( deindent`
${target} = document.createElement( '${node.name}' );
${current.target}.appendChild( ${target} );
` );
} else {
current.renderBlocks.push( deindent`
${target} = document.createElement( '${node.name}' );
${current.anchor}.parentNode.insertBefore( ${target}, ${current.anchor} );
` ); ` );
}
current.removeBlocks.push( deindent` current.teardownStatements.push( deindent`
${target}.parentNode.removeChild( ${target} ); ${name}.parentNode.removeChild( ${name} );
` ); ` );
current = { current = Object.assign( {}, current, {
target, target: name,
conditions: current.conditions, parent: current
children: current.children, });
renderBlocks: current.renderBlocks,
removeBlocks: current.removeBlocks,
anchor: current.anchor,
renderImmediately: false
};
}, },
leave () { leave () {
stack.pop(); const name = current.target;
current = stack[ stack.length - 1 ];
}
},
Text: { current = current.parent;
enter ( node ) {
if ( current.target === ROOT ) {
const identifier = `text_${counters.text++}`;
declarations.push( `var ${identifier};` );
current.renderBlocks.push( deindent` if ( current.useAnchor && current.target === 'target' ) {
${identifier} = document.createTextNode( ${JSON.stringify( node.data )} ); current.initStatements.push( deindent`
${current.target}.appendChild( ${identifier} ); target.insertBefore( ${name}, anchor );
` ); ` );
} else {
current.removeBlocks.push( deindent` current.initStatements.push( deindent`
${identifier}.parentNode.removeChild( ${identifier} ); ${current.target}.appendChild( ${name} );
${identifier} = null;
` ); ` );
} }
}
},
else { Text: {
current.renderBlocks.push( deindent` enter ( node ) {
${current.target}.appendChild( document.createTextNode( ${JSON.stringify( node.data )} ) ); current.initStatements.push( deindent`
${current.target}.appendChild( document.createTextNode( ${JSON.stringify( node.data ) }) );
` ); ` );
} }
}
}, },
MustacheTag: { MustacheTag: {
enter ( node ) { enter ( node ) {
const identifier = `text_${counters.text++}`; const name = `text_${current.counters.text++}`;
const expression = node.expression.type === 'Identifier' ? node.expression.name : 'TODO'; // TODO handle block-local state const expression = flattenExpression( node.expression, current.contexts );
declarations.push( `var ${identifier};` );
current.renderBlocks.push( deindent` current.initStatements.push( deindent`
${identifier} = document.createTextNode( '' ); var ${name} = document.createTextNode( '' );
${current.target}.appendChild( ${identifier} ); var ${name}_value = '';
${current.target}.appendChild( ${name} );
` ); ` );
setStatements.push( deindent` current.updateStatements.push( deindent`
if ( state.${expression} !== oldState.${expression} ) { // TODO and conditions if ( ${expression} !== ${name}_value ) {
${identifier}.data = state.${expression}; ${name}_value = ${expression};
${name}.data = ${name}_value;
} }
` ); ` );
current.removeBlocks.push( deindent`
${identifier}.parentNode.removeChild( ${identifier} );
${identifier} = null;
` );
} }
}, },
IfBlock: { IfBlock: {
enter ( node ) { enter ( node ) {
const anchor = `anchor_${counters.anchor++}`; const i = counters.if++;
const suffix = `if_${counters.if++}`; const name = `ifBlock_${i}`;
const renderer = `renderIfBlock_${i}`;
declarations.push( `var ${anchor};` );
const expression = node.expression.type === 'Identifier' ? node.expression.name : 'TODO'; // TODO handle block-local state const expression = flattenExpression( node.expression, current.contexts );
current.renderBlocks.push( deindent` current.initStatements.push( deindent`
${anchor} = document.createComment( '#if ${template.slice( node.expression.start, node.expression.end)}' ); var ${name}_anchor = document.createComment( '#if ${template.slice( node.expression.start, node.expression.end )}' );
${current.target}.appendChild( ${anchor} ); ${current.target}.appendChild( ${name}_anchor );
var ${name} = null;
` ); ` );
current.removeBlocks.push( deindent` current.updateStatements.push( deindent`
${anchor}.parentNode.removeChild( ${anchor} ); if ( ${expression} && !${name} ) {
${anchor} = null; ${name} = ${renderer}( ${current.target}, ${name}_anchor );
` );
current = {
renderName: `render_${suffix}`,
removeName: `remove_${suffix}`,
target: current.target,
conditions: current.conditions.concat( expression ),
renderBlocks: [],
removeBlocks: [],
anchor,
renderImmediately: false
};
setStatements.push( deindent`
// TODO account for conditions (nested ifs)
if ( state.${expression} && !oldState.${expression} ) ${current.renderName}();
else if ( !state.${expression} && oldState.${expression} ) ${current.removeName}();
` );
teardownStatements.push( deindent`
// TODO account for conditions (nested ifs)
if ( state.${expression} ) ${current.removeName}();
` );
stack.push( current );
},
leave ( node ) {
const { line, column } = locator( node.start );
initStatements.push( deindent`
// (${line}:${column}) {{#if ${template.slice( node.expression.start, node.expression.end )}}}...{{/if}}
function ${current.renderName} () {
${current.renderBlocks.join( '\n\n' )}
} }
function ${current.removeName} () { else if ( !${expression} && ${name} ) {
${current.removeBlocks.join( '\n\n' )} ${name}.teardown();
${name} = null;
} }
` );
stack.pop(); if ( ${name} ) {
current = stack[ stack.length - 1 ]; ${name}.update( context );
} }
},
EachBlock: {
enter ( node ) {
const loopIndex = counters.loop++;
const anchor = `anchor_${counters.anchor++}`;
declarations.push( `var fragment_${loopIndex} = document.createDocumentFragment();` );
declarations.push( `var ${anchor};` );
const expression = node.expression.type === 'Identifier' ? node.expression.name : 'TODO'; // TODO handle block-local state
current.renderBlocks.push( deindent`
${anchor} = document.createComment( '#each ${template.slice( node.expression.start, node.expression.end)}' );
${current.target}.appendChild( ${anchor} );
` ); ` );
current.removeBlocks.push( deindent` current.teardownStatements.push( deindent`
${anchor}.parentNode.removeChild( ${anchor} ); if ( ${name} ) ${name}.teardown();
${anchor} = null;
` ); ` );
current = { current = {
target: `fragment_${loopIndex}`, useAnchor: true,
expression, name: renderer,
conditions: current.conditions, target: 'target',
renderBlocks: [],
removeBlocks: [],
anchor,
loopIndex,
renderImmediately: true
};
setStatements.push( deindent` contextChain: current.contextChain,
// TODO account for conditions (nested ifs)
if ( '${expression}' in state ) each_${loopIndex}.update();
` );
// need to add teardown logic if this is at the initStatements: [],
// top level (TODO or if there are event handlers attached?) updateStatements: [],
if ( current.target === ROOT ) { teardownStatements: [],
teardownStatements.push( deindent`
if ( true ) { // <!-- TODO conditions
for ( let i = 0; i < state.${expression}.length; i += 1 ) {
each_${loopIndex}.removeIteration( i );
}
}
` );
}
stack.push( current ); counters: {
element: 0,
text: 0,
anchor: 0
}, },
leave ( node ) { parent: current
const { line, column } = locator( node.start ); };
},
const loopIndex = current.loopIndex;
initStatements.push( deindent` leave () {
// (${line}:${column}) {{#each ${template.slice( node.expression.start, node.expression.end )}}}...{{/each}} renderers.push( createRenderer( current ) );
${current.renderBlocks.join( '\n\n' )} current = current.parent;
}
},
var each_${loopIndex} = { EachBlock: {
iterations: [], enter ( node ) {
const i = counters.each++;
const name = `eachBlock_${i}`;
const renderer = `renderEachBlock_${i}`;
update: function () { const expression = flattenExpression( node.expression, current.contexts );
var target = document.createDocumentFragment();
var i; current.initStatements.push( deindent`
var ${name}_anchor = document.createComment( '#each ${template.slice( node.expression.start, node.expression.end )}' );
${current.target}.appendChild( ${name}_anchor );
var ${name}_iterations = [];
const ${name}_fragment = document.createDocumentFragment();
` );
for ( i = 0; i < state.${current.expression}.length; i += 1 ) { current.updateStatements.push( deindent`
if ( !this.iterations[i] ) { for ( var i = 0; i < ${expression}.length; i += 1 ) {
this.iterations[i] = this.renderIteration( target ); if ( !${name}_iterations[i] ) {
${name}_iterations[i] = ${renderer}( ${name}_fragment );
} }
const iteration = this.iterations[i]; const iteration = ${name}_iterations[i];
this.updateIteration( this.iterations[i], state.${current.expression}[i] ); ${name}_iterations[i].update( ${current.contextChain.join( ', ' )}, ${expression}[i] );
} }
for ( ; i < this.iterations.length; i += 1 ) { for ( var i = ${expression}.length; i < ${name}_iterations.length; i += 1 ) {
this.removeIteration( i ); ${name}_iterations[i].teardown();
} }
${current.anchor}.parentNode.insertBefore( target, ${current.anchor} ); ${name}_anchor.parentNode.insertBefore( ${name}_fragment, ${name}_anchor );
each_${loopIndex}.length = state.${current.expression}.length; ${name}_iterations.length = ${expression}.length;
}, ` );
renderIteration: function ( target ) { current.teardownStatements.push( deindent`
var fragment = fragment_0.cloneNode( true ); for ( let i = 0; i < ${name}_iterations.length; i += 1 ) {
${name}_iterations[i].teardown();
}
` );
var element_0 = fragment.childNodes[0]; const contexts = Object.assign( {}, current.contexts );
var text_0 = element_0.childNodes[0]; contexts[ node.context ] = true;
var iteration = { current = {
element_0: element_0, useAnchor: false,
text_0: text_0 name: renderer,
}; target: 'target',
target.appendChild( fragment ); contexts,
return iteration; contextChain: current.contextChain.concat( node.context ),
},
initStatements: [],
updateStatements: [],
teardownStatements: [],
updateIteration: function ( iteration, context ) { counters: {
iteration.text_0.data = context; element: 0,
text: 0,
anchor: 0
}, },
removeIteration: function ( i ) { parent: current
var iteration = this.iterations[i];
iteration.element_0.parentNode.removeChild( iteration.element_0 );
}
}; };
` ); },
teardownStatements.push( ...current.removeBlocks ); leave () {
renderers.push( createRenderer( current ) );
stack.pop(); current = current.parent;
current = stack[ stack.length - 1 ];
} }
} }
}); });
initStatements.push( ...current.renderBlocks );
initStatements.unshift( declarations.join( '\n' ) );
teardownStatements.push( ...current.removeBlocks );
}); });
teardownStatements.push( 'state = {};' ); renderers.push( createRenderer( current ) );
const code = deindent` const code = deindent`
${renderers.reverse().join( '\n\n' )}
export default function createComponent ( options ) { export default function createComponent ( options ) {
var component = {}; var component = {};
var state = {}; var state = {};
@ -373,16 +315,18 @@ export default function generate ( parsed, template ) {
// component-specific methods // component-specific methods
component.set = function set ( newState ) { component.set = function set ( newState ) {
${setStatements.join( '\n\n' )} Object.assign( state, newState );
mainFragment.update( state );
}; };
component.teardown = function teardown () { component.teardown = function teardown () {
${teardownStatements.join( '\n\n' )} mainFragment.teardown();
}; mainFragment = null;
// initialisation state = {};
${initStatements.join( '\n\n' )} };
let mainFragment = renderMainFragment( options.target );
component.set( options.data ); component.set( options.data );
return component; return component;

@ -0,0 +1,16 @@
export default function flatten ( node ) {
const parts = [];
while ( node.type === 'MemberExpression' ) {
if ( node.computed ) return null;
parts.unshift( node.property.name );
node = node.object;
}
if ( node.type !== 'Identifier' ) return null;
const name = node.name;
parts.unshift( name );
return { name, keypath: parts.join( '.' ) };
}

@ -0,0 +1,28 @@
export default function isReference ( node, parent ) {
if ( node.type === 'MemberExpression' ) {
return !node.computed && isReference( node.object, node );
}
if ( node.type === 'Identifier' ) {
// the only time we could have an identifier node without a parent is
// if it's the entire body of a function without a block statement
// i.e. an arrow function expression like `a => a`
if ( !parent ) return true;
// TODO is this right?
if ( parent.type === 'MemberExpression' || parent.type === 'MethodDefinition' ) {
return parent.computed || node === parent.object;
}
// disregard the `bar` in `{ bar: foo }`, but keep it in `{ [bar]: foo }`
if ( parent.type === 'Property' ) return parent.computed || node === parent.value;
// disregard the `bar` in `class Foo { bar () {...} }`
if ( parent.type === 'MethodDefinition' ) return false;
// disregard the `bar` in `export { foo as bar }`
if ( parent.type === 'ExportSpecifier' && node !== parent.local ) return;
return true;
}
}

@ -2,9 +2,9 @@ export default {
description: 'nested {{#each}} blocks', description: 'nested {{#each}} blocks',
data: { data: {
columns: [ 'a', 'b', 'c' ], columns: [ 'a', 'b', 'c' ],
row: [ 1, 2, 3 ] rows: [ 1, 2, 3 ]
}, },
html: `<div>a, 1</div><div>b, 1</div><div>c, 1</div><div>a, 2</div><div>b, 2</div><div>c, 2</div><div>a, 3</div><div>b, 3</div><div>c, 3</div>`, html: `<div>a, 1</div><div>a, 2</div><div>a, 3</div><!--#each rows--><div>b, 1</div><div>b, 2</div><div>b, 3</div><!--#each rows--><div>c, 1</div><div>c, 2</div><div>c, 3</div><!--#each rows--><!--#each columns-->`,
test ( component, target ) { test ( component, target ) {
// TODO // TODO
} }

@ -64,7 +64,7 @@ describe( 'svelte', () => {
i = String( i + 1 ); i = String( i + 1 );
while ( i.length < 3 ) i = ` ${i}`; while ( i.length < 3 ) i = ` ${i}`;
return `${i}: ${line}`; return `${i}: ${line.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) )}`;
}).join( '\n' ); }).join( '\n' );
cache[ path.resolve( `test/samples/${dir}/main.svelte` ) ] = code; cache[ path.resolve( `test/samples/${dir}/main.svelte` ) ] = code;

Loading…
Cancel
Save