Scope issues with the callback to JQuery’s animate() function

The fine folks over at the JQuery Google Group helped me solve this problem.

I wanted to create an animation with JQuery’s animate() function, but I did not want to use setTimeout(). My reasoning for this was that setTimeout() can only call functions that are within the global namespace, and I try to avoid anything global for all the well-documented reasons.  I thought about using call() to change the invocation context, but setTimeout.call(object) will not work, because setTimeout() returns void.

So I was going to use the callback within animate() to synchronously loop back and continue the animation.  But I could not find a way to reference the original context of the function where animate() gets called.  Perhaps some code would explain this better…

This is what I was trying to do:

$(document).ready(function(){

	//control toggle
	var startStop = 1;

	//object for creating animated elements that fade in or out while moving to a random point.
	var Animation = function(elementID){ 

		//opa is opacity at the end of the fade
		this.opa = 1; 

		this.floatAround = function(){
		    var top = Math.random() * $('#content').height();
		    var left = Math.random() * $('#content').width();
			$(elementID).animate(
					{	opacity:(this.opa),
						marginTop:top+'px',
						marginLeft:left+'px'
					},
					1000,
					'linear',
					this.callback
			);
			this.opa = this.opa > 0 ? 0 : 1; //toggle: fade in or fade out
		};

		this.callback = function(){
			//alert(this); //test
			//this test will show that
			//"this" refers to the HTML Element that animate() works on.
			//That isn't what we want.
			//We want to refer to the instance of the Animation object
			//so we can call floatAround() again.

			if(startStop == 1){
				this.floatAround(); //won't work.  see above.
			}
		};
	};

	//user may click on the containing div to stop the animation.
	$('#content').toggle(
		function(){
			startStop = 0;
		},
		function(){
			startStop = 1;
		}
	);

	//start the animation
	var floater = new Animation('#animate_me0'); //create a new Animation object
	floater.floatAround(); //cause the the object to start floating around
});

So after some discussion on the JQuery Google Group, a kind soul named mkmanning set me straight.  All I had to do was to save a reference (var _this = this;) to the Animation object inside the Animation object, and then this.callback would be able to refer to it (with “_this”).  Here is the revised code:

$(document).ready(function(){

	//control toggle
	var startStop = 1;

	//object for creating animated elements that fade in or out while moving to a random point.
	var Animation = function(elementID){

		//store a reference to the instance of the Animation object,
		//so we can invoke floatAround from the callback.
		var _this = this; 

		//opa is opacity at the end of the fade
		this.opa = 1; 

		this.floatAround = function(){
		    var top = Math.random() * $('#content').height();
		    var left = Math.random() * $('#content').width();
			$(elementID).animate(
					{	opacity:(this.opa),
						marginTop:top+'px',
						marginLeft:left+'px'
					},
					1000,
					'linear',
					this.callback
			);
			this.opa = this.opa > 0 ? 0 : 1; //toggle: fade in or fade out
		};

		this.callback = function(){
			if(startStop == 1){
				_this.floatAround(); //loops!  works great!
			}
		};
	};

	//user may click on the containing div to stop the animation.
	$('#content').toggle(
		function(){
			startStop = 0;
		},
		function(){
			startStop = 1;
		}
	);

	//start the animation
	var floater = new Animation('#animate_me0');
	floater.floatAround();
});

So I learned some things about animate(), callbacks, invocation contexts, and also about call()…

When call() won’t work:

1. the animate() callback must be provided as a reference to a function, rather than as the invocation of a function.  That is, the arguments to animate must appear like this:

$(‘#myElement’).animate({cssIs:awesome}, milliseconds, easing, callback);

and NOT:

$(‘#myElement’).animate({cssIs:awesome}, milliseconds, easing, callback());

Also note that:

callback(), callback(arg1, arg2) and callback.call(context) are all invocations of the callback function, rather than references to it, so they will execute before the animate() function.

2. call() cannot be invoked on a function when the return value is void.  So you can’t call setTimeout() with call(), for example.

This entry was posted in JQuery, JavaScript, Web Development. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>