From ec0e4a62cfe981a0d4df137adaa6d36234baecb4 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Mon, 1 May 2017 10:53:56 -0400 Subject: [PATCH] support transitions in compound if-blocks --- src/generators/dom/Block.js | 18 +++- src/generators/dom/preprocess.js | 7 ++ src/generators/dom/visitors/IfBlock.js | 139 ++++++++++++++++++++----- 3 files changed, 132 insertions(+), 32 deletions(-) diff --git a/src/generators/dom/Block.js b/src/generators/dom/Block.js index c8327e0b5d..922306d8f3 100644 --- a/src/generators/dom/Block.js +++ b/src/generators/dom/Block.js @@ -31,6 +31,8 @@ export default class Block { destroy: new CodeBuilder() }; + this.hasIntroMethod = false; // a block could have an intro method but not intro transitions, e.g. if a sibling block has intros + this.hasOutroMethod = false; this.hasIntroTransitions = false; this.hasOutroTransitions = false; this.outros = 0; @@ -175,9 +177,13 @@ export default class Block { } } - if ( this.hasIntroTransitions ) { + if ( this.hasIntroMethod ) { if ( this.builders.intro.isEmpty() ) { - properties.addBlock( `intro: ${this.generator.helper( 'noop' )},` ); + properties.addBlock( deindent` + intro: function ( ${this.target}, anchor ) { + this.mount( ${this.target}, anchor ); + }, + ` ); } else { properties.addBlock( deindent` intro: function ( ${this.target}, anchor ) { @@ -193,9 +199,13 @@ export default class Block { } } - if ( this.hasOutroTransitions ) { + if ( this.hasOutroMethod ) { if ( this.builders.outro.isEmpty() ) { - properties.addBlock( `outro: ${this.generator.helper( 'noop' )},` ); + properties.addBlock( deindent` + outro: function ( outrocallback ) { + outrocallback(); + }, + ` ); } else { properties.addBlock( deindent` outro: function ( ${this.alias( 'outrocallback' )} ) { diff --git a/src/generators/dom/preprocess.js b/src/generators/dom/preprocess.js index 1c60a72cdb..6b43f93460 100644 --- a/src/generators/dom/preprocess.js +++ b/src/generators/dom/preprocess.js @@ -59,6 +59,8 @@ const preprocessors = { IfBlock: ( generator, block, state, node ) => { const blocks = []; let dynamic = false; + let hasIntros = false; + let hasOutros = false; function attachBlocks ( node ) { const dependencies = block.findDependencies( node.expression ); @@ -78,6 +80,9 @@ const preprocessors = { block.addDependencies( node._block.dependencies ); } + if ( node._block.hasIntroTransitions ) hasIntros = true; + if ( node._block.hasOutroTransitions ) hasOutros = true; + if ( isElseIf( node.else ) ) { attachBlocks( node.else.children[0] ); } else if ( node.else ) { @@ -101,6 +106,8 @@ const preprocessors = { blocks.forEach( block => { block.hasUpdateMethod = dynamic; + block.hasIntroMethod = hasIntros; + block.hasOutroMethod = hasOutros; }); generator.blocks.push( ...blocks ); diff --git a/src/generators/dom/visitors/IfBlock.js b/src/generators/dom/visitors/IfBlock.js index 28b74fa5ff..11b267717c 100644 --- a/src/generators/dom/visitors/IfBlock.js +++ b/src/generators/dom/visitors/IfBlock.js @@ -9,9 +9,9 @@ function getBranches ( generator, block, state, node ) { const branches = [{ condition: block.contextualise( node.expression ).snippet, block: node._block.name, - dynamic: node._block.dependencies.size > 0, - hasIntroTransitions: node._block.hasIntroTransitions, - hasOutroTransitions: node._block.hasOutroTransitions + hasUpdateMethod: node._block.hasUpdateMethod, + hasIntroMethod: node._block.hasIntroMethod, + hasOutroMethod: node._block.hasOutroMethod }]; visitChildren( generator, block, state, node ); @@ -24,9 +24,9 @@ function getBranches ( generator, block, state, node ) { branches.push({ condition: null, block: node.else ? node.else._block.name : null, - dynamic: node.else ? node.else._block.dependencies.size > 0 : false, - hasIntroTransitions: node.else ? node.else._block.hasIntroTransitions : false, - hasOutroTransitions: node.else ? node.else._block.hasOutroTransitions : false + hasUpdateMethod: node.else ? node.else._block.hasUpdateMethod : false, + hasIntroMethod: node.else ? node.else._block.hasIntroMethod : false, + hasOutroMethod: node.else ? node.else._block.hasOutroMethod : false }); if ( node.else ) { @@ -57,10 +57,15 @@ export default function visitIfBlock ( generator, block, state, node ) { } const branches = getBranches( generator, block, state, node, generator.getUniqueName( `create_if_block` ) ); - const dynamic = branches.some( branch => branch.dynamic ); + const dynamic = branches[0].hasUpdateMethod; // can use [0] as proxy for all, since they necessarily have the same value + const hasOutros = branches[0].hasOutroMethod; if ( node.else ) { - compound( generator, block, state, node, branches, dynamic, vars ); + if ( hasOutros ) { + compoundWithOutros( generator, block, state, node, branches, dynamic, vars ); + } else { + compound( generator, block, state, node, branches, dynamic, vars ); + } } else { simple( generator, block, state, node, branches[0], dynamic, vars ); } @@ -76,7 +81,7 @@ function simple ( generator, block, state, node, branch, dynamic, { name, anchor ` ); const isToplevel = !state.parentNode; - const mountOrIntro = branch.hasIntroTransitions ? 'intro' : 'mount'; + const mountOrIntro = branch.hasIntroMethod ? 'intro' : 'mount'; if ( isToplevel ) { block.builders.mount.addLine( `if ( ${name} ) ${name}.${mountOrIntro}( ${block.target}, null );` ); @@ -87,7 +92,7 @@ function simple ( generator, block, state, node, branch, dynamic, { name, anchor const parentNode = state.parentNode || `${anchor}.parentNode`; const enter = dynamic ? - ( branch.hasIntroTransitions ? + ( branch.hasIntroMethod ? deindent` if ( ${name} ) { ${name}.update( changed, ${params} ); @@ -105,7 +110,7 @@ function simple ( generator, block, state, node, branch, dynamic, { name, anchor ${name}.mount( ${parentNode}, ${anchor} ); } ` ) : - ( branch.hasIntroTransitions ? + ( branch.hasIntroMethod ? deindent` if ( !${name} ) ${name} = ${branch.block}( ${params}, ${block.component} ); ${name}.intro( ${parentNode}, ${anchor} ); @@ -119,7 +124,7 @@ function simple ( generator, block, state, node, branch, dynamic, { name, anchor // no `update()` here — we don't want to update outroing nodes, // as that will typically result in glitching - const exit = branch.hasOutroTransitions ? + const exit = branch.hasOutroMethod ? deindent` ${name}.outro( function () { ${name}.destroy( true ); @@ -141,50 +146,128 @@ function simple ( generator, block, state, node, branch, dynamic, { name, anchor } function compound ( generator, block, state, node, branches, dynamic, { name, anchor, params } ) { - const getBlock = block.getUniqueName( `get_block` ); + const get_block = block.getUniqueName( `get_block` ); const current_block = block.getUniqueName( `current_block` ); block.builders.create.addBlock( deindent` - function ${getBlock} ( ${params} ) { + function ${get_block} ( ${params} ) { ${branches.map( ({ condition, block }) => { return `${condition ? `if ( ${condition} ) ` : ''}return ${block};`; } ).join( '\n' )} } - var ${current_block} = ${getBlock}( ${params} ); + var ${current_block} = ${get_block}( ${params} ); var ${name} = ${current_block} && ${current_block}( ${params}, ${block.component} ); ` ); const isToplevel = !state.parentNode; + const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount'; if ( isToplevel ) { - block.builders.mount.addLine( `if ( ${name} ) ${name}.mount( ${block.target}, null );` ); + block.builders.mount.addLine( `if ( ${name} ) ${name}.${mountOrIntro}( ${block.target}, null );` ); } else { - block.builders.create.addLine( `if ( ${name} ) ${name}.mount( ${state.parentNode}, null );` ); + block.builders.create.addLine( `if ( ${name} ) ${name}.${mountOrIntro}( ${state.parentNode}, null );` ); } const parentNode = state.parentNode || `${anchor}.parentNode`; - if ( block.hasOutroTransitions ) { - throw new Error( 'TODO compound if-blocks with outro transitions are not yet supported' ); - } + const changeBlock = deindent` + if ( ${name} ) ${name}.destroy( true ); + ${name} = ${current_block} && ${current_block}( ${params}, ${block.component} ); + if ( ${name} ) ${name}.${mountOrIntro}( ${parentNode}, ${anchor} ); + `; if ( dynamic ) { block.builders.update.addBlock( deindent` - if ( ${current_block} === ( ${current_block} = ${getBlock}( ${params} ) ) && ${name} ) { + if ( ${current_block} === ( ${current_block} = ${get_block}( ${params} ) ) && ${name} ) { ${name}.update( changed, ${params} ); } else { - if ( ${name} ) ${name}.destroy( true ); - ${name} = ${current_block} && ${current_block}( ${params}, ${block.component} ); - if ( ${name} ) ${name}.mount( ${parentNode}, ${anchor} ); + ${changeBlock} } ` ); } else { block.builders.update.addBlock( deindent` - if ( ${current_block} !== ( ${current_block} = ${getBlock}( ${params} ) ) ) { - if ( ${name} ) ${name}.destroy( true ); - ${name} = ${current_block} && ${current_block}( ${params}, ${block.component} ); - if ( ${name} ) ${name}.mount( ${parentNode}, ${anchor} ); + if ( ${current_block} !== ( ${current_block} = ${get_block}( ${params} ) ) ) { + ${changeBlock} + } + ` ); + } +} + +// if any of the siblings have outros, we need to keep references to the blocks +// (TODO does this only apply to bidi transitions?) +function compoundWithOutros ( generator, block, state, node, branches, dynamic, { name, anchor, params } ) { + const get_block = block.getUniqueName( `get_block` ); + const current_block_index = block.getUniqueName( `current_block_index` ); + const previous_block_index = block.getUniqueName( `previous_block_index` ); + const if_block_creators = block.getUniqueName( `if_block_creators` ); + const if_blocks = block.getUniqueName( `if_blocks` ); + + block.addVariable( current_block_index ); + + block.builders.create.addBlock( deindent` + var ${if_block_creators} = [ + ${branches.map( branch => branch.block ).join( ',\n' )} + ]; + + var ${if_blocks} = []; + + function ${get_block} ( ${params} ) { + ${branches.map( ({ condition, block }, i ) => { + return `${condition ? `if ( ${condition} ) ` : ''}return ${block ? i : -1};`; + } ).join( '\n' )} + } + + if ( ~( ${current_block_index} = ${get_block}( ${params} ) ) ) { + ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} ); + } + ` ); + + const isToplevel = !state.parentNode; + const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount'; + const initialTarget = isToplevel ? block.target : state.parentNode; + + ( isToplevel ? block.builders.mount : block.builders.create ).addBlock( + `if ( ~${current_block_index} ) ${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${initialTarget}, null );` + ); + + const parentNode = state.parentNode || `${anchor}.parentNode`; + + const changeBlock = deindent` + var ${name} = ${if_blocks}[ ${previous_block_index} ]; + if ( ${name} ) { + ${name}.outro( function () { + ${if_blocks}[ ${previous_block_index} ].destroy( true ); + ${if_blocks}[ ${previous_block_index} ] = null; + }); + } + + if ( ~${current_block_index} ) { + ${name} = ${if_blocks}[ ${current_block_index} ]; + if ( !${name} ) { + ${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} ); + } + + ${name}.${mountOrIntro}( ${parentNode}, ${anchor} ); + } + `; + + if ( dynamic ) { + block.builders.update.addBlock( deindent` + var ${previous_block_index} = ${current_block_index}; + ${current_block_index} = ${get_block}( state ); + if ( ${current_block_index} === ${previous_block_index} ) { + if ( ~${current_block_index} ) ${if_blocks}[ ${current_block_index} ].update( changed, ${params} ); + } else { + ${changeBlock} + } + ` ); + } else { + block.builders.update.addBlock( deindent` + var ${previous_block_index} = ${current_block_index}; + ${current_block_index} = ${get_block}( state ); + if ( ${current_block_index} !== ${previous_block_index} ) { + ${changeBlock} } ` ); }