diff --git a/src/generators/Generator.js b/src/generators/Generator.js index c5147c97b1..5d7569be9d 100644 --- a/src/generators/Generator.js +++ b/src/generators/Generator.js @@ -63,6 +63,8 @@ export default class Generator { } contextualise ( block, expression, context, isEventHandler ) { + if ( expression._contextualised ) return expression._contextualised; + this.addSourcemapLocations( expression ); const usedContexts = []; @@ -153,12 +155,13 @@ export default class Generator { } }); - return { + expression._contextualised = { dependencies, contexts: usedContexts, - snippet: `[✂${expression.start}-${expression.end}✂]`, - string: this.code.slice( expression.start, expression.end ) + snippet: `[✂${expression.start}-${expression.end}✂]` }; + + return expression._contextualised; } generate ( result, options, { name, format } ) { diff --git a/src/generators/dom/Block.js b/src/generators/dom/Block.js index 3d57cc7395..652262c512 100644 --- a/src/generators/dom/Block.js +++ b/src/generators/dom/Block.js @@ -2,20 +2,23 @@ import CodeBuilder from '../../utils/CodeBuilder.js'; import deindent from '../../utils/deindent.js'; export default class Block { - constructor ({ generator, name, key, expression, context, contextDependencies, contexts, indexes, params, indexNames, listNames }) { - this.generator = generator; - this.name = name; - this.key = key; - this.expression = expression; - this.context = context; + constructor ( options ) { + this.generator = options.generator; + this.name = options.name; + this.key = options.key; + this.expression = options.expression; + this.context = options.context; - this.contexts = contexts; - this.indexes = indexes; - this.contextDependencies = contextDependencies; + this.contexts = options.contexts; + this.indexes = options.indexes; + this.contextDependencies = options.contextDependencies; + this.dependencies = new Set(); - this.params = params; - this.indexNames = indexNames; - this.listNames = listNames; + this.params = options.params; + this.indexNames = options.indexNames; + this.listNames = options.listNames; + + this.listName = options.listName; this.builders = { create: new CodeBuilder(), @@ -26,10 +29,17 @@ export default class Block { destroy: new CodeBuilder() }; - this.getUniqueName = generator.getUniqueNameMaker( params ); + this.getUniqueName = this.generator.getUniqueNameMaker( options.params ); // unique names this.component = this.getUniqueName( 'component' ); + this.target = this.getUniqueName( 'target' ); + } + + addDependencies ( dependencies ) { + dependencies.forEach( dependency => { + this.dependencies.add( dependency ); + }); } addElement ( name, renderStatement, parentNode, needsIdentifier = false ) { @@ -66,7 +76,7 @@ export default class Block { if ( parentNode ) { this.builders.create.addLine( `${this.generator.helper( 'appendNode' )}( ${name}, ${parentNode} );` ); } else { - this.builders.mount.addLine( `${this.generator.helper( 'insertNode' )}( ${name}, target, anchor );` ); + this.builders.mount.addLine( `${this.generator.helper( 'insertNode' )}( ${name}, ${this.target}, anchor );` ); } } @@ -99,7 +109,7 @@ export default class Block { properties.addBlock( `mount: ${this.generator.helper( 'noop' )},` ); } else { properties.addBlock( deindent` - mount: function ( target, anchor ) { + mount: function ( ${this.target}, anchor ) { ${this.builders.mount} }, ` ); diff --git a/src/generators/dom/index.js b/src/generators/dom/index.js index 1470b39342..c8d108b71e 100644 --- a/src/generators/dom/index.js +++ b/src/generators/dom/index.js @@ -7,7 +7,7 @@ import deindent from '../../utils/deindent.js'; import CodeBuilder from '../../utils/CodeBuilder.js'; import visit from './visit.js'; import Generator from '../Generator.js'; -import Block from './Block.js'; +import preprocess from './preprocess.js'; import * as shared from '../../shared/index.js'; class DomGenerator extends Generator { @@ -47,25 +47,7 @@ export default function dom ( parsed, source, options ) { const { computations, hasJs, templateProperties, namespace } = generator.parseJs(); - const getUniqueName = generator.getUniqueNameMaker( [ 'root' ] ); - const component = getUniqueName( 'component' ); - - const mainBlock = new Block({ - generator, - name: generator.alias( 'create_main_fragment' ), - key: null, - - component, - - contexts: new Map(), - indexes: new Map(), - - params: [ 'root' ], - indexNames: new Map(), - listNames: new Map(), - - getUniqueName - }); + const block = preprocess( generator, parsed.html.children ); const state = { namespace, @@ -74,10 +56,10 @@ export default function dom ( parsed, source, options ) { }; parsed.html.children.forEach( node => { - visit( generator, mainBlock, state, node ); + visit( generator, block, state, node ); }); - generator.addBlock( mainBlock ); + generator.addBlock( block ); const builders = { main: new CodeBuilder(), diff --git a/src/generators/dom/preprocess.js b/src/generators/dom/preprocess.js new file mode 100644 index 0000000000..5350642cbf --- /dev/null +++ b/src/generators/dom/preprocess.js @@ -0,0 +1,155 @@ +import Block from './Block.js'; + +function isElseIf ( node ) { + return node && node.children.length === 1 && node.children[0].type === 'IfBlock'; +} + +const preprocessors = { + MustacheTag: ( generator, block, node ) => { + const { dependencies } = block.contextualise( node.expression ); + block.addDependencies( dependencies ); + }, + + IfBlock: ( generator, block, node ) => { + function attachBlocks ( node ) { + const { dependencies } = block.contextualise( node.expression ); + block.addDependencies( dependencies ); + + node._block = block.child({ + name: generator.getUniqueName( `create_if_block` ) + }); + + preprocessChildren( generator, node._block, node.children ); + block.addDependencies( node._block.dependencies ); + + if ( isElseIf( node.else ) ) { + attachBlocks( node.else.children[0] ); + } else if ( node.else ) { + node.else._block = block.child({ + name: generator.getUniqueName( `create_if_block` ) + }); + + preprocessChildren( generator, node.else._block, node.else.children ); + block.addDependencies( node.else._block.dependencies ); + } + } + + attachBlocks( node ); + }, + + EachBlock: ( generator, block, node ) => { + const { dependencies } = block.contextualise( node.expression ); + block.addDependencies( dependencies ); + + const indexNames = new Map( block.indexNames ); + const indexName = node.index || block.getUniqueName( `${node.context}_index` ); + indexNames.set( node.context, indexName ); + + const listNames = new Map( block.listNames ); + const listName = block.getUniqueName( `each_block_value` ); + listNames.set( node.context, listName ); + + const context = generator.getUniqueName( node.context ); + const contexts = new Map( block.contexts ); + contexts.set( node.context, context ); + + const indexes = new Map( block.indexes ); + if ( node.index ) indexes.set( indexName, node.context ); + + const contextDependencies = new Map( block.contextDependencies ); + contextDependencies.set( node.context, dependencies ); + + node._block = block.child({ + name: generator.getUniqueName( 'create_each_block' ), + expression: node.expression, + context: node.context, + key: node.key, + + contextDependencies, + contexts, + indexes, + + listName, + indexName, + + indexNames, + listNames, + params: block.params.concat( listName, context, indexName ) + }); + + preprocessChildren( generator, node._block, node.children ); + block.addDependencies( node._block.dependencies ); + + if ( node.else ) { + node.else._block = block.child({ + name: generator.getUniqueName( `${node._block.name}_else` ) + }); + + preprocessChildren( generator, node.else._block, node.else.children ); + } + }, + + Element: ( generator, block, node ) => { + node.attributes.forEach( attribute => { + if ( attribute.type === 'Attribute' && attribute.value !== true ) { + attribute.value.forEach( chunk => { + if ( chunk.type !== 'Text' ) { + const { dependencies } = block.contextualise( chunk.expression ); + block.addDependencies( dependencies ); + } + }); + } + + else if ( attribute.type === 'Binding' ) { + const { dependencies } = block.contextualise( attribute.value ); + block.addDependencies( dependencies ); + } + }); + + const isComponent = generator.components.has( node.name ) || node.name === ':Self'; + + if ( isComponent ) { + const name = block.getUniqueName( ( node.name === ':Self' ? generator.name : node.name ).toLowerCase() ); + + node._block = block.child({ + name: generator.getUniqueName( `create_${name}_yield_fragment` ) + }); + + preprocessChildren( generator, node._block, node.children ); + } + + else { + preprocessChildren( generator, block, node.children ); + } + } +}; + +preprocessors.RawMustacheTag = preprocessors.MustacheTag; + +function preprocessChildren ( generator, block, children ) { + children.forEach( child => { + const preprocess = preprocessors[ child.type ]; + if ( preprocess ) preprocess( generator, block, child ); + }); +} + +export default function preprocess ( generator, children ) { + const block = new Block({ + generator, + name: generator.alias( 'create_main_fragment' ), + key: null, + + contexts: new Map(), + indexes: new Map(), + + params: [ 'root' ], + indexNames: new Map(), + listNames: new Map(), + + dependencies: new Set() + }); + + preprocessChildren( generator, block, children ); + + return block; +} \ No newline at end of file diff --git a/src/generators/dom/visitors/Component/Attribute.js b/src/generators/dom/visitors/Component/Attribute.js index 601bc7049f..8458eeb866 100644 --- a/src/generators/dom/visitors/Component/Attribute.js +++ b/src/generators/dom/visitors/Component/Attribute.js @@ -28,12 +28,12 @@ export default function visitAttribute ( generator, block, state, node, attribut else { // simple dynamic attributes - const { dependencies, string } = generator.contextualise( block, value.expression ); + const { dependencies, snippet } = block.contextualise( value.expression ); // TODO only update attributes that have changed local.dynamicAttributes.push({ name: attribute.name, - value: string, + value: snippet, dependencies }); } @@ -48,12 +48,12 @@ export default function visitAttribute ( generator, block, state, node, attribut if ( chunk.type === 'Text' ) { return JSON.stringify( chunk.data ); } else { - const { dependencies, string } = generator.contextualise( block, chunk.expression ); + const { dependencies, snippet } = block.contextualise( chunk.expression ); dependencies.forEach( dependency => { if ( !~allDependencies.indexOf( dependency ) ) allDependencies.push( dependency ); }); - return `( ${string} )`; + return `( ${snippet} )`; } }).join( ' + ' ) ); diff --git a/src/generators/dom/visitors/Component/Binding.js b/src/generators/dom/visitors/Component/Binding.js index f4b86425ec..7ad2e61cb2 100644 --- a/src/generators/dom/visitors/Component/Binding.js +++ b/src/generators/dom/visitors/Component/Binding.js @@ -4,7 +4,7 @@ import getSetter from '../shared/binding/getSetter.js'; export default function visitBinding ( generator, block, state, node, attribute, local ) { const { name, keypath } = flattenReference( attribute.value ); - const { snippet, contexts, dependencies } = generator.contextualise( block, attribute.value ); + const { snippet, contexts, dependencies } = block.contextualise( attribute.value ); if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' ); diff --git a/src/generators/dom/visitors/Component/Component.js b/src/generators/dom/visitors/Component/Component.js index 5e210e9762..fc2729d202 100644 --- a/src/generators/dom/visitors/Component/Component.js +++ b/src/generators/dom/visitors/Component/Component.js @@ -6,10 +6,6 @@ import visitEventHandler from './EventHandler.js'; import visitBinding from './Binding.js'; import visitRef from './Ref.js'; -function capDown ( name ) { - return `${name[0].toLowerCase()}${name.slice( 1 )}`; -} - function stringifyProps ( props ) { if ( !props.length ) return '{}'; @@ -38,7 +34,7 @@ const visitors = { export default function visitComponent ( generator, block, state, node ) { const hasChildren = node.children.length > 0; - const name = block.getUniqueName( capDown( node.name === ':Self' ? generator.name : node.name ) ); + const name = block.getUniqueName( ( node.name === ':Self' ? generator.name : node.name ).toLowerCase() ); const childState = Object.assign( {}, state, { parentNode: null @@ -105,9 +101,7 @@ export default function visitComponent ( generator, block, state, node ) { if ( hasChildren ) { const params = block.params.join( ', ' ); - const childBlock = block.child({ - name: generator.getUniqueName( `create_${name}_yield_fragment` ) // TODO should getUniqueName happen inside Fragment? probably - }); + const childBlock = node._block; node.children.forEach( child => { visit( generator, childBlock, childState, child ); @@ -162,7 +156,7 @@ export default function visitComponent ( generator, block, state, node ) { ` ); if ( isToplevel ) { - block.builders.mount.addLine( `${name}._fragment.mount( target, anchor );` ); + block.builders.mount.addLine( `${name}._fragment.mount( ${block.target}, anchor );` ); } if ( local.dynamicAttributes.length ) { diff --git a/src/generators/dom/visitors/Component/EventHandler.js b/src/generators/dom/visitors/Component/EventHandler.js index 263f88e4ad..7e2ed9b0da 100644 --- a/src/generators/dom/visitors/Component/EventHandler.js +++ b/src/generators/dom/visitors/Component/EventHandler.js @@ -7,7 +7,7 @@ export default function visitEventHandler ( generator, block, state, node, attri const usedContexts = []; attribute.expression.arguments.forEach( arg => { - const { contexts } = generator.contextualise( block, arg, null, true ); + const { contexts } = block.contextualise( arg, null, true ); contexts.forEach( context => { if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context ); @@ -31,5 +31,5 @@ export default function visitEventHandler ( generator, block, state, node, attri ${local.name}.on( '${attribute.name}', function ( event ) { ${handlerBody} }); - ` ); + ` ); } \ No newline at end of file diff --git a/src/generators/dom/visitors/EachBlock.js b/src/generators/dom/visitors/EachBlock.js index f11b2fb929..e352d2cf85 100644 --- a/src/generators/dom/visitors/EachBlock.js +++ b/src/generators/dom/visitors/EachBlock.js @@ -3,219 +3,214 @@ import deindent from '../../../utils/deindent.js'; import visit from '../visit.js'; export default function visitEachBlock ( generator, block, state, node ) { - const name = generator.getUniqueName( `each_block` ); - const renderer = generator.getUniqueName( `create_each_block` ); - const elseName = generator.getUniqueName( `${name}_else` ); - const renderElse = generator.getUniqueName( `${renderer}_else` ); + const each_block = generator.getUniqueName( `each_block` ); + const create_each_block = node._block.name; + const each_block_value = node._block.listName; + const iterations = block.getUniqueName( `${each_block}_iterations` ); const i = block.getUniqueName( `i` ); const params = block.params.join( ', ' ); + const anchor = block.getUniqueName( `${each_block}_anchor` ); - const listName = block.getUniqueName( `${name}_value` ); + const vars = { each_block, create_each_block, each_block_value, iterations, i, params, anchor }; - const isToplevel = !state.parentNode; - - const { dependencies, snippet } = generator.contextualise( block, node.expression ); + const { snippet } = block.contextualise( node.expression ); - const anchor = block.getUniqueName( `${name}_anchor` ); block.createAnchor( anchor, state.parentNode ); - - const localVars = {}; - - localVars.iteration = block.getUniqueName( `${name}_iteration` ); - localVars.iterations = block.getUniqueName( `${name}_iterations` ); - localVars._iterations = block.getUniqueName( `_${name}_iterations` ); - localVars.lookup = block.getUniqueName( `${name}_lookup` ); - localVars._lookup = block.getUniqueName( `_${name}_lookup` ); - - block.builders.create.addLine( `var ${listName} = ${snippet};` ); - block.builders.create.addLine( `var ${localVars.iterations} = [];` ); - if ( node.key ) block.builders.create.addLine( `var ${localVars.lookup} = Object.create( null );` ); - if ( node.else ) block.builders.create.addLine( `var ${elseName} = null;` ); - - const initialRender = new CodeBuilder(); + block.builders.create.addLine( `var ${each_block_value} = ${snippet};` ); + block.builders.create.addLine( `var ${iterations} = [];` ); if ( node.key ) { - localVars.fragment = block.getUniqueName( 'fragment' ); - localVars.value = block.getUniqueName( 'value' ); - localVars.key = block.getUniqueName( 'key' ); - - initialRender.addBlock( deindent` - var ${localVars.key} = ${listName}[${i}].${node.key}; - ${localVars.iterations}[${i}] = ${localVars.lookup}[ ${localVars.key} ] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${block.component}${node.key ? `, ${localVars.key}` : `` } ); - ` ); + keyed( generator, block, state, node, snippet, vars ); } else { - initialRender.addLine( - `${localVars.iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${block.component} );` - ); + unkeyed( generator, block, state, node, snippet, vars ); } - if ( !isToplevel ) { - initialRender.addLine( - `${localVars.iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} );` - ); + const isToplevel = !state.parentNode; + + if ( isToplevel ) { + block.builders.mount.addBlock( deindent` + for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) { + ${iterations}[${i}].mount( ${block.target}, ${anchor} ); + } + ` ); } - block.builders.create.addBlock( deindent` - for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) { - ${initialRender} - } - ` ); + block.builders.destroy.addBlock( + `${generator.helper( 'destroyEach' )}( ${iterations}, ${isToplevel ? 'detach' : 'false'}, 0 );` ); if ( node.else ) { + const each_block_else = generator.getUniqueName( `${each_block}_else` ); + + block.builders.create.addLine( `var ${each_block_else} = null;` ); + + // TODO neaten this up... will end up with an empty line in the block block.builders.create.addBlock( deindent` - if ( !${listName}.length ) { - ${elseName} = ${renderElse}( ${params}, ${block.component} ); - ${!isToplevel ? `${elseName}.mount( ${anchor}.parentNode, ${anchor} );` : ''} + if ( !${each_block_value}.length ) { + ${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} ); + ${!isToplevel ? `${each_block_else}.mount( ${state.parentNode}, ${anchor} );` : ''} } ` ); - } - if ( isToplevel ) { block.builders.mount.addBlock( deindent` - for ( var ${i} = 0; ${i} < ${localVars.iterations}.length; ${i} += 1 ) { - ${localVars.iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} ); + if ( ${each_block_else} ) { + ${each_block_else}.mount( ${state.parentNode || block.target}, ${anchor} ); } ` ); - if ( node.else ) { - block.builders.mount.addBlock( deindent` - if ( ${elseName} ) { - ${elseName}.mount( ${anchor}.parentNode, ${anchor} ); - } - ` ); - } - } - if ( node.key ) { block.builders.update.addBlock( deindent` - var ${listName} = ${snippet}; - var ${localVars._iterations} = []; - var ${localVars._lookup} = Object.create( null ); - - var ${localVars.fragment} = document.createDocumentFragment(); - - // create new iterations as necessary - for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) { - var ${localVars.value} = ${listName}[${i}]; - var ${localVars.key} = ${localVars.value}.${node.key}; - - if ( ${localVars.lookup}[ ${localVars.key} ] ) { - ${localVars._iterations}[${i}] = ${localVars._lookup}[ ${localVars.key} ] = ${localVars.lookup}[ ${localVars.key} ]; - ${localVars._lookup}[ ${localVars.key} ].update( changed, ${params}, ${listName}, ${listName}[${i}], ${i} ); - } else { - ${localVars._iterations}[${i}] = ${localVars._lookup}[ ${localVars.key} ] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${block.component}${node.key ? `, ${localVars.key}` : `` } ); - } - - ${localVars._iterations}[${i}].mount( ${localVars.fragment}, null ); + if ( !${each_block_value}.length && ${each_block_else} ) { + ${each_block_else}.update( changed, ${params} ); + } else if ( !${each_block_value}.length ) { + ${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} ); + ${each_block_else}.mount( ${anchor}.parentNode, ${anchor} ); + } else if ( ${each_block_else} ) { + ${each_block_else}.destroy( true ); } + ` ); - // remove old iterations - for ( var ${i} = 0; ${i} < ${localVars.iterations}.length; ${i} += 1 ) { - var ${localVars.iteration} = ${localVars.iterations}[${i}]; - if ( !${localVars._lookup}[ ${localVars.iteration}.key ] ) { - ${localVars.iteration}.destroy( true ); - } + block.builders.destroy.addBlock( deindent` + if ( ${each_block_else} ) { + ${each_block_else}.destroy( ${isToplevel ? 'detach' : 'false'} ); } + ` ); + } - ${anchor}.parentNode.insertBefore( ${localVars.fragment}, ${anchor} ); + const childBlock = node._block; - ${localVars.iterations} = ${localVars._iterations}; - ${localVars.lookup} = ${localVars._lookup}; - ` ); - } else { - block.builders.update.addBlock( deindent` - var ${listName} = ${snippet}; - - for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) { - if ( !${localVars.iterations}[${i}] ) { - ${localVars.iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${block.component} ); - ${localVars.iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} ); - } else { - ${localVars.iterations}[${i}].update( changed, ${params}, ${listName}, ${listName}[${i}], ${i} ); - } - } + const childState = Object.assign( {}, state, { + parentNode: null, + inEachBlock: true + }); - ${generator.helper( 'destroyEach' )}( ${localVars.iterations}, true, ${listName}.length ); + node.children.forEach( child => { + visit( generator, childBlock, childState, child ); + }); - ${localVars.iterations}.length = ${listName}.length; - ` ); - } + generator.addBlock( childBlock ); if ( node.else ) { - block.builders.update.addBlock( deindent` - if ( !${listName}.length && ${elseName} ) { - ${elseName}.update( changed, ${params} ); - } else if ( !${listName}.length ) { - ${elseName} = ${renderElse}( ${params}, ${block.component} ); - ${elseName}.mount( ${anchor}.parentNode, ${anchor} ); - } else if ( ${elseName} ) { - ${elseName}.destroy( true ); - } - ` ); + node.else.children.forEach( child => { + visit( generator, node.else._block, childState, child ); + }); + + generator.addBlock( node.else._block ); } +} - block.builders.destroy.addBlock( - `${generator.helper( 'destroyEach' )}( ${localVars.iterations}, ${isToplevel ? 'detach' : 'false'}, 0 );` ); +function keyed ( generator, block, state, node, snippet, { each_block, create_each_block, each_block_value, iterations, i, params, anchor } ) { + const fragment = block.getUniqueName( 'fragment' ); + const value = block.getUniqueName( 'value' ); + const key = block.getUniqueName( 'key' ); + const lookup = block.getUniqueName( `${each_block}_lookup` ); + const _lookup = block.getUniqueName( `_${each_block}_lookup` ); + const iteration = block.getUniqueName( `${each_block}_iteration` ); + const _iterations = block.getUniqueName( `_${each_block}_iterations` ); - if ( node.else ) { - block.builders.destroy.addBlock( deindent` - if ( ${elseName} ) { - ${elseName}.destroy( ${isToplevel ? 'detach' : 'false'} ); - } - ` ); + block.builders.create.addLine( `var ${lookup} = Object.create( null );` ); + + const create = new CodeBuilder(); + + create.addBlock( deindent` + var ${key} = ${each_block_value}[${i}].${node.key}; + ${iterations}[${i}] = ${lookup}[ ${key} ] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}${node.key ? `, ${key}` : `` } ); + ` ); + + if ( state.parentNode ) { + create.addLine( + `${iterations}[${i}].mount( ${state.parentNode}, ${anchor} );` + ); } - const indexNames = new Map( block.indexNames ); - const indexName = node.index || block.getUniqueName( `${node.context}_index` ); - indexNames.set( node.context, indexName ); + block.builders.create.addBlock( deindent` + for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) { + ${create} + } + ` ); - const listNames = new Map( block.listNames ); - listNames.set( node.context, listName ); + block.builders.update.addBlock( deindent` + var ${each_block_value} = ${snippet}; + var ${_iterations} = []; + var ${_lookup} = Object.create( null ); - const context = generator.getUniqueName( node.context ); - const contexts = new Map( block.contexts ); - contexts.set( node.context, context ); + var ${fragment} = document.createDocumentFragment(); - const indexes = new Map( block.indexes ); - if ( node.index ) indexes.set( indexName, node.context ); + // create new iterations as necessary + for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) { + var ${value} = ${each_block_value}[${i}]; + var ${key} = ${value}.${node.key}; - const contextDependencies = new Map( block.contextDependencies ); - contextDependencies.set( node.context, dependencies ); + if ( ${lookup}[ ${key} ] ) { + ${_iterations}[${i}] = ${_lookup}[ ${key} ] = ${lookup}[ ${key} ]; + ${_lookup}[ ${key} ].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} ); + } else { + ${_iterations}[${i}] = ${_lookup}[ ${key} ] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}${node.key ? `, ${key}` : `` } ); + } - const childBlock = block.child({ - name: renderer, - expression: node.expression, - context: node.context, - key: node.key, + ${_iterations}[${i}].mount( ${fragment}, null ); + } - contextDependencies, - contexts, - indexes, + // remove old iterations + for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) { + var ${iteration} = ${iterations}[${i}]; + if ( !${_lookup}[ ${iteration}.key ] ) { + ${iteration}.destroy( true ); + } + } - indexNames, - listNames, - params: block.params.concat( listName, context, indexName ) - }); + ${anchor}.parentNode.insertBefore( ${fragment}, ${anchor} ); - const childState = Object.assign( {}, state, { - parentNode: null, - inEachBlock: true - }); + ${iterations} = ${_iterations}; + ${lookup} = ${_lookup}; + ` ); +} - node.children.forEach( child => { - visit( generator, childBlock, childState, child ); +function unkeyed ( generator, block, state, node, snippet, { create_each_block, each_block_value, iterations, i, params, anchor } ) { + const create = new CodeBuilder(); + + create.addLine( + `${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );` + ); + + if ( state.parentNode ) { + create.addLine( + `${iterations}[${i}].mount( ${state.parentNode}, ${anchor} );` + ); + } + + block.builders.create.addBlock( deindent` + for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) { + ${create} + } + ` ); + + const { dependencies } = block.contextualise( node.expression ); + const allDependencies = new Set( block.dependencies ); + dependencies.forEach( dependency => { + allDependencies.add( dependency ); }); - generator.addBlock( childBlock ); + const condition = Array.from( allDependencies ) + .map( dependency => `'${dependency}' in changed` ) + .join( ' || ' ); - if ( node.else ) { - const childBlock = block.child({ - name: renderElse - }); + if ( condition !== '' ) { + block.builders.update.addBlock( deindent` + var ${each_block_value} = ${snippet}; + + if ( ${condition} ) { + for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) { + if ( !${iterations}[${i}] ) { + ${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} ); + ${iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} ); + } else { + ${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} ); + } + } - node.else.children.forEach( child => { - visit( generator, childBlock, childState, child ); - }); + ${generator.helper( 'destroyEach' )}( ${iterations}, true, ${each_block_value}.length ); - generator.addBlock( childBlock ); + ${iterations}.length = ${each_block_value}.length; + } + ` ); } } \ No newline at end of file diff --git a/src/generators/dom/visitors/Element/Attribute.js b/src/generators/dom/visitors/Element/Attribute.js index b4ecf58d7f..3b9a3cc299 100644 --- a/src/generators/dom/visitors/Element/Attribute.js +++ b/src/generators/dom/visitors/Element/Attribute.js @@ -27,7 +27,7 @@ export default function visitAttribute ( generator, block, state, node, attribut if ( attribute.value.length === 1 ) { // single {{tag}} — may be a non-string - const { snippet } = generator.contextualise( block, attribute.value[0].expression ); + const { snippet } = block.contextualise( attribute.value[0].expression ); value = snippet; } else { // '{{foo}} {{bar}}' — treat as string concatenation @@ -36,7 +36,7 @@ export default function visitAttribute ( generator, block, state, node, attribut if ( chunk.type === 'Text' ) { return JSON.stringify( chunk.data ); } else { - const { snippet } = generator.contextualise( block, chunk.expression ); + const { snippet } = block.contextualise( chunk.expression ); return `( ${snippet} )`; } }).join( ' + ' ) diff --git a/src/generators/dom/visitors/Element/Binding.js b/src/generators/dom/visitors/Element/Binding.js index c9e2c66cbe..7e8f38b947 100644 --- a/src/generators/dom/visitors/Element/Binding.js +++ b/src/generators/dom/visitors/Element/Binding.js @@ -5,7 +5,7 @@ import getStaticAttributeValue from './getStaticAttributeValue.js'; export default function visitBinding ( generator, block, state, node, attribute ) { const { name, keypath } = flattenReference( attribute.value ); - const { snippet, contexts, dependencies } = generator.contextualise( block, attribute.value ); + const { snippet, contexts, dependencies } = block.contextualise( attribute.value ); if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' ); diff --git a/src/generators/dom/visitors/IfBlock.js b/src/generators/dom/visitors/IfBlock.js index 837fa559fe..5ff5f0565d 100644 --- a/src/generators/dom/visitors/IfBlock.js +++ b/src/generators/dom/visitors/IfBlock.js @@ -1,39 +1,38 @@ import deindent from '../../../utils/deindent.js'; import visit from '../visit.js'; -function getConditionsAndBlocks ( generator, block, state, node, _name, i = 0 ) { - const name = generator.getUniqueName( `${_name}_${i}` ); +function isElseIf ( node ) { + return node && node.children.length === 1 && node.children[0].type === 'IfBlock'; +} +function getConditionsAndBlocks ( generator, block, state, node ) { const conditionsAndBlocks = [{ - condition: generator.contextualise( block, node.expression ).snippet, - block: name + condition: block.contextualise( node.expression ).snippet, + block: node._block.name }]; - generateBlock( generator, block, state, node, name ); + generateBlock( generator, block, state, node ); - if ( node.else && node.else.children.length === 1 && - node.else.children[0].type === 'IfBlock' ) { + if ( isElseIf( node.else ) ) { conditionsAndBlocks.push( - ...getConditionsAndBlocks( generator, block, state, node.else.children[0], _name, i + 1 ) + ...getConditionsAndBlocks( generator, block, state, node.else.children[0] ) ); } else { - const name = generator.getUniqueName( `${_name}_${i + 1}` ); conditionsAndBlocks.push({ condition: null, - block: node.else ? name : null, + block: node.else ? node.else._block.name : null, }); if ( node.else ) { - generateBlock( generator, block, state, node.else, name ); + generateBlock( generator, block, state, node.else ); } } + return conditionsAndBlocks; } -function generateBlock ( generator, block, state, node, name ) { - const childBlock = block.child({ - name - }); +function generateBlock ( generator, block, state, node ) { + const childBlock = node._block; const childState = Object.assign( {}, state, { parentNode: null @@ -53,7 +52,6 @@ export default function visitIfBlock ( generator, block, state, node ) { const currentBlock = block.getUniqueName( `current_block` ); const _currentBlock = block.getUniqueName( `_current_block` ); - const isToplevel = !state.parentNode; const conditionsAndBlocks = getConditionsAndBlocks( generator, block, state, node, generator.getUniqueName( `create_if_block` ) ); const anchor = `${name}_anchor`; @@ -70,11 +68,12 @@ export default function visitIfBlock ( generator, block, state, node ) { var ${name} = ${currentBlock} && ${currentBlock}( ${params}, ${block.component} ); ` ); - const mountStatement = `if ( ${name} ) ${name}.mount( ${anchor}.parentNode, ${anchor} );`; + const isToplevel = !state.parentNode; + if ( isToplevel ) { - block.builders.mount.addLine( mountStatement ); + block.builders.mount.addLine( `if ( ${name} ) ${name}.mount( ${block.target}, ${anchor} );` ); } else { - block.builders.create.addLine( mountStatement ); + block.builders.create.addLine( `if ( ${name} ) ${name}.mount( ${state.parentNode}, ${anchor} );` ); } block.builders.update.addBlock( deindent` diff --git a/src/generators/dom/visitors/MustacheTag.js b/src/generators/dom/visitors/MustacheTag.js index e6b6ee593a..60e64e1f8e 100644 --- a/src/generators/dom/visitors/MustacheTag.js +++ b/src/generators/dom/visitors/MustacheTag.js @@ -3,7 +3,7 @@ import deindent from '../../../utils/deindent.js'; export default function visitMustacheTag ( generator, block, state, node ) { const name = block.getUniqueName( 'text' ); - const { snippet } = generator.contextualise( block, node.expression ); + const { snippet } = block.contextualise( node.expression ); block.builders.create.addLine( `var last_${name} = ${snippet};` ); block.addElement( name, `${generator.helper( 'createText' )}( last_${name} )`, state.parentNode, true ); diff --git a/src/generators/dom/visitors/RawMustacheTag.js b/src/generators/dom/visitors/RawMustacheTag.js index 13a9398056..a99d7cba8f 100644 --- a/src/generators/dom/visitors/RawMustacheTag.js +++ b/src/generators/dom/visitors/RawMustacheTag.js @@ -3,7 +3,7 @@ import deindent from '../../../utils/deindent.js'; export default function visitRawMustacheTag ( generator, block, state, node ) { const name = block.getUniqueName( 'raw' ); - const { snippet } = generator.contextualise( block, node.expression ); + const { snippet } = block.contextualise( node.expression ); // we would have used comments here, but the `insertAdjacentHTML` api only // exists for `Element`s. diff --git a/src/generators/dom/visitors/YieldTag.js b/src/generators/dom/visitors/YieldTag.js index f64b12f92c..2ee3105669 100644 --- a/src/generators/dom/visitors/YieldTag.js +++ b/src/generators/dom/visitors/YieldTag.js @@ -3,7 +3,7 @@ export default function visitYieldTag ( generator, block, state ) { block.createAnchor( anchor, state.parentNode ); block.builders.mount.addLine( - `${block.component}._yield && ${block.component}._yield.mount( ${state.parentNode || 'target'}, ${anchor} );` + `${block.component}._yield && ${block.component}._yield.mount( ${state.parentNode || block.target}, ${anchor} );` ); block.builders.destroy.addLine( diff --git a/src/generators/server-side-rendering/Block.js b/src/generators/server-side-rendering/Block.js index 9f9a213453..ca08a529e4 100644 --- a/src/generators/server-side-rendering/Block.js +++ b/src/generators/server-side-rendering/Block.js @@ -27,4 +27,8 @@ export default class Block { child ( options ) { return new Block( Object.assign( {}, this, options, { parent: this } ) ); } + + contextualise ( expression, context, isEventHandler ) { + return this.generator.contextualise( this, expression, context, isEventHandler ); + } } \ No newline at end of file diff --git a/src/generators/server-side-rendering/visitors/Component.js b/src/generators/server-side-rendering/visitors/Component.js index 2c718484c5..fccc7d6bc0 100644 --- a/src/generators/server-side-rendering/visitors/Component.js +++ b/src/generators/server-side-rendering/visitors/Component.js @@ -5,7 +5,7 @@ export default function visitComponent ( generator, block, node ) { function stringify ( chunk ) { if ( chunk.type === 'Text' ) return chunk.data; if ( chunk.type === 'MustacheTag' ) { - const { snippet } = generator.contextualise( block, chunk.expression ); + const { snippet } = block.contextualise( chunk.expression ); return '${__escape( ' + snippet + ')}'; } } @@ -34,7 +34,7 @@ export default function visitComponent ( generator, block, node ) { if ( chunk.type === 'Text' ) { value = isNaN( chunk.data ) ? JSON.stringify( chunk.data ) : chunk.data; } else { - const { snippet } = generator.contextualise( block, chunk.expression ); + const { snippet } = block.contextualise( chunk.expression ); value = snippet; } } else { diff --git a/src/generators/server-side-rendering/visitors/EachBlock.js b/src/generators/server-side-rendering/visitors/EachBlock.js index 5e59a10a4c..b26bbb4222 100644 --- a/src/generators/server-side-rendering/visitors/EachBlock.js +++ b/src/generators/server-side-rendering/visitors/EachBlock.js @@ -1,7 +1,7 @@ import visit from '../visit.js'; export default function visitEachBlock ( generator, block, node ) { - const { dependencies, snippet } = generator.contextualise( block, node.expression ); + const { dependencies, snippet } = block.contextualise( node.expression ); const open = `\${ ${snippet}.map( ${ node.index ? `( ${node.context}, ${node.index} )` : node.context} => \``; generator.append( open ); diff --git a/src/generators/server-side-rendering/visitors/Element.js b/src/generators/server-side-rendering/visitors/Element.js index c473d6b62c..0a6c5f42c4 100644 --- a/src/generators/server-side-rendering/visitors/Element.js +++ b/src/generators/server-side-rendering/visitors/Element.js @@ -30,7 +30,7 @@ export default function visitElement ( generator, block, node ) { return chunk.data; } - const { snippet } = generator.contextualise( block, chunk.expression ); + const { snippet } = block.contextualise( chunk.expression ); return '${' + snippet + '}'; }).join( '' ) + `"`; } diff --git a/src/generators/server-side-rendering/visitors/IfBlock.js b/src/generators/server-side-rendering/visitors/IfBlock.js index cd2575be6e..eb2af2caae 100644 --- a/src/generators/server-side-rendering/visitors/IfBlock.js +++ b/src/generators/server-side-rendering/visitors/IfBlock.js @@ -1,7 +1,7 @@ import visit from '../visit.js'; export default function visitIfBlock ( generator, block, node ) { - const { snippet } = generator.contextualise( block, node.expression ); + const { snippet } = block.contextualise( node.expression ); generator.append( '${ ' + snippet + ' ? `' ); diff --git a/src/generators/server-side-rendering/visitors/MustacheTag.js b/src/generators/server-side-rendering/visitors/MustacheTag.js index d433d0f1cb..4135ef9203 100644 --- a/src/generators/server-side-rendering/visitors/MustacheTag.js +++ b/src/generators/server-side-rendering/visitors/MustacheTag.js @@ -1,4 +1,4 @@ export default function visitMustacheTag ( generator, block, node ) { - const { snippet } = generator.contextualise( block, node.expression ); + const { snippet } = block.contextualise( node.expression ); generator.append( '${__escape( ' + snippet + ' )}' ); } \ No newline at end of file diff --git a/src/generators/server-side-rendering/visitors/RawMustacheTag.js b/src/generators/server-side-rendering/visitors/RawMustacheTag.js index 27aa695fa1..93b1f3c5f8 100644 --- a/src/generators/server-side-rendering/visitors/RawMustacheTag.js +++ b/src/generators/server-side-rendering/visitors/RawMustacheTag.js @@ -1,4 +1,4 @@ export default function visitRawMustacheTag ( generator, block, node ) { - const { snippet } = generator.contextualise( block, node.expression ); + const { snippet } = block.contextualise( node.expression ); generator.append( '${' + snippet + '}' ); } \ No newline at end of file diff --git a/test/js/index.js b/test/js/index.js index fd5eb102bf..bd3e9cbc6d 100644 --- a/test/js/index.js +++ b/test/js/index.js @@ -18,9 +18,16 @@ describe( 'js', () => { dir = path.resolve( 'test/js/samples', dir ); const input = fs.readFileSync( `${dir}/input.html`, 'utf-8' ).replace( /\s+$/, '' ); - const actual = svelte.compile( input, { - shared: true - }).code; + let actual; + + try { + actual = svelte.compile( input, { + shared: true + }).code; + } catch ( err ) { + console.log( err.frame ); + throw err; + } fs.writeFileSync( `${dir}/_actual.js`, actual ); const expected = fs.readFileSync( `${dir}/expected.js`, 'utf-8' ); diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js new file mode 100644 index 0000000000..8b3edeb87e --- /dev/null +++ b/test/js/samples/each-block-changed-check/expected.js @@ -0,0 +1,146 @@ +import { appendNode, assign, createComment, createElement, createText, destroyEach, detachBetween, detachNode, dispatchObservers, insertNode, proto } from "svelte/shared.js"; + +function create_main_fragment ( root, component ) { + var each_block_anchor = createComment(); + var each_block_value = root.comments; + var each_block_iterations = []; + + for ( var i = 0; i < each_block_value.length; i += 1 ) { + each_block_iterations[i] = create_each_block( root, each_block_value, each_block_value[i], i, component ); + } + + return { + mount: function ( target, anchor ) { + insertNode( each_block_anchor, target, anchor ); + + for ( var i = 0; i < each_block_iterations.length; i += 1 ) { + each_block_iterations[i].mount( target, each_block_anchor ); + } + }, + + update: function ( changed, root ) { + var each_block_value = root.comments; + + if ( 'comments' in changed || 'elapsed' in changed || 'time' in changed ) { + for ( var i = 0; i < each_block_value.length; i += 1 ) { + if ( !each_block_iterations[i] ) { + each_block_iterations[i] = create_each_block( root, each_block_value, each_block_value[i], i, component ); + each_block_iterations[i].mount( each_block_anchor.parentNode, each_block_anchor ); + } else { + each_block_iterations[i].update( changed, root, each_block_value, each_block_value[i], i ); + } + } + + destroyEach( each_block_iterations, true, each_block_value.length ); + + each_block_iterations.length = each_block_value.length; + } + }, + + destroy: function ( detach ) { + destroyEach( each_block_iterations, detach, 0 ); + + if ( detach ) { + detachNode( each_block_anchor ); + } + } + }; +} + +function create_each_block ( root, each_block_value, comment, comment_index, component ) { + var div = createElement( 'div' ); + div.className = "comment"; + var span = createElement( 'span' ); + appendNode( span, div ); + span.className = "meta"; + var last_text = comment.author; + var text = createText( last_text ); + appendNode( text, span ); + appendNode( createText( " wrote " ), span ); + var last_text_2 = root.elapsed(comment.time, root.time); + var text_2 = createText( last_text_2 ); + appendNode( text_2, span ); + appendNode( createText( " ago:" ), span ); + appendNode( createText( "\n\n\t\t" ), div ); + var raw_before = createElement( 'noscript' ); + appendNode( raw_before, div ); + var raw_after = createElement( 'noscript' ); + appendNode( raw_after, div ); + var last_raw = comment.html; + raw_before.insertAdjacentHTML( 'afterend', last_raw ); + + return { + mount: function ( target, anchor ) { + insertNode( div, target, anchor ); + }, + + update: function ( changed, root, each_block_value, comment, comment_index ) { + var tmp; + + if ( ( tmp = comment.author ) !== last_text ) { + text.data = last_text = tmp; + } + + if ( ( tmp = root.elapsed(comment.time, root.time) ) !== last_text_2 ) { + text_2.data = last_text_2 = tmp; + } + + if ( ( tmp = comment.html ) !== last_raw ) { + last_raw = tmp; + detachBetween( raw_before, raw_after ); + raw_before.insertAdjacentHTML( 'afterend', last_raw ); + } + }, + + destroy: function ( detach ) { + if ( detach ) { + detachBetween( raw_before, raw_after ); + + detachNode( div ); + } + } + }; +} + +function SvelteComponent ( options ) { + options = options || {}; + this._state = options.data || {}; + + this._observers = { + pre: Object.create( null ), + post: Object.create( null ) + }; + + this._handlers = Object.create( null ); + + this._root = options._root; + this._yield = options._yield; + + this._torndown = false; + + this._fragment = create_main_fragment( this._state, this ); + if ( options.target ) this._fragment.mount( options.target, null ); +} + +assign( SvelteComponent.prototype, proto ); + +SvelteComponent.prototype._set = function _set ( newState ) { + var oldState = this._state; + this._state = assign( {}, oldState, newState ); + + dispatchObservers( this, this._observers.pre, newState, oldState ); + if ( this._fragment ) this._fragment.update( newState, this._state ); + dispatchObservers( this, this._observers.post, newState, oldState ); +}; + +SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) { + this.fire( 'destroy' ); + + this._fragment.destroy( detach !== false ); + this._fragment = null; + + this._state = {}; + this._torndown = true; +}; + +export default SvelteComponent; \ No newline at end of file diff --git a/test/js/samples/each-block-changed-check/input.html b/test/js/samples/each-block-changed-check/input.html new file mode 100644 index 0000000000..fd7b4746dc --- /dev/null +++ b/test/js/samples/each-block-changed-check/input.html @@ -0,0 +1,9 @@ +{{#each comments as comment}} +