Context
I'm looking to create a webapp that looks at a set of data as a function of time elapsed since pageload. Think "how many calories have you burnt since opening this webpage".
I'm still trying to wrap my head around AngularJS services, factories, etc, and was wondering what the best way to create an auto-updating Timer that could be used to regularly (per second) manipulate and update ng-model.
How I've (unsuccessfully) imagined it'd work:
I've got something like this at the moment:
app.factory('Timer', function($timeout) {
var time = 0;
var Timer = function() {
this.time++;
this.timeout = $timeout(this.Timer, 1000);
}
});
And use as
$timeout(function() {
$scope.someNgModelVarForTheView = Timer.time * $scope.data;
}, 1000);
But... well. In my mind that works beautifully. In reality screw all happens, and I'm kidding myself if I know the right way to do this...
So I suppose, two questions:
How do you calculate time since pageload, as a callable function?
How do you recalculate the data model on a regular (per second) basis? Is $timeout a good method?
If you want to have your own service, you could do it like this:
.factory('MyTimer', function($interval){
return function(delay){
var initialMs= (new Date()).getTime();
var result = {totalMilliseconds:0, counts:0};
$interval(function() {
result.totalMilliseconds = (new Date()).getTime() - initialMs;
result.counts++;
}, delay);
return result;
};
})
And you could use it like this:
.controller('testController', function($scope, MyTimer){
$scope.t = MyTimer(1000);
});
And in your html you could do this:
<div ng-app="app" ng-controller="testController">
Total Ms: {{t.totalMilliseconds}}
Counts: {{t.counts}}
</div>
Example
Eh, I ended up overcomplicating my thoughts. This did the trick:
var delay = 1000; // 1 sec
$scope.currentTime = 0;
$interval(function() {
$scope.currentTime += delay;
$scope.someData *= $scope.currentTime;
}, delay);
Related
I am trying to create an Angular service which uses a web worker to change countdown variable using set interval.
What I want to do is to show the count down in the view.
I can easily do this by putting all the code in controller, which works but I got struck in creating the service
I am struck. I dont know how to proceed.
I have tried this plunkr here
script.js
angular.module('app', []).
controller('mainCtrl', mainCtrl);
function mainCtrl($scope,timer) {
$scope.time = 100;
console.log(timer.timeValue.time);
}
mainCtrl.$inject = ['$scope','timer'];
timer.js
angular.module('app')
.service('timer', timer);
function timer() {
var time;
this.timeValue = function(value) {
var worker = new Worker('worker.js');
worker.onmessage = function(e) {
//console.log('From Main:'+ e.data.time);
time = e.data.time;
};
worker.postMessage(time);
return time;
};
}
worker.js
angular.module('app')
.service('timer', timer);
function timer() {
var time;
this.timeValue = function(value) {
var worker = new Worker('worker.js');
worker.onmessage = function(e) {
//console.log('From Main:'+ e.data.time);
time = e.data.time;
};
worker.postMessage(time);
return time;
};
}
What I want to do is like this. This is my earlier plunk.This do the same thing using controller.
plunkr here
I found out why it's not working with your code. Just for the record, a countdown is not something you want to do with a Webworker, but anyway!
First of all in timer.js:
angular.module('app')
.service('timer', timer);
timer.$inject=['$rootScope']
function timer($rootScope) {
this.timeValue = function(value) {
var time = value;
var worker = new Worker('worker.js');
worker.onmessage = function(e) {
time = e.data.time;
$rootScope.$broadcast('timerUpdate', time)
};
worker.postMessage(time);
};
}
You have to start the var time with a value.
I injected $rootScope to the service, so i can $broadcast a message back to the main scope.
In the main script I did this:
function mainCtrl($scope,timer) {
function init() {
timer.timeValue(100);
}
$scope.time = 100;
$scope.$on('timerUpdate', function(event, time) {
$scope.$apply(function() {
$scope.time = time;
})
})
init();
}
mainCtrl.$inject = ['$scope','timer'];
So, i made a Init function that gets triggered once in the beginning. That triggers your service into making a webworker.
Once the webworker gives back the message(time). The timerService sends out a $rootScope.$broadcast picked up by $scope.$on().
The $scope.$apply is not really the best thing to have in a simple script like this, but it's the only thing that will force digest(Angular page update) the page and give the $scope.time a new value.
and last the webworker:
self.onmessage = function(e) {
var time = e.data;
var timer = setInterval(toDo, 1000);
function toDo() {
time--;
postMessage({
time: time
});
}
}
(Only thing i did was change time = time - 1 to time--; (shorthand version, looks beter !)
Hope this helps !
(also, just for the record, try no to use the $rootScope or the $scope.$apply function! It's not the best way to do stuff I hear, but I'm also new to Angular and haven't found anything beter for these things..)
And the plunker:
https://plnkr.co/edit/7IoGxFaaqQRH4AErGenl?p=preview
I've got what I thought was a fairly simple AngularJS application. I used to have a simple countdown timer in my controller code, but I decided to break it out into its own service. That's where the problems began.
Previously, when my timer code was embedded within the controller, the countdown scope variable displayed correctly - every second, it would count down one less, until 0, as per the timer function. However, now that I've moved this to a service, and been passing the data back and forth with some function calls, the countdownvariable counts down 2 numbers every second, rather than 1. If I console.log(countdown); in my service's rundownClock() function, the correct countdown number is displayed each pass, however: 10,9,8,7...to 1.
Can anyone figure out what I'm now doing wrong, and why this "double counting" is occurring? Am I not maintaining the scope correctly in the controller?
Here is some of the controller, with the relevant CountdownService bits highlighted:
myApp.controller('myCtrl', ['$scope', 'CountdownService', function($scope, CountdownService) {
// TIMER SERVICES
$scope.startTimer = CountdownService.startTimer;
$scope.runClock = function () {
$scope.updateCountdown();
if (($scope.countdown > 0) && ($scope.roundStarted == true)) {
CountdownService.rundownClock($scope.countdown);
}
};
$interval($scope.runClock, 1000);
$scope.updateCountdown = function () {
CountdownService.setCurrentRound($scope.currentRound);
$scope.countdown = CountdownService.getCountdown();
$scope.roundStarted = CountdownService.getRoundStarted();
}
}]);
Here's some of the service in question. (Don't worry about the rounds variable set-up at the beginning, it's not relevant to the problem):
myApp
.factory("CountdownService", function (gameSetUp) {
var rounds = gameSetUp.rounds,
roundStarted = false,
roundFinished = false,
currentRound = 0,
countdown = rounds[currentRound].time;
// grab the round from the controller's scope to set the current round
function setCurrentRound(round) {
currentRound = round;
}
function getRoundStarted() {
return roundStarted;
}
function getCountdown() {
return countdown;
}
function startTimer() {
roundStarted = true;
}
function rundownClock() {
if (roundStarted === true) {
if (countdown > 0) {
countdown = countdown - 1;
}
if (countdown === 0) {
roundFinished = true;
}
}
}
return {
startTimer: startTimer,
rundownClock: rundownClock,
getCountdown: getCountdown,
getRoundStarted: getRoundStarted,
setCurrentRound: setCurrentRound
};
});
And finally, a snippet from the view, where the countdown scope variable is displayed:
<div class="timer md-body-2">{{ countdown }} seconds</div>
Update #downvoter :
Here is a working demo ( without using controller in 2 places route and template)
Here is the exact behavior that the author is talking about (using controller in route and template)
My original answer
I think your myCtrl controller is running twice, so, your $interval($scope.runClock, 1000); is running twice also ...
Are using registering myCtrl as route controller and in your template with ng-controller ?
Asking for help with Angular.
Somewhy, cannot refresh property (timerValue) when its value is changed. It does render it once.
Here's html div:
<div>{{ game.timerValue }}</div>
The js:
// Game status
$scope.game = {
"started" : false,
"timerValue" : 60,
"score" : 0,
"question" : "? ? ?",
"message" : "If all options are set up, then you may start!",
"wrong" : ""
};
// Handle Start Button click
$scope.startGame = function () {
if($scope.game.timer) clearTimeout($scope.game.timer);
$scope.game.score = 0;
$scope.game.wrong = "";
$scope.game.message = "The game started!";
$scope.game.timer = setInterval(function() {
$scope.game.timerValue -= 1;
if( $scope.game.timerValue <= 0)
{
$scope.game.message = "Defeat! Time is out! Your score is " + $scope.game.score;
clearTimeout($scope.game.timer);
}
},1000);
};
Running out of ideas, thanks for any help.
Update: The property is changed, the timer is working. It is not refreshing.
The reason your UI is not updated is because your game timer logic runs outside the regular Angular digest cycle. There's a nice article explaining it: $watch How the $apply Runs a $digest.
Instead of using setInterval, it is recommended to use Angular's $interval service. It is a wrapper for window.setInterval and releases you from the duty of having to manually call $scope.$apply or "tell Angular to update the UI".
Additional benefits of using $interval:
It wraps your callback for you automatically in a try/catch block and let's you handle errors in the $exceptionHandler service.
It returns a promise and thus tends to interoperate better with other promise-based code than the traditional callback approach. When your callback returns, the value returned is used to resolved the promise.
An alternative solution would be to explicitly call $scope.$apply() inside setInterval to notify Angular that "model data has changed, update the UI".
you can do a $scope.$apply() at the end of each interval to get what you want to achieve. just be wary of confilicts if you try to do a $scope.$apply() any where inside this function (if you were to extend it) or outside, if you were to extend the function that calls this.
You could also do what #Discosultan suggested and use $interval, which should automatically apply changes to your view from the scope at the end of each interval and will not create conflicts if you use a $scope.$apply() elsewhere in your code. By using $interval it will become part of your digest cycle, and you want to make sure not to put to much computational heavy code inside your digest loop otherwise it could slow down your entire app, as explained below in the comments by #AlvinThompson
setInterval does its work in a separate thread (sort of), so Angular cannot detect any changes to properties it makes. You have to wrap any functions that modifies properties with $scope.$apply(function () {... so that Angular detects them and pushes those changes to the UI.
$scope.$apply();
Working JS Bin
$scope.game.timer = setInterval(function() {
$scope.game.timerValue -= 1;
if( $scope.game.timerValue <= 0)
{
$scope.game.message = "Defeat! Time is out! Your score is " + $scope.game.score;
clearTimeout($scope.game.timer);
}
$scope.$apply();
},1000);
You are refreshing with a function that is outside Angular (setInterval). To tell angular to apply the change in your view, you have two solutions :
using $scope.$apply() :
$scope.startGame = function () {
if($scope.game.timer) clearTimeout($scope.game.timer);
$scope.game.score = 0;
$scope.game.wrong = "";
$scope.game.message = "The game started!";
$scope.game.timer = setInterval(function() {
$scope.game.timerValue -= 1;
$scope.$apply();
if( $scope.game.timerValue <= 0)
{
$scope.game.message = "Defeat! Time is out! Your score is " + $scope.game.score;
clearTimeout($scope.game.timer);
}
},1000);
};
or using $timeout :
$scope.startGame = function () {
if($scope.game.timer) $timeout.cancel($scope.game.timer);
$scope.game.score = 0;
$scope.game.wrong = "";
$scope.game.message = "The game started!";
$scope.game.timer = $timeout(function() {
$scope.game.timerValue -= 1;
if( $scope.game.timerValue <= 0)
{
$scope.game.message = "Defeat! Time is out! Your score is " + $scope.game.score;
$timeout.cancel($scope.game.timer);
}
},1000);
};
Without forgetting to inject ̀$timeout in your controller dependencies.
I'm using angular-timer: http://siddii.github.io/angular-timer/
My goal is to create a timer for an app that keeps reference to a variable somewhere else. That way instead of having a timer that just restarts on page load I will have a timer that consistently counts down regardless of what the user does. Most examples with angular-timer have you enter a countdown number. Is there any way to pass in a variable like so:
var timeRemaining = 1000;
<h1 class="timer"><timer countdown=timeRemaining max-time-unit="'minute'" interval="1000">{{mminutes}} minute{{minutesS}}, {{sseconds}} second{{secondsS}}</timer></h1>
Instead of being forced to write the countdown like this:
countdown="1000"
I've already tried passing in the variable via the toString() method as well. Thanks.
It looks like you cannot do what you are trying to without editing the directive itself.
Alternatively, you could use the timer from this question on your scope, just modify it to count down: https://stackoverflow.com/a/12050481/4322479
Counting down, from the question's comments: http://jsfiddle.net/dpeaep/LQGE2/1/
function AlbumCtrl($scope,$timeout) {
$scope.counter = 5;
$scope.onTimeout = function(){
$scope.counter--;
if ($scope.counter > 0) {
mytimeout = $timeout($scope.onTimeout,1000);
}
else {
alert("Time is up!");
}
}
var mytimeout = $timeout($scope.onTimeout,1000);
$scope.reset= function(){
$scope.counter = 5;
mytimeout = $timeout($scope.onTimeout,1000);
}
}
No need to edit directive, you could also use ng-if on your timer element to check for your startTime variable.
<timer ng-if="yourCountdownVaariable>0" countdown="yourCountdownVaariable"
max-time-unit="'hour'" interval="60000">
{{hhours}} hour{{hoursS}}, {{mminutes}} minute{{minutesS}}
</timer>
This way, the directive will only be initialised when you have your date, and it will work. Original issue below:
https://github.com/siddii/angular-timer/issues/36
I'm working on a custom jQuery plugin that makes use of SetInterval, but it breaks when it's called more than once.
I have something sort of like this:
(function($){
$.fn.myplugin = function(options) {
var defaults = {};
var options = $.extend(defaults, options);
var interval;
this.each(function() {
//etc.
interval = setInterval(function(){ doMyOtherFunc(options); }, 1000);
});
function doMyOtherFunc(options) {
//etc
}
}
})(jQuery);
Functionality works as expected if I call it once, but if I call it again on a second element it breaks.
$('#myelement').myplugin({'option1', 'option2'});
$('#myotherelement').myplugin({'option1', 'option2'});
Somehow, the interval in the second instance overrides the one on the previous element, data and all. (But the styling passed doesn't get screwed up.) Is this a weird limitation of setInterval, or am I doing something wrong?
You have to make the interval handle private to each element. For this, you could use $.data:
this.each(function() {
var interval = setInterval(function(){ doMyOtherFunc(options); }, 1000);
$(this).data('myplugin-interval', interval);
});
And you could retrieve the interval this way:
$(this).data('myplugin-interval');