Merge pull request #525 from sveltejs/gh-7

Transitions
pull/555/head
Rich Harris 8 years ago committed by GitHub
commit 1078bae696

@ -17,11 +17,13 @@
"precodecov": "npm run coverage",
"lint": "eslint src test/*.js",
"build": "npm run build:main && npm run build:shared && npm run build:ssr",
"build:main": "rollup -c rollup/rollup.config.main.js",
"build:main": "node src/shared/_build.js && rollup -c rollup/rollup.config.main.js",
"build:shared": "rollup -c rollup/rollup.config.shared.js",
"build:ssr": "rollup -c rollup/rollup.config.ssr.js",
"dev": "node src/shared/_build.js && rollup -c rollup/rollup.config.main.js -w",
"dev:shared": "rollup -c rollup/rollup.config.shared.js -w",
"pretest": "npm run build",
"prepublish": "npm run lint && npm run build"
"prepublish": "npm run build && npm run lint"
},
"repository": {
"type": "git",
@ -72,6 +74,7 @@
"rollup-plugin-commonjs": "^7.0.0",
"rollup-plugin-json": "^2.1.0",
"rollup-plugin-node-resolve": "^2.0.0",
"rollup-watch": "^3.2.2",
"source-map": "^0.5.6",
"source-map-support": "^0.4.8"
},

