Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
intro.start and outro.start events don't fire
We do get `intro.end` and `outro.end` events though


Discussion:

Resolving patch:
diff --git a/mocha.opts b/mocha.opts
--- a/mocha.opts
+++ b/mocha.opts
@@ -1,2 +1 @@
---bail
 test/test.js
\ No newline at end of file
diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts
--- a/src/generators/dom/index.ts
+++ b/src/generators/dom/index.ts
@@ -122,10 +122,9 @@ export default function dom(
 		@dispatchObservers( this, this._observers.pre, newState, oldState );
 		${block.hasUpdateMethod && `this._fragment.update( newState, this._state );`}
 		@dispatchObservers( this, this._observers.post, newState, oldState );
-		${(generator.hasComponents || generator.hasIntroTransitions) &&
-			`this._flush();`}
-		${generator.hasComplexBindings &&
-			`while ( this._bindings.length ) this._bindings.pop()();`}
+		${generator.hasComponents && `@callAll(this._oncreate);`}
+		${generator.hasComplexBindings && `@callAll(this._bindings);`}
+		${generator.hasIntroTransitions && `@callAll(this._postcreate);`}
 	`;
 
 	if (hasJs) {
@@ -158,7 +157,7 @@ export default function dom(
 		? `@proto `
 		: deindent`
 		{
-			${['get', 'fire', 'observe', 'on', 'set', '_flush']
+			${['get', 'fire', 'observe', 'on', 'set']
 				.map(n => `${n}: @${n}`)
 				.join(',\n')}
 		}`;
@@ -199,9 +198,9 @@ export default function dom(
 			${generator.css &&
 				options.css !== false &&
 				`if ( !document.getElementById( '${generator.cssId}-style' ) ) @add_css();`}
-			${(generator.hasComponents || generator.hasIntroTransitions) &&
-				`this._oncreate = [];`}
+			${generator.hasComponents && `this._oncreate = [];`}
 			${generator.hasComplexBindings && `this._bindings = [];`}
+			${generator.hasIntroTransitions && `this._postcreate = [];`}
 
 			this._fragment = @create_main_fragment( this._state, this );
 
@@ -219,19 +218,17 @@ export default function dom(
 				this._fragment.${block.hasIntroMethod ? 'intro' : 'mount'}( options.target, null );
 			}
 			
-			${(generator.hasComponents || generator.hasIntroTransitions) &&
-				`this._flush();`}
-			${generator.hasComplexBindings &&
-				`while ( this._bindings.length ) this._bindings.pop()();`}
+			${generator.hasComponents && `@callAll(this._oncreate);`}
+			${generator.hasComplexBindings && `@callAll(this._bindings);`}
 
-			${templateProperties.oncreate &&
-				deindent`
+			${templateProperties.oncreate && deindent`
 				if ( options._root ) {
 					options._root._oncreate.push( @template.oncreate.bind( this ) );
 				} else {
 					@template.oncreate.call( this );
-				}
-			`}
+				}`}
+
+			${generator.hasIntroTransitions && `@callAll(this._postcreate);`}
 		}
 
 		@assign( ${prototypeBase}, ${proto});
diff --git a/src/generators/dom/visitors/Element/addTransitions.ts b/src/generators/dom/visitors/Element/addTransitions.ts
--- a/src/generators/dom/visitors/Element/addTransitions.ts
+++ b/src/generators/dom/visitors/Element/addTransitions.ts
@@ -23,8 +23,8 @@ export default function addTransitions(
 		const fn = `@template.transitions.${intro.name}`;
 
 		block.builders.intro.addBlock(deindent`
-			#component._oncreate.push( function () {
-				if ( !${name} ) ${name} = @wrapTransition( ${state.name}, ${fn}, ${snippet}, true, null );
+			#component._postcreate.push( function () {
+				if ( !${name} ) ${name} = @wrapTransition( #component, ${state.name}, ${fn}, ${snippet}, true, null );
 				${name}.run( true, function () {
 					#component.fire( 'intro.end', { node: ${state.name} });
 				});
@@ -58,8 +58,8 @@ export default function addTransitions(
 			}
 
 			block.builders.intro.addBlock(deindent`
-				#component._oncreate.push( function () {
-					${introName} = @wrapTransition( ${state.name}, ${fn}, ${snippet}, true, null );
+				#component._postcreate.push( function () {
+					${introName} = @wrapTransition( #component, ${state.name}, ${fn}, ${snippet}, true, null );
 					${introName}.run( true, function () {
 						#component.fire( 'intro.end', { node: ${state.name} });
 					});
@@ -78,7 +78,7 @@ export default function addTransitions(
 			// 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} = @wrapTransition( #component, ${state.name}, ${fn}, ${snippet}, false, null );
 				${outroName}.run( false, function () {
 					#component.fire( 'outro.end', { node: ${state.name} });
 					if ( --#outros === 0 ) #outrocallback();
diff --git a/src/shared/index.js b/src/shared/index.js
--- a/src/shared/index.js
+++ b/src/shared/index.js
@@ -106,15 +106,11 @@ export function onDev(eventName, handler) {
 
 export function set(newState) {
 	this._set(assign({}, newState));
-	this._root._flush();
+	callAll(this._root._oncreate);
 }
 
-export function _flush() {
-	if (!this._oncreate) return;
-
-	while (this._oncreate.length) {
-		this._oncreate.pop()();
-	}
+export function callAll(fns) {
+	while (fns && fns.length) fns.pop()();
 }
 
 export var proto = {
@@ -122,8 +118,7 @@ export var proto = {
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set,
-	_flush: _flush
+	set: set
 };
 
 export var protoDev = {
@@ -131,6 +126,5 @@ export var protoDev = {
 	fire: fire,
 	observe: observeDev,
 	on: onDev,
-	set: set,
-	_flush: _flush
+	set: set
 };
diff --git a/src/shared/transitions.js b/src/shared/transitions.js
--- a/src/shared/transitions.js
+++ b/src/shared/transitions.js
@@ -32,7 +32,7 @@ export function hash(str) {
 	return hash >>> 0;
 }
 
-export function wrapTransition(node, fn, params, intro, outgroup) {
+export function wrapTransition(component, node, fn, params, intro, outgroup) {
 	var obj = fn(node, params);
 	var duration = obj.duration || 300;
 	var ease = obj.easing || linear;
@@ -78,6 +78,8 @@ export function wrapTransition(node, fn, params, intro, outgroup) {
 			}
 		},
 		start: function(program) {
+			component.fire(program.intro ? 'intro.start' : 'outro.start', { node: node });
+
 			program.a = this.t;
 			program.b = program.intro ? 1 : 0;
 			program.delta = program.b - program.a;
@@ -149,7 +151,7 @@ export var transitionManager = {
 
 		if (!this.running) {
 			this.running = true;
-			this.next();
+			requestAnimationFrame(this.bound || (this.bound = this.next.bind(this)));
 		}
 	},
 
@@ -186,7 +188,7 @@ export var transitionManager = {
 		}
 
 		if (this.running) {
-			requestAnimationFrame(this.bound || (this.bound = this.next.bind(this)));
+			requestAnimationFrame(this.bound);
 		} else if (this.stylesheet) {
 			var i = this.stylesheet.cssRules.length;
 			while (i--) this.stylesheet.deleteRule(i);


Test patch:
diff --git a/test/js/samples/collapses-text-around-comments/expected-bundle.js b/test/js/samples/collapses-text-around-comments/expected-bundle.js
--- a/test/js/samples/collapses-text-around-comments/expected-bundle.js
+++ b/test/js/samples/collapses-text-around-comments/expected-bundle.js
@@ -115,15 +115,11 @@ function on(eventName, handler) {
 
 function set(newState) {
 	this._set(assign({}, newState));
-	this._root._flush();
+	callAll(this._root._oncreate);
 }
 
-function _flush() {
-	if (!this._oncreate) return;
-
-	while (this._oncreate.length) {
-		this._oncreate.pop()();
-	}
+function callAll(fns) {
+	while (fns && fns.length) fns.pop()();
 }
 
 var proto = {
@@ -131,8 +127,7 @@ var proto = {
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set,
-	_flush: _flush
+	set: set
 };
 
 var template = (function () {
diff --git a/test/js/samples/computed-collapsed-if/expected-bundle.js b/test/js/samples/computed-collapsed-if/expected-bundle.js
--- a/test/js/samples/computed-collapsed-if/expected-bundle.js
+++ b/test/js/samples/computed-collapsed-if/expected-bundle.js
@@ -91,15 +91,11 @@ function on(eventName, handler) {
 
 function set(newState) {
 	this._set(assign({}, newState));
-	this._root._flush();
+	callAll(this._root._oncreate);
 }
 
-function _flush() {
-	if (!this._oncreate) return;
-
-	while (this._oncreate.length) {
-		this._oncreate.pop()();
-	}
+function callAll(fns) {
+	while (fns && fns.length) fns.pop()();
 }
 
 var proto = {
@@ -107,8 +103,7 @@ var proto = {
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set,
-	_flush: _flush
+	set: set
 };
 
 function recompute ( state, newState, oldState, isInitial ) {
diff --git a/test/js/samples/each-block-changed-check/expected-bundle.js b/test/js/samples/each-block-changed-check/expected-bundle.js
--- a/test/js/samples/each-block-changed-check/expected-bundle.js
+++ b/test/js/samples/each-block-changed-check/expected-bundle.js
@@ -124,15 +124,11 @@ function on(eventName, handler) {
 
 function set(newState) {
 	this._set(assign({}, newState));
-	this._root._flush();
+	callAll(this._root._oncreate);
 }
 
-function _flush() {
-	if (!this._oncreate) return;
-
-	while (this._oncreate.length) {
-		this._oncreate.pop()();
-	}
+function callAll(fns) {
+	while (fns && fns.length) fns.pop()();
 }
 
 var proto = {
@@ -140,8 +136,7 @@ var proto = {
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set,
-	_flush: _flush
+	set: set
 };
 
 function create_main_fragment ( state, component ) {
diff --git a/test/js/samples/event-handlers-custom/expected-bundle.js b/test/js/samples/event-handlers-custom/expected-bundle.js
--- a/test/js/samples/event-handlers-custom/expected-bundle.js
+++ b/test/js/samples/event-handlers-custom/expected-bundle.js
@@ -109,15 +109,11 @@ function on(eventName, handler) {
 
 function set(newState) {
 	this._set(assign({}, newState));
-	this._root._flush();
+	callAll(this._root._oncreate);
 }
 
-function _flush() {
-	if (!this._oncreate) return;
-
-	while (this._oncreate.length) {
-		this._oncreate.pop()();
-	}
+function callAll(fns) {
+	while (fns && fns.length) fns.pop()();
 }
 
 var proto = {
@@ -125,8 +121,7 @@ var proto = {
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set,
-	_flush: _flush
+	set: set
 };
 
 var template = (function () {
diff --git a/test/js/samples/if-block-no-update/expected-bundle.js b/test/js/samples/if-block-no-update/expected-bundle.js
--- a/test/js/samples/if-block-no-update/expected-bundle.js
+++ b/test/js/samples/if-block-no-update/expected-bundle.js
@@ -115,15 +115,11 @@ function on(eventName, handler) {
 
 function set(newState) {
 	this._set(assign({}, newState));
-	this._root._flush();
+	callAll(this._root._oncreate);
 }
 
-function _flush() {
-	if (!this._oncreate) return;
-
-	while (this._oncreate.length) {
-		this._oncreate.pop()();
-	}
+function callAll(fns) {
+	while (fns && fns.length) fns.pop()();
 }
 
 var proto = {
@@ -131,8 +127,7 @@ var proto = {
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set,
-	_flush: _flush
+	set: set
 };
 
 function create_main_fragment ( state, component ) {
diff --git a/test/js/samples/if-block-simple/expected-bundle.js b/test/js/samples/if-block-simple/expected-bundle.js
--- a/test/js/samples/if-block-simple/expected-bundle.js
+++ b/test/js/samples/if-block-simple/expected-bundle.js
@@ -115,15 +115,11 @@ function on(eventName, handler) {
 
 function set(newState) {
 	this._set(assign({}, newState));
-	this._root._flush();
+	callAll(this._root._oncreate);
 }
 
-function _flush() {
-	if (!this._oncreate) return;
-
-	while (this._oncreate.length) {
-		this._oncreate.pop()();
-	}
+function callAll(fns) {
+	while (fns && fns.length) fns.pop()();
 }
 
 var proto = {
@@ -131,8 +127,7 @@ var proto = {
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set,
-	_flush: _flush
+	set: set
 };
 
 function create_main_fragment ( state, component ) {
diff --git a/test/js/samples/non-imported-component/expected-bundle.js b/test/js/samples/non-imported-component/expected-bundle.js
--- a/test/js/samples/non-imported-component/expected-bundle.js
+++ b/test/js/samples/non-imported-component/expected-bundle.js
@@ -103,15 +103,11 @@ function on(eventName, handler) {
 
 function set(newState) {
 	this._set(assign({}, newState));
-	this._root._flush();
+	callAll(this._root._oncreate);
 }
 
-function _flush() {
-	if (!this._oncreate) return;
-
-	while (this._oncreate.length) {
-		this._oncreate.pop()();
-	}
+function callAll(fns) {
+	while (fns && fns.length) fns.pop()();
 }
 
 var proto = {
@@ -119,8 +115,7 @@ var proto = {
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set,
-	_flush: _flush
+	set: set
 };
 
 var template = (function () {
@@ -192,7 +187,7 @@ function SvelteComponent ( options ) {
 		this._fragment.mount( options.target, null );
 	}
 
-	this._flush();
+	callAll(this._oncreate);
 }
 
 assign( SvelteComponent.prototype, proto );
@@ -202,7 +197,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	this._state = assign( {}, oldState, newState );
 	dispatchObservers( this, this._observers.pre, newState, oldState );
 	dispatchObservers( this, this._observers.post, newState, oldState );
-	this._flush();
+	callAll(this._oncreate);
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
diff --git a/test/js/samples/non-imported-component/expected.js b/test/js/samples/non-imported-component/expected.js
--- a/test/js/samples/non-imported-component/expected.js
+++ b/test/js/samples/non-imported-component/expected.js
@@ -1,6 +1,6 @@
 import Imported from 'Imported.html';
 
-import { assign, createText, detachNode, dispatchObservers, insertNode, proto } from "svelte/shared.js";
+import { assign, callAll, createText, detachNode, dispatchObservers, insertNode, proto } from "svelte/shared.js";
 
 var template = (function () {
 	return {
@@ -71,7 +71,7 @@ function SvelteComponent ( options ) {
 		this._fragment.mount( options.target, null );
 	}
 
-	this._flush();
+	callAll(this._oncreate);
 }
 
 assign( SvelteComponent.prototype, proto );
@@ -81,7 +81,7 @@ SvelteComponent.prototype._set = function _set ( newState ) {
 	this._state = assign( {}, oldState, newState );
 	dispatchObservers( this, this._observers.pre, newState, oldState );
 	dispatchObservers( this, this._observers.post, newState, oldState );
-	this._flush();
+	callAll(this._oncreate);
 };
 
 SvelteComponent.prototype.teardown = SvelteComponent.prototype.destroy = function destroy ( detach ) {
diff --git a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
--- a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
+++ b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
@@ -91,15 +91,11 @@ function on(eventName, handler) {
 
 function set(newState) {
 	this._set(assign({}, newState));
-	this._root._flush();
+	callAll(this._root._oncreate);
 }
 
-function _flush() {
-	if (!this._oncreate) return;
-
-	while (this._oncreate.length) {
-		this._oncreate.pop()();
-	}
+function callAll(fns) {
+	while (fns && fns.length) fns.pop()();
 }
 
 var proto = {
@@ -107,8 +103,7 @@ var proto = {
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set,
-	_flush: _flush
+	set: set
 };
 
 var template = (function () {
diff --git a/test/js/samples/use-elements-as-anchors/expected-bundle.js b/test/js/samples/use-elements-as-anchors/expected-bundle.js
--- a/test/js/samples/use-elements-as-anchors/expected-bundle.js
+++ b/test/js/samples/use-elements-as-anchors/expected-bundle.js
@@ -115,15 +115,11 @@ function on(eventName, handler) {
 
 function set(newState) {
 	this._set(assign({}, newState));
-	this._root._flush();
+	callAll(this._root._oncreate);
 }
 
-function _flush() {
-	if (!this._oncreate) return;
-
-	while (this._oncreate.length) {
-		this._oncreate.pop()();
-	}
+function callAll(fns) {
+	while (fns && fns.length) fns.pop()();
 }
 
 var proto = {
@@ -131,8 +127,7 @@ var proto = {
 	fire: fire,
 	observe: observe,
 	on: on,
-	set: set,
-	_flush: _flush
+	set: set
 };
 
 function create_main_fragment ( state, component ) {
diff --git a/test/runtime/samples/transition-js-events/_config.js b/test/runtime/samples/transition-js-events/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-events/_config.js
@@ -0,0 +1,35 @@
+export default {
+	data: {
+		visible: true,
+		things: ['a', 'b', 'c', 'd']
+	},
+
+	test (assert, component, target, window, raf) {
+		raf.tick(50);
+		assert.deepEqual(component.intros.sort(), ['a', 'b', 'c', 'd']);
+		assert.equal(component.introCount, 4);
+
+		raf.tick(100);
+		assert.equal(component.introCount, 0);
+
+		component.set({ visible: false });
+
+		raf.tick(150);
+		assert.deepEqual(component.outros.sort(), ['a', 'b', 'c', 'd']);
+		assert.equal(component.outroCount, 4);
+
+		raf.tick(200);
+		assert.equal(component.outroCount, 0);
+
+		component.set({ visible: true });
+		component.on('intro.start', () => {
+			throw new Error(`intro.start should fire during set(), not after`);
+		});
+
+		raf.tick(250);
+		assert.deepEqual(component.intros.sort(), ['a', 'a', 'b', 'b', 'c', 'c', 'd', 'd']);
+		assert.equal(component.introCount, 4);
+
+		component.destroy();
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/transition-js-events/main.html b/test/runtime/samples/transition-js-events/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/transition-js-events/main.html
@@ -0,0 +1,45 @@
+{{#each things as thing}}
+	{{#if visible}}
+		<p transition:foo>{{thing}}</p>
+	{{/if}}
+{{/each}}
+
+<script>
+	export default {
+		transitions: {
+			foo: function ( node, params ) {
+				return {
+					duration: 100,
+					tick: t => {
+						node.foo = t;
+					}
+				};
+			}
+		},
+
+		oncreate() {
+			this.intros = [];
+			this.outros = [];
+			this.introCount = 0;
+			this.outroCount = 0;
+
+			this.on('intro.start', ({ node }) => {
+				this.intros.push(node.textContent);
+				this.introCount += 1;
+			});
+
+			this.on('intro.end', () => {
+				this.introCount -= 1;
+			});
+
+			this.on('outro.start', ({ node }) => {
+				this.outros.push(node.textContent);
+				this.outroCount += 1;
+			});
+
+			this.on('outro.end', () => {
+				this.outroCount -= 1;
+			});
+		}
+	};
+</script>
\ No newline at end of file