From 7aa5273fe4efbe16cc4508299dd670fd6f6049ed Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Wed, 26 Apr 2017 17:36:29 -0400 Subject: [PATCH] support very basic outro transitions --- src/generators/dom/Block.js | 33 +++++++++++-- src/generators/dom/index.js | 2 +- src/generators/dom/preprocess.js | 8 ++++ .../dom/visitors/Element/Element.js | 30 ++++++++---- .../dom/visitors/Element/Transition.js | 30 ------------ .../dom/visitors/Element/addTransitions.js | 48 +++++++++++++++++++ src/generators/dom/visitors/IfBlock.js | 27 ++++++++--- src/shared/index.js | 3 +- src/shared/transitions.js | 19 ++++---- 9 files changed, 139 insertions(+), 61 deletions(-) delete mode 100644 src/generators/dom/visitors/Element/Transition.js create mode 100644 src/generators/dom/visitors/Element/addTransitions.js diff --git a/src/generators/dom/Block.js b/src/generators/dom/Block.js index 8c6bbec667..d375676fb3 100644 --- a/src/generators/dom/Block.js +++ b/src/generators/dom/Block.js @@ -24,11 +24,16 @@ export default class Block { create: new CodeBuilder(), mount: new CodeBuilder(), update: new CodeBuilder(), + outro: new CodeBuilder(), detach: new CodeBuilder(), detachRaw: new CodeBuilder(), destroy: new CodeBuilder() }; + this.hasIntroTransitions = false; + this.hasOutroTransitions = false; + this.outros = 0; + this.aliases = new Map(); this.variables = new Map(); this.getUniqueName = this.generator.getUniqueNameMaker( options.params ); @@ -100,6 +105,12 @@ export default class Block { } render () { + let outroing; + if ( this.hasOutroTransitions ) { + outroing = this.getUniqueName( 'outroing' ); + this.addVariable( outroing ); + } + if ( this.variables.size ) { const variables = Array.from( this.variables.keys() ) .map( key => { @@ -135,11 +146,6 @@ export default class Block { properties.addBlock( `key: ${localKey},` ); } - if ( this.outros.length ) { - // TODO - properties.addLine( `outro: null,` ); - } - if ( this.builders.mount.isEmpty() ) { properties.addBlock( `mount: ${this.generator.helper( 'noop' )},` ); } else { @@ -162,6 +168,23 @@ export default class Block { } } + if ( this.hasOutroTransitions ) { + if ( this.builders.outro.isEmpty() ) { + properties.addBlock( `outro: ${this.generator.helper( 'noop' )},` ); + } else { + properties.addBlock( deindent` + outro: function ( ${this.alias( 'outrocallback' )} ) { + if ( ${outroing} ) return; + ${outroing} = true; + + var ${this.alias( 'outros' )} = ${this.outros}; + + ${this.builders.outro} + }, + ` ); + } + } + if ( this.builders.destroy.isEmpty() ) { properties.addBlock( `destroy: ${this.generator.helper( 'noop' )}` ); } else { diff --git a/src/generators/dom/index.js b/src/generators/dom/index.js index abe0fba0e6..8cf7226a22 100644 --- a/src/generators/dom/index.js +++ b/src/generators/dom/index.js @@ -169,7 +169,7 @@ export default function dom ( parsed, source, options ) { if ( templateProperties.oncreate ) { builders.init.addBlock( deindent` if ( options._root ) { - options._root._renderHooks.push({ fn: ${generator.alias( 'template' )}.oncreate, context: this }); + options._root._renderHooks.push( ${generator.alias( 'template' )}.oncreate.bind( this ) ); } else { ${generator.alias( 'template' )}.oncreate.call( this ); } diff --git a/src/generators/dom/preprocess.js b/src/generators/dom/preprocess.js index 6e6c33eaff..1c60a72cdb 100644 --- a/src/generators/dom/preprocess.js +++ b/src/generators/dom/preprocess.js @@ -200,6 +200,14 @@ const preprocessors = { const dependencies = block.findDependencies( attribute.value ); block.addDependencies( dependencies ); } + + else if ( attribute.type === 'Transition' ) { + if ( attribute.intro ) generator.hasIntroTransitions = block.hasIntroTransitions = true; + if ( attribute.outro ) { + generator.hasOutroTransitions = block.hasOutroTransitions = true; + block.outros += 1; + } + } }); if ( node.children.length ) { diff --git a/src/generators/dom/visitors/Element/Element.js b/src/generators/dom/visitors/Element/Element.js index 0643211645..11b3db978f 100644 --- a/src/generators/dom/visitors/Element/Element.js +++ b/src/generators/dom/visitors/Element/Element.js @@ -6,7 +6,7 @@ import visitAttribute from './Attribute.js'; import visitEventHandler from './EventHandler.js'; import visitBinding from './Binding.js'; import visitRef from './Ref.js'; -import visitTransition from './Transition.js'; +import addTransitions from './addTransitions.js'; const meta = { ':Window': visitWindow @@ -16,16 +16,14 @@ const order = { Attribute: 1, Binding: 2, EventHandler: 3, - Ref: 4, - Transition: 5 + Ref: 4 }; const visitors = { Attribute: visitAttribute, EventHandler: visitEventHandler, Binding: visitBinding, - Ref: visitRef, - Transition: visitTransition + Ref: visitRef }; export default function visitElement ( generator, block, state, node ) { @@ -43,21 +41,35 @@ export default function visitElement ( generator, block, state, node ) { block.builders.create.addLine( `var ${name} = ${getRenderStatement( generator, childState.namespace, node.name )};` ); block.mount( name, state.parentNode ); - if ( !state.parentNode ) { - block.builders.detach.addLine( `${generator.helper( 'detachNode' )}( ${name} );` ); - } - // add CSS encapsulation attribute if ( generator.cssId && state.isTopLevel ) { block.builders.create.addLine( `${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );` ); } function visitAttributes () { + let intro; + let outro; + node.attributes .sort( ( a, b ) => order[ a.type ] - order[ b.type ] ) .forEach( attribute => { + if ( attribute.type === 'Transition' ) { + if ( attribute.intro ) intro = attribute; + if ( attribute.outro ) outro = attribute; + return; + } + visitors[ attribute.type ]( generator, block, childState, node, attribute ); }); + + addTransitions( generator, block, childState, node, intro, outro ); + + if ( !outro && !state.parentNode ) { + // TODO this probably doesn't belong here. We eventually need to consider + // what happens to elements that belong to the same outgroup as an + // outroing element... + block.builders.detach.addLine( `${generator.helper( 'detachNode' )}( ${name} );` ); + } } if ( node.name !== 'select' ) { diff --git a/src/generators/dom/visitors/Element/Transition.js b/src/generators/dom/visitors/Element/Transition.js deleted file mode 100644 index 50f7534979..0000000000 --- a/src/generators/dom/visitors/Element/Transition.js +++ /dev/null @@ -1,30 +0,0 @@ -import deindent from '../../../../utils/deindent.js'; - -export default function visitTransition ( generator, block, state, node, attribute ) { - const name = block.getUniqueName( `${state.name}_${attribute.intro ? 'intro' : 'outro'}` ); - - block.addVariable( name ); - - const snippet = attribute.expression ? block.contextualise( attribute.expression ).snippet : '{}'; - const fn = `${generator.alias( 'template' )}.transitions.${attribute.name}`; // TODO add built-in transitions? - - if ( attribute.intro ) { - generator.hasIntroTransitions = true; - - block.builders.create.addBlock( deindent` - ${block.component}._renderHooks.push({ - fn: function () { - ${name} = ${generator.helper( 'wrapTransition' )}( ${state.name}, ${fn}, ${snippet}, true ); - ${generator.helper( 'transitionManager' )}.add( ${name} ); - }, - context: ${block.component} - }); - ` ); - } - - if ( attribute.outro ) { - generator.hasOutroTransitions = true; - - throw new Error( 'TODO' ); - } -} \ No newline at end of file diff --git a/src/generators/dom/visitors/Element/addTransitions.js b/src/generators/dom/visitors/Element/addTransitions.js new file mode 100644 index 0000000000..b3e1a7e510 --- /dev/null +++ b/src/generators/dom/visitors/Element/addTransitions.js @@ -0,0 +1,48 @@ +import deindent from '../../../../utils/deindent.js'; + +export default function addTransitions ( generator, block, state, node, intro, outro ) { + const introName = intro && block.getUniqueName( `${state.name}_intro` ); + const outroName = outro && block.getUniqueName( `${state.name}_outro` ); + + const introSnippet = intro && intro.expression ? block.contextualise( intro.expression ).snippet : '{}'; + + const outroSnippet = outro === intro ? + introSnippet : + outro && outro.expression ? block.contextualise( outro.expression ).snippet : '{}'; + + const wrapTransition = generator.helper( 'wrapTransition' ); + + if ( intro ) { + block.addVariable( introName ); + + const fn = `${generator.alias( 'template' )}.transitions.${intro.name}`; // TODO add built-in transitions? + + block.builders.create.addBlock( deindent` + ${block.component}._renderHooks.push( function () { + ${introName} = ${wrapTransition}( ${state.name}, ${fn}, ${introSnippet}, true, null, function () { + ${block.component}.fire( 'intro.end', { node: ${state.name} }); + }); + ${generator.helper( 'transitionManager' )}.add( ${introName} ); + }); + ` ); + } + + if ( outro ) { + block.addVariable( outroName ); + + const fn = `${generator.alias( 'template' )}.transitions.${outro.name}`; + + if ( intro ) { + block.builders.outro.addBlock( `${introName}.abort();` ); + } + + block.builders.outro.addBlock( deindent` + ${outroName} = ${wrapTransition}( ${state.name}, ${fn}, ${outroSnippet}, false, null, function () { + detachNode( div ); + ${block.component}.fire( 'outro.end', { node: ${state.name} }); + if ( --${block.alias( 'outros' )} === 0 ) ${block.alias( 'outrocallback' )}(); + }); + transitionManager.add( ${outroName} ); + ` ); + } +} \ No newline at end of file diff --git a/src/generators/dom/visitors/IfBlock.js b/src/generators/dom/visitors/IfBlock.js index e6c3d9952a..6ce7897344 100644 --- a/src/generators/dom/visitors/IfBlock.js +++ b/src/generators/dom/visitors/IfBlock.js @@ -9,7 +9,8 @@ function getBranches ( generator, block, state, node ) { const branches = [{ condition: block.contextualise( node.expression ).snippet, block: node._block.name, - dynamic: node._block.dependencies.size > 0 + dynamic: node._block.dependencies.size > 0, + hasOutroTransitions: node._block.hasOutroTransitions }]; visitChildren( generator, block, state, node ); @@ -22,7 +23,8 @@ 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 + dynamic: node.else ? node.else._block.dependencies.size > 0 : false, + hasOutroTransitions: node.else ? node.else._block.hasOutroTransitions : false }); if ( node.else ) { @@ -81,6 +83,17 @@ function simple ( generator, block, state, node, branch, dynamic, { name, anchor const parentNode = state.parentNode || `${anchor}.parentNode`; + const remove = branch.hasOutroTransitions ? + deindent` + ${name}.outro( function () { + ${name} = null; + }); + ` : + deindent` + ${name}.destroy( true ); + ${name} = null; + `; + if ( dynamic ) { block.builders.update.addBlock( deindent` if ( ${branch.condition} ) { @@ -91,8 +104,7 @@ function simple ( generator, block, state, node, branch, dynamic, { name, anchor ${name}.mount( ${parentNode}, ${anchor} ); } } else if ( ${name} ) { - ${name}.destroy( true ); - ${name} = null; + ${remove} } ` ); } else { @@ -103,8 +115,7 @@ function simple ( generator, block, state, node, branch, dynamic, { name, anchor ${name}.mount( ${parentNode}, ${anchor} ); } } else if ( ${name} ) { - ${name}.destroy( true ); - ${name} = null; + ${remove} } ` ); } @@ -135,6 +146,10 @@ function compound ( generator, block, state, node, branches, dynamic, { name, an const parentNode = state.parentNode || `${anchor}.parentNode`; + if ( block.hasOutroTransitions ) { + throw new Error( 'TODO compound if-blocks with outro transitions are not yet supported' ); + } + if ( dynamic ) { block.builders.update.addBlock( deindent` if ( ${current_block} === ( ${current_block} = ${getBlock}( ${params} ) ) && ${name} ) { diff --git a/src/shared/index.js b/src/shared/index.js index 476f343fa5..d31bbffc17 100644 --- a/src/shared/index.js +++ b/src/shared/index.js @@ -106,8 +106,7 @@ export function _flush () { if ( !this._renderHooks ) return; while ( this._renderHooks.length ) { - var hook = this._renderHooks.pop(); - hook.fn.call( hook.context ); + this._renderHooks.pop()(); } } diff --git a/src/shared/transitions.js b/src/shared/transitions.js index 2dc2444cb0..1b4cb1b90d 100644 --- a/src/shared/transitions.js +++ b/src/shared/transitions.js @@ -4,8 +4,8 @@ export function linear ( t ) { return t; } -export function wrapTransition ( node, fn, params, isIntro ) { - var obj = fn( node, params, isIntro ); +export function wrapTransition ( node, fn, params, intro, outgroup, callback ) { + var obj = fn( node, params, intro ); var start = window.performance.now() + ( obj.delay || 0 ); var duration = obj.duration || 300; @@ -14,16 +14,18 @@ export function wrapTransition ( node, fn, params, isIntro ) { if ( obj.tick ) { // JS transition - if ( isIntro ) obj.tick( 0 ); + if ( intro ) obj.tick( 0 ); return { start: start, end: end, update: function ( now ) { - obj.tick( ease( ( now - start ) / duration ) ); + const p = intro ? now - start : end - now; + obj.tick( ease( p / duration ) ); }, done: function () { - obj.tick( isIntro ? 1 : 0 ); + obj.tick( intro ? 1 : 0 ); + callback(); }, abort: noop }; @@ -39,7 +41,7 @@ export function wrapTransition ( node, fn, params, isIntro ) { init: function () { for ( var key in obj.styles ) { inlineStyles[ key ] = node.style[ key ]; - node.style[ key ] = isIntro ? obj.styles[ key ] : computedStyles[ key ]; + node.style[ key ] = intro ? obj.styles[ key ] : computedStyles[ key ]; } }, update: function ( now ) { @@ -52,7 +54,7 @@ export function wrapTransition ( node, fn, params, isIntro ) { // TODO use a keyframe animation for custom easing functions for ( var key in obj.styles ) { - node.style[ key ] = isIntro ? computedStyles[ key ] : obj.styles[ key ]; + node.style[ key ] = intro ? computedStyles[ key ] : obj.styles[ key ]; } started = true; @@ -60,11 +62,12 @@ export function wrapTransition ( node, fn, params, isIntro ) { }, done: function () { // TODO what if one of these styles was dynamic? - if ( isIntro ) { + if ( intro ) { for ( var key in obj.styles ) { node.style[ key ] = inlineStyles[ key ]; } } + callback(); }, abort: function () { node.style.cssText = getComputedStyle( node ).cssText;