Earlier this week, I needed a way to make users wait for a certain amount of time after performing an action on a web application. The Angular UI progress bar seemed like a fitting method to let the user know their wait time.
JavaScript timers, and specifically Angular’s $timeout service, provide easy mechanisms to set a timeout. We can also have actions/methods repeat at a specific interval. This lends itself very nicely to waiting a specific amount of time, while updating a progress indicator at regular intervals.
We can encapsulate an overall handler for altering and maintaining a timer in an Angular service. Really, all we want this service to do is start a timer and raise an event when the timer is finished. The service also needs to let us stop/start/reset the timer.
var timerService = function ($rootScope, $timeout, $log) { var idleTimer = null, expireTime = 10000, startTimer = function () { $log.log('Starting timer'); idleTimer = $timeout(timerExpiring, expireTime); }, stopTimer = function () { if (idleTimer) { $timeout.cancel(idleTimer); } }, resetTimer = function (timeoutSeconds) { expireTime = typeof (timeoutSeconds) === 'undefined' ? 10000 : timeoutSeconds * 1000; stopTimer(); startTimer(); }, timerExpiring = function () { stopTimer(); $rootScope.$broadcast('timeoutExpired'); $log.log('Timer expiring ..'); }; startTimer(); return { startTimer: startTimer, stopTimer: stopTimer, resetTimer: resetTimer }; };
Angular’s $timeout service provides us with all of the methods we need for stopping/starting and knowing when our specified interval is complete. We basically, just as in plain JavaScript’s setInterval, give $timeout a callback method to execute when we reach expiration. You can see in the code above that when this occurs, an event is being broadcast on the $rootScope.
Taking this into consideration in an Angular controller, we can listen for this event. However, we need to also determine how frequently we want to update our progress. The percentage completion needs to be calculated and passed to the UI to update the progress accordingly. Since this is a time-based example, we can determine how much time has elapsed with simple JavaScript date objects.
The controller I’m using looks like this:
var myCtrl = function ($scope, $sce, $timeout, $animate, $log, timerService, dialogService) { var vm = this, myTimeout, max = 10, // seconds interval = 0.0001, // seconds updateSeconds, timestamp, elapsedTime, startCounter = function() { vm.counter = 0; vm.percentage = 0; timestamp = new Date() / 1000.0; myTimeout = $timeout(incrementCounter, updateSeconds); timerService.resetTimer(max); }, resetCounter = function() { vm.progressUpdateInterval = interval = Math.max(vm.progressUpdateInterval, 0.01); updateSeconds = interval * 1000; max = vm.progressSeconds; $timeout.cancel(myTimeout) startCounter(); }, incrementCounter = function() { vm.counter += interval; vm.percentage = Math.min(parseInt((vm.counter / max) * 100), 100.0); var currentTimestamp = new Date() / 1000.0; vm.counter = currentTimestamp - timestamp; vm.percentage = Math.min(parseInt((vm.counter / max) * 100), 100.0); myTimeout = $timeout(incrementCounter, updateSeconds); }, init = function() { vm.progressUpdateInterval = interval = Math.max(interval, 0.01); updateSeconds = interval * 1000; vm.progressSeconds = max; startCounter(); vm.resetCounter = resetCounter; $scope.$on('timeoutExpired', function() { vm.percentage = 100.0; $timeout.cancel(myTimeout) var modalTitle = 'Done!'; var modalBody = 'Your wait has ended. Relax!'; dialogService.openDialog("modalGeneral.html", ['$scope', '$uibModalInstance', function ($modalScope, $uibModalInstance) { $modalScope.modalHeader = $sce.trustAsHtml(modalTitle); $modalScope.modalBody = $sce.trustAsHtml(dialogService.stringFormat("<p><strong>{0}</strong></p>", modalBody)); $modalScope.ok = function () { $uibModalInstance.close(); makeRequest(); }; $modalScope.hasCancel = false; $modalScope.cancel = function () { $uibModalInstance.close(); }; }]); }); }, makeRequest = function () { timerService.resetTimer(); resetCounter(); }; init(); };
The salient points here are that the controller is using it’s own $timeout to determine how frequently to update the progress. When that timeout is triggered, we look at the total number of seconds that have expired since the timer was started. Using the total amount of time the user will wait, a percentage completion is calculated. When the service event is triggered indicating overall completion, a dialog is displayed to the user. When they click OK, the process starts all over again.
Creating this demo was a pretty fun little side project. An in-line working interactive demo is below.
The full source can be seen on Github.
Worked like a charm! Thanks for sharing the code, Stephen.
You’re very welcome! Incidentally, I actually wound up revisiting this recently and used it in a project in conjunction with displaying stored procedure progress.