diff --git a/.gitignore b/.gitignore index d7c1b39fcf..fc40bd1c55 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ coverage coverage.lcov test/sourcemaps/*/output.js test/sourcemaps/*/output.js.map +scratch diff --git a/src/generators/dom/index.js b/src/generators/dom/index.js index 6c4e40dbd9..8088fbc912 100644 --- a/src/generators/dom/index.js +++ b/src/generators/dom/index.js @@ -50,22 +50,30 @@ class DomGenerator extends Generator { ` ); } + const properties = new CodeBuilder(); + + if ( fragment.key ) properties.addBlock( `key: key,` ); + + properties.addBlock( deindent` + mount: function ( target, anchor ) { + ${fragment.builders.mount} + }, + + update: function ( changed, ${fragment.params} ) { + ${fragment.builders.update} + }, + + teardown: function ( detach ) { + ${fragment.builders.teardown} + } + ` ); + this.renderers.push( deindent` - function ${fragment.name} ( ${fragment.params}, component ) { + function ${fragment.name} ( ${fragment.params}, component${fragment.key ? `, key` : ''} ) { ${fragment.builders.init} return { - mount: function ( target, anchor ) { - ${fragment.builders.mount} - }, - - update: function ( changed, ${fragment.params} ) { - ${fragment.builders.update} - }, - - teardown: function ( detach ) { - ${fragment.builders.teardown} - } + ${properties} }; } ` ); @@ -127,6 +135,7 @@ export default function dom ( parsed, source, options, names ) { namespace, target: 'target', localElementDepth: 0, + key: null, contexts: {}, indexes: {}, diff --git a/src/generators/dom/visitors/Component.js b/src/generators/dom/visitors/Component.js index 113417c2fe..72294df8fb 100644 --- a/src/generators/dom/visitors/Component.js +++ b/src/generators/dom/visitors/Component.js @@ -117,7 +117,8 @@ export default { namespace: local.namespace, target: name, parent: generator.current, - localElementDepth: generator.current.localElementDepth + 1 + localElementDepth: generator.current.localElementDepth + 1, + key: null }); }, diff --git a/src/generators/dom/visitors/EachBlock.js b/src/generators/dom/visitors/EachBlock.js index b0ec2113e0..13bf576a5b 100644 --- a/src/generators/dom/visitors/EachBlock.js +++ b/src/generators/dom/visitors/EachBlock.js @@ -1,3 +1,4 @@ +import CodeBuilder from '../../../utils/CodeBuilder.js'; import deindent from '../../../utils/deindent.js'; import getBuilders from '../utils/getBuilders.js'; @@ -22,16 +23,42 @@ export default { const anchor = `${name}_anchor`; generator.createAnchor( anchor, `#each ${generator.source.slice( node.expression.start, node.expression.end )}` ); - generator.current.builders.init.addBlock( deindent` - var ${name}_value = ${snippet}; - var ${iterations} = []; - ${node.else ? `var ${elseName} = null;` : ''} + generator.current.builders.init.addLine( `var ${name}_value = ${snippet};` ); + generator.current.builders.init.addLine( `var ${iterations} = [];` ); + if ( node.key ) generator.current.builders.init.addLine( `var ${name}_lookup = Object.create( null );` ); + if ( node.else ) generator.current.builders.init.addLine( `var ${elseName} = null;` ); + + const initialRender = new CodeBuilder(); + + const localVars = {}; + + if ( node.key ) { + localVars.fragment = generator.current.getUniqueName( 'fragment' ); + localVars.value = generator.current.getUniqueName( 'value' ); + localVars.key = generator.current.getUniqueName( 'key' ); + + initialRender.addBlock( deindent` + var ${localVars.key} = ${name}_value[${i}].${node.key}; + ${name}_iterations[${i}] = ${name}_lookup[ ${localVars.key} ] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, component${node.key ? `, ${localVars.key}` : `` } ); + ` ); + } else { + initialRender.addLine( + `${name}_iterations[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, component );` + ); + } + if ( !isToplevel ) { + initialRender.addLine( + `${name}_iterations[${i}].mount( ${anchor}.parentNode, ${anchor} );` + ); + } + + generator.current.builders.init.addBlock( deindent` for ( var ${i} = 0; ${i} < ${name}_value.length; ${i} += 1 ) { - ${iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, component ); - ${!isToplevel ? `${iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} );` : ''} + ${initialRender} } ` ); + if ( node.else ) { generator.current.builders.init.addBlock( deindent` if ( !${name}_value.length ) { @@ -56,24 +83,62 @@ export default { } } - generator.current.builders.update.addBlock( deindent` - var ${name}_value = ${snippet}; + if ( node.key ) { + generator.current.builders.update.addBlock( deindent` + var ${name}_value = ${snippet}; + var _${name}_iterations = []; + var _${name}_lookup = Object.create( null ); + + var ${localVars.fragment} = document.createDocumentFragment(); + + // create new iterations as necessary + for ( var ${i} = 0; ${i} < ${name}_value.length; ${i} += 1 ) { + var ${localVars.value} = ${name}_value[${i}]; + var ${localVars.key} = ${localVars.value}.${node.key}; + + if ( ${name}_lookup[ ${localVars.key} ] ) { + _${name}_iterations[${i}] = _${name}_lookup[ ${localVars.key} ] = ${name}_lookup[ ${localVars.key} ]; + _${name}_lookup[ ${localVars.key} ].update( changed, ${params}, ${listName}, ${listName}[${i}], ${i} ); + } else { + _${name}_iterations[${i}] = _${name}_lookup[ ${localVars.key} ] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, component${node.key ? `, ${localVars.key}` : `` } ); + } - for ( var ${i} = 0; ${i} < ${name}_value.length; ${i} += 1 ) { - if ( !${iterations}[${i}] ) { - ${iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, component ); - ${iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} ); - } else { - ${iterations}[${i}].update( changed, ${params}, ${listName}, ${listName}[${i}], ${i} ); + _${name}_iterations[${i}].mount( ${localVars.fragment}, null ); } - } - for ( var ${i} = ${name}_value.length; ${i} < ${iterations}.length; ${i} += 1 ) { - ${iterations}[${i}].teardown( true ); - } + // remove old iterations + for ( var ${i} = 0; ${i} < ${name}_iterations.length; ${i} += 1 ) { + var ${name}_iteration = ${name}_iterations[${i}]; + if ( !_${name}_lookup[ ${name}_iteration.${localVars.key} ] ) { + ${name}_iteration.teardown( true ); + } + } - ${iterations}.length = ${listName}.length; - ` ); + ${name}_anchor.parentNode.insertBefore( ${localVars.fragment}, ${name}_anchor ); + + ${name}_iterations = _${name}_iterations; + ${name}_lookup = _${name}_lookup; + ` ); + } else { + generator.current.builders.update.addBlock( deindent` + var ${name}_value = ${snippet}; + + for ( var ${i} = 0; ${i} < ${name}_value.length; ${i} += 1 ) { + if ( !${iterations}[${i}] ) { + ${iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, component ); + ${iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} ); + } else { + ${iterations}[${i}].update( changed, ${params}, ${listName}, ${listName}[${i}], ${i} ); + } + } + + for ( var ${i} = ${name}_value.length; ${i} < ${iterations}.length; ${i} += 1 ) { + ${iterations}[${i}].teardown( true ); + } + + ${iterations}.length = ${listName}.length; + ` ); + } if ( node.else ) { generator.current.builders.update.addBlock( deindent` @@ -128,6 +193,7 @@ export default { target: 'target', expression: node.expression, context: node.context, + key: node.key, localElementDepth: 0, contextDependencies, diff --git a/src/generators/dom/visitors/Element.js b/src/generators/dom/visitors/Element.js index 67759ad60a..9409ff9710 100644 --- a/src/generators/dom/visitors/Element.js +++ b/src/generators/dom/visitors/Element.js @@ -88,7 +88,8 @@ export default { namespace: local.namespace, target: name, parent: generator.current, - localElementDepth: generator.current.localElementDepth + 1 + localElementDepth: generator.current.localElementDepth + 1, + key: null }); },