Creating A Reusable Timer In AngularJS
In my AngularJS applications, I want my controllers to respond to events. But, often times, I don't necessarily want the controller to react to every single event; rather, I'd like to throttle those events such that closely timed events trigger only one reaction. After implementing this logic a good number of times, I wanted to see if I could encapsulate it inside of a reusable timer that could be injected into any of my AngularJS classes.
Run this demo in my JavaScript Demos project on GitHub.
The pattern that I'm talking about looks a bit like this:
<script type="text/javascript">
function prepareToDoSomething() {
if ( timer ) {
clearTimeout( timer );
}
timer = setTimeout( doSomething, duration );
}
var timer = null;
var duration = 2000;
</script>
In this case, when an event occurs, we clear any existing timeout and then re-create it. Basically, I'm "debouncing" the event such that the callback is only invoked at the end of a group of closely timed events. This is the logic that I'd like to simplify.
In AngularJS, we have the $timeout() service, which is already hooked into the $rootScope and the $digest lifecycle. So, rather than trying to re-implement that, I'd like to create a thin wrapper class that uses $timeout internally. This way, my logic is isolated around the actual timing and not around the error handling and digest invocation.
What I've come up with only had a few public methods:
- isActive() - Determines if the timer is currently counting-down.
- start() - Starts the timer.
- stop() - Stops the timer (if its active).
- restart() - Resets the timer and starts it again.
- teardown() - Cleans up object references for garbage collection.
Mostly, what I wanted was the start() and restart() methods, which mimic the code pattern that I outlined above:
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Creating A Reusable Timer In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">
<h1>
Creating A Reusable Timer In AngularJS
</h1>
<p>
<a ng-click="handleClick()">Click me</a> to star the timer!
</p>
<p ng-if="logExecutedAt">
Executed: {{ logExecutedAt.getTime() }}
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.2.22.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope, timer ) {
// I am a timer that will invoke the given callback once the timer has
// finished. The timer can be reset at any time.
// --
// NOTE: Is a thin wrapper around $timeout() which will trigger a $digest
// when the callback is invoked.
var logClickTimer = timer( logClick, timer.TWO_SECONDS );
$scope.logExecutedAt = null;
// When the current scope is destroyed, we want to make sure to stop
// the current timer (if it's still running). And, give it a chance to
// clean up its own internal memory structures.
$scope.$on(
"$destroy",
function() {
logClickTimer.teardown();
}
);
// ---
// PUBLIC METHODS.
// ---
// I handle the click event. Instead of logging the click right away,
// we're going to throttle the click through a timer.
$scope.handleClick = function() {
$scope.logExecutedAt = null;
logClickTimer.restart();
};
// ---
// PRIVATE METHODS.
// ---
// I log the fact that the click happened at this point in time.
function logClick() {
$scope.logExecutedAt = new Date();
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I create timers that wrap the $timeout and provide easy ways to cancel and
// reset the timer.
app.factory(
"timer",
function( $timeout ) {
// I provide a simple wrapper around the core $timeout that allows for
// the timer to be easily reset.
function Timer( callback, duration, invokeApply ) {
// Store properties.
this._callback = callback;
this._duration = ( duration || 0 );
this._invokeApply = ( invokeApply !== false );
// I hold the $timeout promise. This will only be non-null when the
// timer is actively counting down to callback invocation.
this._timer = null;
}
// Define the instance methods.
Timer.prototype = {
// Set constructor to help with instanceof operations.
constructor: Timer,
// I determine if the timer is currently counting down.
isActive: function() {
return( !! this._timer );
},
// I stop (if it is running) and then start the timer again.
restart: function() {
this.stop();
this.start();
},
// I start the timer, which will invoke the callback upon timeout.
start: function() {
var self = this;
// NOTE: Instead of passing the callback directly to the timeout,
// we're going to wrap it in an anonymous function so we can set
// the enable flag. We need to do this approach, rather than
// binding to the .then() event since the .then() will initiate a
// digest, which the user may not want.
this._timer = $timeout(
function handleTimeoutResolve() {
try {
self._callback.call( null );
} finally {
self = self._timer = null;
}
},
this._duration,
this._invokeApply
);
},
// I stop the current timer, if it is running, which will prevent the
// callback from being invoked.
stop: function() {
$timeout.cancel( this._timer );
this._timer = false;
},
// I clean up the internal object references to help garbage
// collection (hopefully).
teardown: function() {
this.stop();
this._callback = null;
this._duration = null;
this._invokeApply = null;
this._timer = null;
}
};
// Create a factory that will call the constructor. This will simplify
// the calling context.
function timerFactory( callback, duration, invokeApply ) {
return( new Timer( callback, duration, invokeApply ) );
}
// Store the actual constructor as a factory property so that it is still
// accessible if anyone wants to use it directly.
timerFactory.Timer = Timer;
// Set up some time-based constants to help readability of code.
timerFactory.ONE_SECOND = ( 1 * 1000 );
timerFactory.TWO_SECONDS = ( 2 * 1000 );
timerFactory.THREE_SECONDS = ( 3 * 1000 );
timerFactory.FOUR_SECONDS = ( 4 * 1000 );
timerFactory.FIVE_SECONDS = ( 5 * 1000 );
// Return the factory.
return( timerFactory );
}
);
</script>
</body>
</html>
As you can see, I have a timer that debounces click events for 2-seconds. And, even though the callback is happening asynchronously, AngularJS still knows about it since the encapsulated $timeout() service is triggering a digest.
Not much else to say, really. This just a fun experiment.
Want to use code from this post? Check out the license.
Reader Comments
Great stuff.
I also love the way you encapsulates the object prototype inside the factory
@Lars,
Thanks my man! I'm seeing that as more of a trend - people not really exposing the "new" operator directly and using a factory that does it for you. I see this in Node.js a lot as well.
@Ben ... Nice demo... I've recently been dabbling with Angular and coming from working with ColdSpring and Spring I'm quite at home with the IoC pattern Angular uses... leaving object creation up to the framework on ... Hollywood yada yada...
Great share ...
@Edward,
Ha ha, the D/I stuff is pretty cool. I've played a bit with FW/1 on the server, so am somewhat familiar with it. But this is the first time I really used it in JavaScript. It's pretty cool! The only issue with it in AngularJS is that the D/I container has to be populated up-front. Meaning, you can't (easily) added new module definitions after the app has started.
I mean, you can sort of do it if you proxy some core methods. But, for the most part, the entire app has to be front-loaded. I think they are working on changing that a bit, though, which will be awesome.
Hello Ben,
this post was really helpful to me, as i did a project involving googleMaps where i wanted to prevent request spamming.
What i did, and therefore i'd like to hear your opinion if this is maybe not the right way...i returned the "_timer" variable to have the possibility of using the promise.
e.g.
$scope.handleActionTimer = function() {
$scope.logExecutedAt = null;
actionTimer.restart();
return actionTimer._timer;
};
$scope.calculateRoute = function() {
$scope.handleActionTimer().then(function() {
...
});
};
For me this solution works perfect, i just wanna know if there is maybe a better way of doing this.
Thanks in advance :)