@ -24,6 +24,7 @@ export default class Generator {
this.helpers = new Set();
this.components = new Set();
this.events = new Set();
this.transitions = new Set();
this.importedComponents = new Map();
this.bindingGroups = [];
@ -328,7 +329,7 @@ export default class Generator {
});
}
[ 'helpers', 'events', 'components' ].forEach( key => {
[ 'helpers', 'events', 'components', 'transitions' ].forEach( key => {
if ( templateProperties[ key ] ) {
templateProperties[ key ].value.properties.forEach( prop => {
this[ key ].add( prop.key.name );

@ -24,11 +24,17 @@ export default class Block {
create: new CodeBuilder(),
mount: new CodeBuilder(),
update: new CodeBuilder(),
intro: new CodeBuilder(),
outro: new CodeBuilder(),
detach: new CodeBuilder(),
detachRaw: new CodeBuilder(),
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.outros = 0;
this.aliases = new Map();
this.variables = new Map();
this.getUniqueName = this.generator.getUniqueNameMaker( options.params );
@ -100,6 +106,20 @@ export default class Block {
}
render () {
let introing;
const hasIntros = !this.builders.intro.isEmpty();
if ( hasIntros ) {
introing = this.getUniqueName( 'introing' );
this.addVariable( introing );
}
let outroing;
const hasOutros = !this.builders.outro.isEmpty();
if ( hasOutros ) {
outroing = this.getUniqueName( 'outroing' );
this.addVariable( outroing );
}
if ( this.variables.size ) {
const variables = Array.from( this.variables.keys() )
.map( key => {
@ -157,6 +177,50 @@ export default class Block {
}
}
if ( this.hasIntroMethod ) {
if ( hasIntros ) {
properties.addBlock( deindent`
intro: function ( ${this.target}, anchor ) {
if ( ${introing} ) return;
${introing} = true;
${hasOutros && `${outroing} = false;`}
${this.builders.intro}
this.mount( ${this.target}, anchor );
},
` );
} else {
properties.addBlock( deindent`
intro: function ( ${this.target}, anchor ) {
this.mount( ${this.target}, anchor );
},
` );
}
}
if ( this.hasOutroMethod ) {
if ( hasOutros ) {
properties.addBlock( deindent`
outro: function ( ${this.alias( 'outrocallback' )} ) {
if ( ${outroing} ) return;
${outroing} = true;
${hasIntros && `${introing} = false;`}
var ${this.alias( 'outros' )} = ${this.outros};
${this.builders.outro}
},
` );
} else {
properties.addBlock( deindent`
outro: function ( outrocallback ) {
outrocallback();
},
` );
}
}
if ( this.builders.destroy.isEmpty() ) {
properties.addBlock( `destroy: ${this.generator.helper( 'noop' )}` );
} else {

@ -1,12 +1,12 @@
import MagicString from 'magic-string';
import { parse } from 'acorn';
import { parseExpressionAt } from 'acorn';
import annotateWithScopes from '../../utils/annotateWithScopes.js';
import isReference from '../../utils/isReference.js';
import { walk } from 'estree-walker';
import deindent from '../../utils/deindent.js';
import CodeBuilder from '../../utils/CodeBuilder.js';
import visit from './visit.js';
import { nameMap, sharedMap } from './sharedNames.js';
import shared from './shared.js';
import Generator from '../Generator.js';
import preprocess from './preprocess.js';
@ -25,7 +25,7 @@ class DomGenerator extends Generator {
}
helper ( name ) {
if ( this.options.dev && sharedMap.has( `${name}Dev` ) ) {
if ( this.options.dev && `${name}Dev` in shared ) {
name = `${name}Dev`;
}
@ -138,7 +138,7 @@ export default function dom ( parsed, source, options ) {
builders.init.addLine( `if ( !${generator.alias( 'added_css' )} ) ${generator.alias( 'add_css' )}();` );
}
if ( generator.hasComponents ) {
if ( generator.hasComponents || generator.hasIntroTransitions ) {
builders.init.addLine( `this._renderHooks = [];` );
}
@ -158,7 +158,7 @@ export default function dom ( parsed, source, options ) {
` );
}
if ( generator.hasComponents ) {
if ( generator.hasComponents || generator.hasIntroTransitions ) {
const statement = `this._flush();`;
builders.init.addBlock( statement );
@ -168,7 +168,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 );
}
@ -218,7 +218,7 @@ export default function dom ( parsed, source, options ) {
this._handlers = Object.create( null );
this._root = options._root;
this._root = options._root || this;
this._yield = options._yield;
${builders.init}
@ -275,20 +275,20 @@ export default function dom ( parsed, source, options ) {
);
} else {
generator.uses.forEach( key => {
const str = sharedMap.get( key );
const str = shared[ key ];
const code = new MagicString( str );
const fn = parse( str ).body[0];
const expression = parseExpressionAt( str, 0 );
let scope = annotateWithScopes( fn );
let scope = annotateWithScopes( expression );
walk( fn, {
walk( expression, {
enter ( node, parent ) {
if ( node._scope ) scope = node._scope;
if ( node.type === 'Identifier' && isReference( node, parent ) && !scope.has( node.name ) ) {
if ( nameMap.has( node.name ) ) {
if ( node.name in shared ) {
// this helper function depends on another one
const dependency = nameMap.get( node.name );
const dependency = node.name;
generator.uses.add( dependency );
const alias = generator.alias( dependency );
@ -302,10 +302,18 @@ export default function dom ( parsed, source, options ) {
}
});
const alias = generator.alias( key );
if ( alias !== fn.id.name ) code.overwrite( fn.id.start, fn.id.end, alias );
if ( key === 'transitionManager' ) { // special case
const global = `_svelteTransitionManager`;
builders.main.addBlock( code.toString() );
builders.main.addBlock(
`var ${generator.alias( 'transitionManager' )} = window.${global} || ( window.${global} = ${code});`
);
} else {
const alias = generator.alias( expression.id.name );
if ( alias !== expression.id.name ) code.overwrite( expression.id.start, expression.id.end, alias );
builders.main.addBlock( code.toString() );
}
});
}

@ -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.hasIntroMethod ) hasIntros = true;
if ( node._block.hasOutroMethod ) 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 );
@ -200,6 +207,14 @@ const preprocessors = {
const dependencies = block.findDependencies( attribute.value );
block.addDependencies( dependencies );
}
else if ( attribute.type === 'Transition' ) {
if ( attribute.intro ) generator.hasIntroTransitions = block.hasIntroMethod = true;
if ( attribute.outro ) {
generator.hasOutroTransitions = block.hasOutroMethod = true;
block.outros += 1;
}
}
});
if ( node.children.length ) {

@ -1,12 +0,0 @@
import * as shared from '../../shared/index.js';
export const nameMap = new Map();
export const sharedMap = new Map();
Object.keys(shared).forEach( key => {
const value = shared[ key ]; // eslint-disable-line import/namespace
if ( typeof value === 'function' ) {
nameMap.set( value.name, key );
}
sharedMap.set( key, value.toString() );
});

@ -92,7 +92,7 @@ export default function visitComponent ( generator, block, state, node ) {
const componentInitProperties = [
`target: ${!isToplevel ? state.parentNode: 'null'}`,
`_root: ${block.component}._root || ${block.component}`
`_root: ${block.component}._root`
];
// Component has children, put them in a separate {{yield}} block

@ -11,7 +11,8 @@ export default function visitEachBlock ( generator, block, state, node ) {
const params = block.params.join( ', ' );
const anchor = node.needsAnchor ? block.getUniqueName( `${each_block}_anchor` ) : ( node.next && node.next._state.name ) || 'null';
const vars = { each_block, create_each_block, each_block_value, iterations, i, params, anchor };
const mountOrIntro = node._block.hasIntroMethod ? 'intro' : 'mount';
const vars = { each_block, create_each_block, each_block_value, iterations, i, params, anchor, mountOrIntro };
const { snippet } = block.contextualise( node.expression );
@ -29,7 +30,7 @@ export default function visitEachBlock ( generator, block, state, node ) {
if ( isToplevel ) {
block.builders.mount.addBlock( deindent`
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].mount( ${block.target}, null );
${iterations}[${i}].${mountOrIntro}( ${block.target}, null );
}
` );
}
@ -52,13 +53,13 @@ export default function visitEachBlock ( generator, block, state, node ) {
block.builders.create.addBlock( deindent`
if ( !${each_block_value}.length ) {
${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
${!isToplevel ? `${each_block_else}.mount( ${state.parentNode}, null );` : ''}
${!isToplevel ? `${each_block_else}.${mountOrIntro}( ${state.parentNode}, null );` : ''}
}
` );
block.builders.mount.addBlock( deindent`
if ( ${each_block_else} ) {
${each_block_else}.mount( ${state.parentNode || block.target}, null );
${each_block_else}.${mountOrIntro}( ${state.parentNode || block.target}, null );
}
` );
@ -70,7 +71,7 @@ export default function visitEachBlock ( generator, block, state, node ) {
${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( ${parentNode}, ${anchor} );
${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} );
} else if ( ${each_block_else} ) {
${each_block_else}.destroy( true );
${each_block_else} = null;
@ -85,7 +86,7 @@ export default function visitEachBlock ( generator, block, state, node ) {
}
} else if ( !${each_block_else} ) {
${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
${each_block_else}.mount( ${parentNode}, ${anchor} );
${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} );
}
` );
}
@ -109,12 +110,12 @@ export default function visitEachBlock ( generator, block, state, node ) {
}
}
function keyed ( generator, block, state, node, snippet, { each_block, create_each_block, each_block_value, iterations, i, params, anchor } ) {
function keyed ( generator, block, state, node, snippet, { each_block, create_each_block, each_block_value, iterations, i, params, anchor, mountOrIntro } ) {
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 keys = block.getUniqueName( `${each_block}_keys` );
const iteration = block.getUniqueName( `${each_block}_iteration` );
const _iterations = block.getUniqueName( `_${each_block}_iterations` );
@ -124,12 +125,12 @@ function keyed ( generator, block, state, node, snippet, { each_block, create_ea
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}` : `` } );
${iterations}[${i}] = ${lookup}[ ${key} ] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} );
` );
if ( state.parentNode ) {
create.addLine(
`${iterations}[${i}].mount( ${state.parentNode}, null );`
`${iterations}[${i}].${mountOrIntro}( ${state.parentNode}, null );`
);
}
@ -141,17 +142,40 @@ function keyed ( generator, block, state, node, snippet, { each_block, create_ea
const consequent = node._block.hasUpdateMethod ?
deindent`
${_iterations}[${i}] = ${_lookup}[ ${key} ] = ${lookup}[ ${key} ];
${_lookup}[ ${key} ].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );
${_iterations}[${i}] = ${lookup}[ ${key} ] = ${lookup}[ ${key} ];
${lookup}[ ${key} ].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );
` :
`${_iterations}[${i}] = ${_lookup}[ ${key} ] = ${lookup}[ ${key} ];`;
`${_iterations}[${i}] = ${lookup}[ ${key} ] = ${lookup}[ ${key} ];`;
const parentNode = state.parentNode || `${anchor}.parentNode`;
const hasIntros = node._block.hasIntroMethod;
const destroy = node._block.hasOutroMethod ?
deindent`
function outro ( key ) {
${lookup}[ key ].outro( function () {
${lookup}[ key ].destroy( true );
${lookup}[ key ] = null;
});
}
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${key} = ${iterations}[${i}].key;
if ( !${keys}[ ${key} ] ) outro( ${key} );
}
` :
deindent`
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
var ${iteration} = ${iterations}[${i}];
if ( !${keys}[ ${iteration}.key ] ) ${iteration}.destroy( true );
}
`;
block.builders.update.addBlock( deindent`
var ${each_block_value} = ${snippet};
var ${_iterations} = [];
var ${_lookup} = Object.create( null );
var ${keys} = Object.create( null );
var ${fragment} = document.createDocumentFragment();
@ -159,32 +183,29 @@ function keyed ( generator, block, state, node, snippet, { each_block, create_ea
for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) {
var ${value} = ${each_block_value}[${i}];
var ${key} = ${value}.${node.key};
${keys}[ ${key} ] = true;
if ( ${lookup}[ ${key} ] ) {
${consequent}
${hasIntros && `${_iterations}[${i}].mount( ${fragment}, null );`}
} else {
${_iterations}[${i}] = ${_lookup}[ ${key} ] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}${node.key ? `, ${key}` : `` } );
${_iterations}[${i}] = ${lookup}[ ${key} ] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} );
${hasIntros && `${_iterations}[${i}].intro( ${fragment}, null );`}
}
${_iterations}[${i}].mount( ${fragment}, null );
${!hasIntros && `${_iterations}[${i}].mount( ${fragment}, null );`}
}
// remove old iterations
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
var ${iteration} = ${iterations}[${i}];
if ( !${_lookup}[ ${iteration}.key ] ) {
${iteration}.destroy( true );
}
}
${destroy}
${parentNode}.insertBefore( ${fragment}, ${anchor} );
${iterations} = ${_iterations};
${lookup} = ${_lookup};
` );
}
function unkeyed ( generator, block, state, node, snippet, { create_each_block, each_block_value, iterations, i, params, anchor } ) {
function unkeyed ( generator, block, state, node, snippet, { create_each_block, each_block_value, iterations, i, params, anchor, mountOrIntro } ) {
const create = new CodeBuilder();
create.addLine(
@ -193,7 +214,7 @@ function unkeyed ( generator, block, state, node, snippet, { create_each_block,
if ( state.parentNode ) {
create.addLine(
`${iterations}[${i}].mount( ${state.parentNode}, null );`
`${iterations}[${i}].${mountOrIntro}( ${state.parentNode}, null );`
);
}
@ -222,16 +243,34 @@ function unkeyed ( generator, block, state, node, snippet, { create_each_block,
${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );
} else {
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${iterations}[${i}].mount( ${parentNode}, ${anchor} );
${iterations}[${i}].${mountOrIntro}( ${parentNode}, ${anchor} );
}
` :
deindent`
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${iterations}[${i}].mount( ${parentNode}, ${anchor} );
${iterations}[${i}].${mountOrIntro}( ${parentNode}, ${anchor} );
`;
const start = node._block.hasUpdateMethod ? '0' : `${iterations}.length`;
const destroy = node._block.hasOutroMethod ?
deindent`
function outro ( i ) {
if ( ${iterations}[i] ) {
${iterations}[i].outro( function () {
${iterations}[i].destroy( true );
${iterations}[i] = null;
});
}
}
for ( ; ${i} < ${iterations}.length; ${i} += 1 ) outro( ${i} );
` :
deindent`
${generator.helper( 'destroyEach' )}( ${iterations}, true, ${each_block_value}.length );
${iterations}.length = ${each_block_value}.length;
`;
block.builders.update.addBlock( deindent`
var ${each_block_value} = ${snippet};
@ -240,9 +279,7 @@ function unkeyed ( generator, block, state, node, snippet, { create_each_block,
${forLoopBody}
}
${generator.helper( 'destroyEach' )}( ${iterations}, true, ${each_block_value}.length );
${iterations}.length = ${each_block_value}.length;
${destroy}
}
` );
}

@ -6,6 +6,7 @@ import visitAttribute from './Attribute.js';
import visitEventHandler from './EventHandler.js';
import visitBinding from './Binding.js';
import visitRef from './Ref.js';
import addTransitions from './addTransitions.js';
const meta = {
':Window': visitWindow
@ -40,21 +41,34 @@ 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 );
});
if ( intro || outro ) addTransitions( generator, block, childState, node, intro, outro );
}
if ( !state.parentNode ) {
// TODO 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' ) {

@ -0,0 +1,73 @@
import deindent from '../../../../utils/deindent.js';
export default function addTransitions ( generator, block, state, node, intro, outro ) {
const wrapTransition = generator.helper( 'wrapTransition' );
if ( intro === outro ) {
const name = block.getUniqueName( `${state.name}_transition` );
const snippet = intro.expression ? block.contextualise( intro.expression ).snippet : '{}';
block.addVariable( name );
const fn = `${generator.alias( 'template' )}.transitions.${intro.name}`;
block.builders.intro.addBlock( deindent`
${block.component}._renderHooks.push( function () {
if ( !${name} ) ${name} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, true, null );
${name}.run( ${name}.t, 1, function () {
${block.component}.fire( 'intro.end', { node: ${state.name} });
});
});
` );
block.builders.outro.addBlock( deindent`
${name}.run( ${name}.t, 0, function () {
${block.component}.fire( 'outro.end', { node: ${state.name} });
if ( --${block.alias( 'outros' )} === 0 ) ${block.alias( 'outrocallback' )}();
${name} = null;
});
` );
}
else {
const introName = intro && block.getUniqueName( `${state.name}_intro` );
const outroName = outro && block.getUniqueName( `${state.name}_outro` );
if ( intro ) {
block.addVariable( introName );
const snippet = intro.expression ? block.contextualise( intro.expression ).snippet : '{}';
const fn = `${generator.alias( 'template' )}.transitions.${intro.name}`; // TODO add built-in transitions?
if ( outro ) {
block.builders.intro.addBlock( `if ( ${outroName} ) ${outroName}.abort();` );
}
block.builders.intro.addBlock( deindent`
${block.component}._renderHooks.push( function () {
${introName} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, true, null );
${introName}.run( 0, 1, function () {
${block.component}.fire( 'intro.end', { node: ${state.name} });
});
});
` );
}
if ( outro ) {
block.addVariable( outroName );
const snippet = outro.expression ? block.contextualise( outro.expression ).snippet : '{}';
const fn = `${generator.alias( 'template' )}.transitions.${outro.name}`;
// TODO hide elements that have outro'd (unless they belong to a still-outroing
// group) prior to their removal from the DOM
block.builders.outro.addBlock( deindent`
${outroName} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, false, null );
${outroName}.run( 1, 0, function () {
${block.component}.fire( 'outro.end', { node: ${state.name} });
if ( --${block.alias( 'outros' )} === 0 ) ${block.alias( 'outrocallback' )}();
});
` );
}
}
}

@ -9,7 +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
hasUpdateMethod: node._block.hasUpdateMethod,
hasIntroMethod: node._block.hasIntroMethod,
hasOutroMethod: node._block.hasOutroMethod
}];
visitChildren( generator, block, state, node );
@ -22,7 +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
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 ) {
@ -53,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 );
}
@ -72,85 +81,193 @@ function simple ( generator, block, state, node, branch, dynamic, { name, anchor
` );
const isToplevel = !state.parentNode;
const mountOrIntro = branch.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 ( dynamic ) {
block.builders.update.addBlock( deindent`
if ( ${branch.condition} ) {
const enter = dynamic ?
( branch.hasIntroMethod ?
deindent`
if ( ${name} ) {
${name}.update( changed, ${params} );
} else {
${name} = ${branch.block}( ${params}, ${block.component} );
}
${name}.intro( ${parentNode}, ${anchor} );
` :
deindent`
if ( ${name} ) {
${name}.update( changed, ${params} );
} else {
${name} = ${branch.block}( ${params}, ${block.component} );
${name}.mount( ${parentNode}, ${anchor} );
}
} else if ( ${name} ) {
${name}.destroy( true );
${name} = null;
}
` );
} else {
block.builders.update.addBlock( deindent`
if ( ${branch.condition} ) {
` ) :
( branch.hasIntroMethod ?
deindent`
if ( !${name} ) ${name} = ${branch.block}( ${params}, ${block.component} );
${name}.intro( ${parentNode}, ${anchor} );
` :
deindent`
if ( !${name} ) {
${name} = ${branch.block}( ${params}, ${block.component} );
${name}.mount( ${parentNode}, ${anchor} );
}
} else if ( ${name} ) {
` );
// no `update()` here — we don't want to update outroing nodes,
// as that will typically result in glitching
const exit = branch.hasOutroMethod ?
deindent`
${name}.outro( function () {
${name}.destroy( true );
${name} = null;
}
` );
}
});
` :
deindent`
${name}.destroy( true );
${name} = null;
`;
block.builders.update.addBlock( deindent`
if ( ${branch.condition} ) {
${enter}
} else if ( ${name} ) {
${exit}
}
` );
}
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`;
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} = ${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`
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} );
var ${previous_block_index} = ${current_block_index};
${current_block_index} = ${get_block}( state );
if ( ${current_block_index} !== ${previous_block_index} ) {
${changeBlock}
}
` );
}

@ -1,19 +1,11 @@
import { parse, parseExpressionAt } from 'acorn';
import { parseExpressionAt } from 'acorn';
import spaces from '../../utils/spaces.js';
export function readEventHandlerDirective ( parser, start, name ) {
const quoteMark = (
parser.eat( `'` ) ? `'` :
parser.eat( `"` ) ? `"` :
null
);
const expressionStart = parser.index;
function readExpression ( parser, start, quoteMark ) {
let str = '';
let escaped = false;
for ( let i = expressionStart; i < parser.template.length; i += 1 ) {
for ( let i = start; i < parser.template.length; i += 1 ) {
const char = parser.template[i];
if ( quoteMark ) {
@ -21,7 +13,6 @@ export function readEventHandlerDirective ( parser, start, name ) {
if ( escaped ) {
str += quoteMark;
} else {
parser.index = i + 1;
break;
}
} else if ( escaped ) {
@ -35,7 +26,6 @@ export function readEventHandlerDirective ( parser, start, name ) {
}
else if ( /\s/.test( char ) ) {
parser.index = i;
break;
}
@ -44,13 +34,25 @@ export function readEventHandlerDirective ( parser, start, name ) {
}
}
const ast = parse( spaces( expressionStart ) + str );
const expression = parseExpressionAt( spaces( start ) + str, start );
parser.index = expression.end;
if ( ast.body.length > 1 ) {
parser.error( `Event handler should be a single call expression`, ast.body[1].start );
}
parser.allowWhitespace();
if ( quoteMark ) parser.eat( quoteMark, true );
return expression;
}
export function readEventHandlerDirective ( parser, start, name ) {
const quoteMark = (
parser.eat( `'` ) ? `'` :
parser.eat( `"` ) ? `"` :
null
);
const expressionStart = parser.index;
const expression = ast.body[0].expression;
const expression = readExpression( parser, expressionStart, quoteMark );
if ( expression.type !== 'CallExpression' ) {
parser.error( `Expected call expression`, expressionStart );
@ -127,3 +129,33 @@ export function readBindingDirective ( parser, start, name ) {
value
};
}
export function readTransitionDirective ( parser, start, name, type ) {
let expression = null;
if ( parser.eat( '=' ) ) {
const quoteMark = (
parser.eat( `'` ) ? `'` :
parser.eat( `"` ) ? `"` :
null
);
const expressionStart = parser.index;
expression = readExpression( parser, expressionStart, quoteMark );
if ( expression.type !== 'ObjectExpression' ) {
parser.error( `Expected object expression`, expressionStart );
}
}
return {
start,
end: parser.index,
type: 'Transition',
name,
intro: type === 'in' || type === 'transition',
outro: type === 'out' || type === 'transition',
expression
};
}

@ -1,7 +1,7 @@
import readExpression from '../read/expression.js';
import readScript from '../read/script.js';
import readStyle from '../read/style.js';
import { readEventHandlerDirective, readBindingDirective } from '../read/directives.js';
import { readEventHandlerDirective, readBindingDirective, readTransitionDirective } from '../read/directives.js';
import { trimStart, trimEnd } from '../../utils/trim.js';
import { decodeCharacterReferences } from '../utils/html.js';
import isVoidElementName from '../../utils/isVoidElementName.js';
@ -253,6 +253,11 @@ function readAttribute ( parser, uniqueNames ) {
};
}
const match = /^(in|out|transition):/.exec( name );
if ( match ) {
return readTransitionDirective( parser, start, name.slice( match[0].length ), match[1] );
}
let value;
// :foo is shorthand for foo='{{foo}}'

@ -0,0 +1,38 @@
{
"root": true,
"rules": {
"indent": [ 2, "tab", { "SwitchCase": 1 } ],
"semi": [ 2, "always" ],
"keyword-spacing": [ 2, { "before": true, "after": true } ],
"space-before-blocks": [ 2, "always" ],
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
"no-cond-assign": 0,
"no-unused-vars": 2,
"no-const-assign": 2,
"no-class-assign": 2,
"no-this-before-super": 2,
"no-unreachable": 2,
"valid-typeof": 2,
"quote-props": [ 2, "as-needed" ],
"arrow-spacing": 2,
"no-inner-declarations": 0
},
"env": {
"es6": true,
"browser": true,
"node": true,
"mocha": true
},
"extends": [
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"settings": {
"import/core-modules": [ "svelte" ]
}
}

@ -0,0 +1,35 @@
const fs = require( 'fs' );
const path = require( 'path' );
const acorn = require( 'acorn' );
const declarations = {};
fs.readdirSync( __dirname ).forEach( file => {
if ( !/^[a-z]+\.js$/.test( file ) ) return;
const source = fs.readFileSync( path.join( __dirname, file ), 'utf-8' );
const ast = acorn.parse( source, {
ecmaVersion: 6,
sourceType: 'module'
});
ast.body.forEach( node => {
if ( node.type !== 'ExportNamedDeclaration' ) return;
const declaration = node.declaration;
if ( !declaration ) return;
const name = declaration.type === 'VariableDeclaration' ?
declaration.declarations[0].id.name :
declaration.id.name;
const value = declaration.type === 'VariableDeclaration' ?
declaration.declarations[0].init :
declaration;
declarations[ name ] = source.slice( value.start, value.end );
});
});
fs.writeFileSync( 'src/generators/dom/shared.js', `// this file is auto-generated, do not edit it
export default ${JSON.stringify( declarations, null, '\t' )};` );

@ -1,15 +1,7 @@
import { assign } from './utils.js';
export * from './dom.js';
export function noop () {}
export function assign ( target ) {
for ( var i = 1; i < arguments.length; i += 1 ) {
var source = arguments[i];
for ( var k in source ) target[k] = source[k];
}
return target;
}
export * from './transitions.js';
export * from './utils.js';
export function differs ( a, b ) {
return ( a !== b ) || ( a && ( typeof a === 'object' ) || ( typeof a === 'function' ) );
@ -107,15 +99,14 @@ export function onDev ( eventName, handler ) {
export function set ( newState ) {
this._set( assign( {}, newState ) );
( this._root || this )._flush();
this._root._flush();
}
export function _flush () {
if ( !this._renderHooks ) return;
while ( this._renderHooks.length ) {
var hook = this._renderHooks.pop();
hook.fn.call( hook.context );
this._renderHooks.pop()();
}
}

@ -0,0 +1,129 @@
import { assign, noop } from './utils.js';
export function linear ( t ) {
return t;
}
function generateKeyframes ( a, b, delta, duration, ease, fn, node, style ) {
var id = '__svelte' + ~~( Math.random() * 1e9 ); // TODO make this more robust
var keyframes = '@keyframes ' + id + '{\n';
for ( var p = 0; p <= 1; p += 16.666 / duration ) {
var t = a + delta * ease( p );
keyframes += ( p * 100 ) + '%{' + fn( t ) + '}\n';
}
keyframes += '100% {' + fn( b ) + '}\n}';
style.textContent += keyframes;
document.head.appendChild( style );
node.style.animation = node.style.animation.split( ',' )
.filter( function ( anim ) {
// when introing, discard old animations if there are any
return anim && ( delta < 0 || !/__svelte/.test( anim ) );
})
.concat( id + ' ' + duration + 'ms linear 1 forwards' )
.join( ', ' );
}
export function wrapTransition ( node, fn, params, intro, outgroup ) {
var obj = fn( node, params, intro );
var duration = obj.duration || 300;
var ease = obj.easing || linear;
// TODO share <style> tag between all transitions?
if ( obj.css ) {
var style = document.createElement( 'style' );
}
if ( intro && obj.tick ) obj.tick( 0 );
return {
start: null,
end: null,
a: null,
b: null,
d: null,
running: false,
t: intro ? 0 : 1,
callback: null,
run: function ( a, b, callback ) {
this.a = a;
this.b = b;
this.delta = b - a;
this.start = window.performance.now() + ( obj.delay || 0 );
this.duration = duration * Math.abs( b - a );
this.end = this.start + this.duration;
this.callback = callback;
if ( obj.css ) {
generateKeyframes( this.a, this.b, this.delta, this.duration, ease, obj.css, node, style );
}
if ( !this.running ) {
this.running = true;
transitionManager.add( this );
}
},
update: function ( now ) {
var p = now - this.start;
this.t = this.a + this.delta * ease( p / this.duration );
if ( obj.tick ) obj.tick( this.t );
},
done: function () {
if ( obj.tick ) obj.tick( intro ? 1 : 0 );
if ( obj.css ) document.head.removeChild( style );
this.callback();
this.running = false;
},
abort: function () {
if ( !intro && obj.tick ) obj.tick( 1 ); // reset css for intro
if ( obj.css ) document.head.removeChild( style );
this.running = false;
}
};
}
export var transitionManager = {
running: false,
transitions: [],
add: function ( transition ) {
transitionManager.transitions.push( transition );
if ( !this.running ) {
this.running = true;
this.next();
}
},
next: function () {
transitionManager.running = false;
var now = window.performance.now();
var i = transitionManager.transitions.length;
while ( i-- ) {
var transition = transitionManager.transitions[i];
if ( transition.running ) {
if ( now >= transition.end ) {
transition.running = false;
transition.done();
} else if ( now > transition.start ) {
transition.update( now );
}
transitionManager.running = true;
} else {
transitionManager.transitions.splice( i, 1 );
}
}
if ( transitionManager.running ) {
requestAnimationFrame( transitionManager.next );
}
}
};

@ -0,0 +1,10 @@
export function noop () {}
export function assign ( target ) {
for ( var i = 1; i < arguments.length; i += 1 ) {
var source = arguments[i];
for ( var k in source ) target[k] = source[k];
}
return target;
}

@ -9,13 +9,24 @@ export default function deindent ( strings, ...values ) {
let trailingIndentation = getTrailingIndentation( result );
for ( let i = 1; i < strings.length; i += 1 ) {
const value = String( values[ i - 1 ] ).replace( /\n/g, `\n${trailingIndentation}` );
result += value + strings[i].replace( pattern, '' );
const expression = values[ i - 1 ];
const string = strings[i].replace( pattern, '' );
if ( expression || expression === '' ) {
const value = String( expression ).replace( /\n/g, `\n${trailingIndentation}` );
result += value + string;
}
else {
let c = result.length;
while ( /\s/.test( result[ c - 1 ] ) ) c -= 1;
result = result.slice( 0, c ) + string;
}
trailingIndentation = getTrailingIndentation( result );
}
return result.trim();
return result.trim().replace( /\t+$/gm, '' );
}
function getTrailingIndentation ( str ) {

@ -0,0 +1,44 @@
import deindent from './deindent.js';
export default function toSource ( thing ) {
if ( typeof thing === 'function' ) {
return normaliseIndentation( thing.toString() );
}
if ( Array.isArray( thing ) ) {
if ( thing.length === 0 ) return '[]';
throw new Error( 'TODO' ); // not currently needed
}
if ( thing && typeof thing === 'object' ) {
const keys = Object.keys( thing );
if ( keys.length === 0 ) return '{}';
const props = keys.map( key => `${key}: ${toSource( thing[ key ] )}` ).join( ',\n' );
return deindent`
{
${props}
}
`;
}
return JSON.stringify( thing );
}
function normaliseIndentation ( str ) {
const lines = str.split( '\n' ).slice( 1, -1 );
let minIndentation = Infinity;
lines.forEach( line => {
if ( !/\S/.test( line ) ) return;
const indentation = /^\t*/.exec( line )[0].length;
if ( indentation < minIndentation ) minIndentation = indentation;
});
if ( minIndentation !== Infinity && minIndentation !== 1 ) {
const pattern = new RegExp( `^\\t{${minIndentation - 1}}`, 'gm' );
return str.replace( pattern, '' );
}
return str;
}

@ -9,6 +9,7 @@ import methods from './methods.js';
import components from './components.js';
import events from './events.js';
import namespace from './namespace.js';
import transitions from './transitions.js';
export default {
data,
@ -21,5 +22,6 @@ export default {
methods,
components,
events,
namespace
namespace,
transitions
};

@ -0,0 +1,17 @@
import checkForDupes from '../utils/checkForDupes.js';
import checkForComputedKeys from '../utils/checkForComputedKeys.js';
export default function transitions ( validator, prop ) {
if ( prop.value.type !== 'ObjectExpression' ) {
validator.error( `The 'transitions' property must be an object literal`, prop.start );
return;
}
checkForDupes( validator, prop.value.properties );
checkForComputedKeys( validator, prop.value.properties );
prop.value.properties.forEach( () => {
// TODO probably some validation that can happen here...
// checking for use of `this` etc?
});
}

@ -55,7 +55,7 @@ function SvelteComponent ( options ) {
this._handlers = Object.create( null );
this._root = options._root;
this._root = options._root || this;
this._yield = options._yield;
this._torndown = false;

@ -38,7 +38,7 @@ function SvelteComponent ( options ) {
this._handlers = Object.create( null );
this._root = options._root;
this._root = options._root || this;
this._yield = options._yield;
this._torndown = false;

@ -39,7 +39,6 @@ function create_main_fragment ( state, component ) {
}
destroyEach( each_block_iterations, true, each_block_value.length );
each_block_iterations.length = each_block_value.length;
}
@ -131,7 +130,7 @@ function SvelteComponent ( options ) {
this._handlers = Object.create( null );
this._root = options._root;
this._root = options._root || this;
this._yield = options._yield;
this._torndown = false;

@ -51,7 +51,7 @@ function SvelteComponent ( options ) {
this._handlers = Object.create( null );
this._root = options._root;
this._root = options._root || this;
this._yield = options._yield;
this._torndown = false;

@ -80,7 +80,7 @@ function SvelteComponent ( options ) {
this._handlers = Object.create( null );
this._root = options._root;
this._root = options._root || this;
this._yield = options._yield;
this._torndown = false;

@ -61,7 +61,7 @@ function SvelteComponent ( options ) {
this._handlers = Object.create( null );
this._root = options._root;
this._root = options._root || this;
this._yield = options._yield;
this._torndown = false;

@ -211,7 +211,7 @@ function SvelteComponent ( options ) {
this._handlers = Object.create( null );
this._root = options._root;
this._root = options._root || this;
this._yield = options._yield;
this._torndown = false;

@ -0,0 +1,37 @@
{
"hash": 1535528483,
"html": {
"start": 0,
"end": 27,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 27,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 5,
"end": 12,
"type": "Transition",
"name": "fade",
"intro": true,
"outro": false,
"expression": null
}
],
"children": [
{
"start": 13,
"end": 21,
"type": "Text",
"data": "fades in"
}
]
}
]
},
"css": null,
"js": null
}

@ -0,0 +1 @@
<div in:style='{opacity: 0}'>fades in</div>

@ -0,0 +1,65 @@
{
"hash": 3160753914,
"html": {
"start": 0,
"end": 43,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 43,
"type": "Element",
"name": "div",
"attributes": [
{
"start": 5,
"end": 28,
"type": "Transition",
"name": "style",
"intro": true,
"outro": false,
"expression": {
"start": 15,
"end": 27,
"type": "ObjectExpression",
"properties": [
{
"start": 16,
"end": 26,
"type": "Property",
"method": false,
"computed": false,
"shorthand": false,
"kind": "init",
"key": {
"type": "Identifier",
"start": 16,
"end": 23,
"name": "opacity"
},
"value": {
"start": 25,
"end": 26,
"type": "Literal",
"value": 0,
"raw": "0"
}
}
]
}
}
],
"children": [
{
"start": 29,
"end": 37,
"type": "Text",
"data": "fades in"
}
]
}
]
},
"css": null,
"js": null
}

@ -4,6 +4,7 @@ import * as path from 'path';
import * as fs from 'fs';
import * as acorn from 'acorn';
import * as babel from 'babel-core';
import { transitionManager } from '../../shared.js';
import { addLineNumbers, loadConfig, loadSvelte, env, setupHtmlEqual } from '../helpers.js';
@ -90,17 +91,42 @@ describe( 'runtime', () => {
let SvelteComponent;
try {
SvelteComponent = require( `./samples/${dir}/main.html` ).default;
} catch ( err ) {
if ( !config.show ) console.log( addLineNumbers( code ) ); // eslint-disable-line no-console
throw err;
}
let unintendedError = null;
return env()
.then( window => {
// set of hacks to support transition tests
transitionManager.running = false;
transitionManager.transitions = [];
const raf = {
time: 0,
callback: null,
tick: now => {
raf.time = now;
if ( raf.callback ) raf.callback();
}
};
window.performance = { now: () => raf.time };
global.requestAnimationFrame = cb => {
let called = false;
raf.callback = () => {
if ( !called ) {
called = true;
cb();
}
};
};
global.window = window;
try {
SvelteComponent = require( `./samples/${dir}/main.html` ).default;
} catch ( err ) {
if ( !config.show ) console.log( addLineNumbers( code ) ); // eslint-disable-line no-console
throw err;
}
Object.assign = () => {
throw new Error( 'cannot use Object.assign in generated code, as it is not supported everywhere' );
};
@ -146,7 +172,7 @@ describe( 'runtime', () => {
Object.assign = Object_assign;
if ( config.test ) {
config.test( assert, component, target, window );
config.test( assert, component, target, window, raf );
} else {
component.destroy();
assert.equal( target.innerHTML, '' );
@ -173,9 +199,10 @@ describe( 'runtime', () => {
});
});
const shared = path.resolve( 'shared.js' );
describe( 'shared helpers', () => {
fs.readdirSync( 'test/runtime/samples' ).forEach( dir => {
runTest( dir, path.resolve( 'shared.js' ) );
runTest( dir, shared );
});
});

@ -0,0 +1,37 @@
export default {
data: {
name: 'world'
},
test ( assert, component, target, window, raf ) {
global.count = 0;
component.set({ visible: true });
assert.equal( global.count, 1 );
const div = target.querySelector( 'div' );
assert.equal( div.foo, 0 );
raf.tick( 300 );
component.set({ name: 'everybody' });
assert.equal( div.foo, 0.75 );
assert.htmlEqual( div.innerHTML, 'hello everybody!' );
component.set({ visible: false, name: 'again' });
assert.htmlEqual( div.innerHTML, 'hello everybody!' );
raf.tick( 500 );
assert.equal( div.foo, 0.25 );
component.set({ visible: true });
raf.tick( 700 );
assert.equal( div.foo, 0.75 );
assert.htmlEqual( div.innerHTML, 'hello again!' );
raf.tick( 800 );
assert.equal( div.foo, 1 );
raf.tick( 900 );
component.destroy();
}
};

@ -0,0 +1,19 @@
{{#if visible}}
<div transition:foo>hello {{name}}!</div>
{{/if}}
<script>
export default {
transitions: {
foo: function ( node, params ) {
global.count += 1;
return {
duration: 400,
tick: t => {
node.foo = t;
}
};
}
}
};
</script>

@ -0,0 +1,32 @@
export default {
data: {
things: [ 'a', 'b', 'c' ]
},
test ( assert, component, target, window, raf ) {
let divs = target.querySelectorAll( 'div' );
assert.equal( divs[0].foo, 0 );
assert.equal( divs[1].foo, 0 );
assert.equal( divs[2].foo, 0 );
raf.tick( 50 );
assert.equal( divs[0].foo, 0.5 );
assert.equal( divs[1].foo, 0.5 );
assert.equal( divs[2].foo, 0.5 );
component.set({ things: [ 'a', 'b', 'c', 'd' ] });
divs = target.querySelectorAll( 'div' );
assert.equal( divs[0].foo, 0.5 );
assert.equal( divs[1].foo, 0.5 );
assert.equal( divs[2].foo, 0.5 );
assert.equal( divs[3].foo, 0 );
raf.tick( 75 );
assert.equal( divs[0].foo, 0.75 );
assert.equal( divs[1].foo, 0.75 );
assert.equal( divs[2].foo, 0.75 );
assert.equal( divs[3].foo, 0.25 );
component.destroy();
}
};

@ -0,0 +1,18 @@
{{#each things as thing}}
<div in:foo>{{thing}}</div>
{{/each}}
<script>
export default {
transitions: {
foo: function ( node, params ) {
return {
duration: 100,
tick: t => {
node.foo = t;
}
};
}
}
};
</script>

@ -0,0 +1,49 @@
export default {
data: {
things: [
{ name: 'a' },
{ name: 'b' },
{ name: 'c' }
]
},
test ( assert, component, target, window, raf ) {
let divs = target.querySelectorAll( 'div' );
assert.equal( divs[0].foo, 0 );
assert.equal( divs[1].foo, 0 );
assert.equal( divs[2].foo, 0 );
raf.tick( 50 );
assert.equal( divs[0].foo, 0.5 );
assert.equal( divs[1].foo, 0.5 );
assert.equal( divs[2].foo, 0.5 );
component.set({
things: [
{ name: 'a' },
{ name: 'woo!' },
{ name: 'b' },
{ name: 'c' }
]
});
assert.htmlEqual( target.innerHTML, `
<div>a</div>
<div>woo!</div>
<div>b</div>
<div>c</div>
` );
divs = target.querySelectorAll( 'div' );
assert.equal( divs[0].foo, 0.5 );
assert.equal( divs[1].foo, 0 );
assert.equal( divs[2].foo, 0.5 );
assert.equal( divs[3].foo, 0.5 );
raf.tick( 75 );
assert.equal( divs[0].foo, 0.75 );
assert.equal( divs[1].foo, 0.25 );
assert.equal( divs[2].foo, 0.75 );
assert.equal( divs[3].foo, 0.75 );
component.destroy();
}
};

@ -0,0 +1,18 @@
{{#each things as thing @name}}
<div in:foo>{{thing.name}}</div>
{{/each}}
<script>
export default {
transitions: {
foo: function ( node, params ) {
return {
duration: 100,
tick: t => {
node.foo = t;
}
};
}
}
};
</script>

@ -0,0 +1,25 @@
export default {
data: {
things: [
{ name: 'a' },
{ name: 'b' },
{ name: 'c' }
]
},
test ( assert, component, target, window, raf ) {
const divs = target.querySelectorAll( 'div' );
component.set({
things: [
{ name: 'a' },
{ name: 'c' }
]
});
raf.tick( 50 );
assert.equal( divs[0].foo, undefined );
assert.equal( divs[1].foo, 0.5 );
assert.equal( divs[2].foo, undefined );
}
};

@ -0,0 +1,18 @@
{{#each things as thing @name}}
<div out:foo>{{thing.name}}</div>
{{/each}}
<script>
export default {
transitions: {
foo: function ( node, params ) {
return {
duration: 100,
tick: t => {
node.foo = t;
}
};
}
}
};
</script>

@ -0,0 +1,16 @@
export default {
data: {
things: [ 'a', 'b', 'c' ]
},
test ( assert, component, target, window, raf ) {
const divs = target.querySelectorAll( 'div' );
component.set({ things: [ 'a' ] });
raf.tick( 50 );
assert.equal( divs[0].foo, undefined );
assert.equal( divs[1].foo, 0.5 );
assert.equal( divs[2].foo, 0.5 );
}
};

@ -0,0 +1,18 @@
{{#each things as thing}}
<div out:foo>{{thing}}</div>
{{/each}}
<script>
export default {
transitions: {
foo: function ( node, params ) {
return {
duration: 100,
tick: t => {
node.foo = t;
}
};
}
}
};
</script>

@ -0,0 +1,30 @@
export default {
test ( assert, component, target, window, raf ) {
global.count = 0;
component.set({ visible: true });
assert.equal( global.count, 1 );
const div = target.querySelector( 'div' );
assert.equal( div.foo, 0 );
raf.tick( 300 );
assert.equal( div.foo, 0.75 );
component.set({ visible: false });
assert.equal( global.count, 1 );
raf.tick( 500 );
assert.equal( div.foo, 0.25 );
component.set({ visible: true });
raf.tick( 700 );
assert.equal( div.foo, 0.75 );
raf.tick( 800 );
assert.equal( div.foo, 1 );
raf.tick( 900 );
component.destroy();
}
};

@ -0,0 +1,19 @@
{{#if visible}}
<div transition:foo>foo bidi</div>
{{/if}}
<script>
export default {
transitions: {
foo: function ( node, params ) {
global.count += 1;
return {
duration: 400,
tick: t => {
node.foo = t;
}
};
}
}
};
</script>

@ -0,0 +1,46 @@
export default {
test ( assert, component, target, window, raf ) {
component.set({ visible: true });
let div = target.querySelector( 'div' );
assert.equal( div.foo, 0 );
raf.tick( 200 );
assert.equal( div.foo, 0.5 );
raf.tick( 400 );
assert.equal( div.foo, 1 );
raf.tick( 500 );
assert.equal( div.foo, 1 );
component.set({ visible: false });
raf.tick( 600 );
assert.equal( div.foo, 1 );
assert.equal( div.bar, 0.75 );
raf.tick( 900 );
assert.equal( div.foo, 1 );
assert.equal( div.bar, 0 );
// test outro before intro complete
raf.tick( 1000 );
component.set({ visible: true });
div = target.querySelector( 'div' );
raf.tick( 1200 );
assert.equal( div.foo, 0.5 );
component.set({ visible: false });
raf.tick( 1300 );
assert.equal( div.foo, 0.75 );
assert.equal( div.bar, 0.75 );
raf.tick( 1400 );
assert.equal( div.foo, 1 );
assert.equal( div.bar, 0.5 );
raf.tick( 2000 );
component.destroy();
}
};

@ -0,0 +1,27 @@
{{#if visible}}
<div in:foo out:bar>foo then bar</div>
{{/if}}
<script>
export default {
transitions: {
foo: function ( node, params ) {
return {
duration: 400,
tick: t => {
node.foo = t;
}
};
},
bar: function ( node, params ) {
return {
duration: 400,
tick: t => {
node.bar = t;
}
};
}
}
};
</script>

@ -0,0 +1,17 @@
export default {
test ( assert, component, target, window, raf ) {
component.set({ visible: true });
const div = target.querySelector( 'div' );
assert.equal( window.getComputedStyle( div ).opacity, 0 );
raf.tick( 200 );
assert.equal( window.getComputedStyle( div ).opacity, 0.5 );
raf.tick( 400 );
assert.equal( window.getComputedStyle( div ).opacity, 1 );
raf.tick( 500 );
component.destroy();
}
};

@ -0,0 +1,18 @@
{{#if visible}}
<div in:fade>fades in</div>
{{/if}}
<script>
export default {
transitions: {
fade: function ( node, params ) {
return {
duration: 400,
tick: t => {
node.style.opacity = t;
}
};
}
}
};
</script>

@ -0,0 +1,21 @@
export default {
test ( assert, component, target, window, raf ) {
assert.equal( target.querySelector( 'div' ), component.refs.no );
assert.equal( component.refs.no.foo, 0 );
raf.tick( 200 );
assert.equal( component.refs.no.foo, 0.5 );
raf.tick( 500 );
component.set({ x: true });
assert.equal( component.refs.no, undefined );
assert.equal( component.refs.yes.foo, 0 );
raf.tick( 700 );
assert.equal( component.refs.yes.foo, 0.5 );
raf.tick( 1000 );
component.destroy();
}
};

@ -0,0 +1,20 @@
{{#if x}}
<div ref:yes in:foo>yes</div>
{{else}}
<div ref:no in:foo>no</div>
{{/if}}
<script>
export default {
transitions: {
foo: function ( node, params ) {
return {
duration: 400,
tick: t => {
node.foo = t;
}
};
}
}
};
</script>

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save