angularjs services and controllers data out of sync - javascript

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 ?

Related

Having an Angular $interval running independent of controller

I have different pages on may application which have their own controllers. One of them has an $interval function, let's say a timer. Click on a button will start this interval function, which updates itself every second. What i want to have is, i want to be able to go to any other page in my application (calling different controllers), but i want my interval to continue running until i stop it explicitly from the first controller. A rootScope interval so to speak. How can i do it?
EDIT: Thanks to Chris and Patrick i now have a simple Service, looks like this:
.service('TimerService', function($interval) {
var promise;
var timerSeconds = 0;
this.start = function () {
promise = $interval(function () {
timerSeconds++;
}, 1000);
};
this.stop = function () {
promise.cancel(interval);
timerSeconds = 0;
};
this.getTimer = function() {
return timerSeconds;
}
})
I store also my current value (timerSeconds) in this service. But how can i sync this value to my controller? The service increments the timerSeconds, and at the beginning of my controller i read it from this service through its getTimer() function, but it clearly will not be updated on my controller. How can i sync this service attribute with my local attribute?
EDIT:
when i define my service attribute as an object and the timerSeconds as number inside that object (it seems primitives cannot be synced):
var timer = {seconds : 0};
this.getTimer = function() {
return timer;
}
and get this object from my controller through that getter:
vm.timer = TimerService.getTimer();
they are all in sync.
Don't bother adding it to $rootScope. Use a service that can be used anywhere in the app. Here is a singleton timer that can start and stop. Define the intervalTimeout in the service itself, or if you want to be really flexible, do so in a provider (probably overkill for this example).
angular.module('myApp', [])
.service('AppCallback', function ($interval) {
var states = states = {
PENDING: 0,
STARTED: 1
}, intervalTimeout = 3000, // Set this
interval;
this.states = states;
this.state = states.PENDING;
this.start = function (callback) {
if (this.state !== states.PENDING) {
return;
}
interval = $interval(callback, intervalTimeout);
this.state = states.STARTED;
}
this.stop = function () {
if (this.state !== states.STARTED) {
return;
}
$interval.cancel(interval);
this.state = states.PENDING;
};
})
.controller('MainController', function ($scope, AppCallback) {
var vm = {},
count = 0;
vm.toggle = function toggle() {
if (AppCallback.state === AppCallback.states.PENDING) {
AppCallback.start(function () {
vm.data = 'Ticked ' + (++count) + ' times.';
});
} else {
AppCallback.stop();
}
};
$scope.vm = vm;
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="MainController">
{{vm.data}}
<br />
<button ng-click="vm.toggle()">Toggle</button>
</div>
If you want to share any data between controllers the correct way is to use a service.
I would then create a service that allows you to stop and start this timer / interval.
The initial controller would kick this off and it would continue to "tick" forever until it is stopped.

Can't render model's property

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.

Directive updates to parent scope one step delayed

I moved my simple paging from controller to a directive and got stuck with a strange problem. Every time I update parent scope from within the directive it only gets updated on the next change. So if ng-options are [10,20,30] and I ng-change it to 10 - nothing happens. Then I change it to 20 and it updates to previous value of 10, next change to whatever value gets the model updated to the one I picked before, and so on
I tried to use $scope.$apply but it does not help. I still get delayed updates. What am I missing? I realize that it is something to do with digest update and $scope.$apply but I cant figure out where to use it. Anything I try just does not work.
Relevant controller parts:
vm.pages = 0;
vm.articles = [];
vm.load = { page: { batch: 10, current: 1 }
, sort: { _id: -1 }
, filter: {}
};
vm.getArticles = function() {
articleS.list(vm.load, function (data){
vm.pages = data.pages;
vm.articles = data.articles;
});
}
The directive:
.directive("paging", function() {
var scope = { update: '&', current: '=', pages: '=', batch: '=' };
function link(s, e, a) {
s.options = [10,20,50,100];
s.toPage = function(p) {
switch(p) {
case "last":
if (s.current != s.pages) {
s.current = s.pages;
s.update();
}
break;
case "next":
if (s.current < s.pages) {
s.current ++;
s.update();
}
break;
case "prev":
if (s.current > 1) {
s.current --;
s.update();
}
break;
default:
s.current = 1;
s.update();
}
}
}
return {
replace: true,
scope: scope,
templateUrl: 'paging.tpl',
link: link
}
});
The directive template:
<section class='pages'>
<select
ng-model="batch"
ng-change="toPage('first')"
ng-options="value for value in options">
</select>
<div>
<button ng-click="toPage('first')"> 1 </button>
<button ng-click="toPage('prev')"> << </button>
<div>{{current}}</div>
<button ng-click="toPage('next')"> >> </button>
<button ng-click="toPage('last')"> {{pages}} </button>
</div>
</section>
Directive call:
<paging
update="vm.getArticles()"
current="vm.load.page.current"
batch="vm.load.page.batch"
pages="vm.pages">
</paging>
OK. I figured this out.
If I understood correctly, $apply will generate a already in progress error if you run it on an isolated scope variable bound to its parent with '='. I think that '=' triggers the $digest (this is just an observation from trial and error while trying to solve this).
My problem was that s.update() function was run before the next digest cycle. So local scope variables "did not have time" to copy to the parent scope. That's why I could only see the update at the next operation or at the next digest cycle hens always seeing previous change.
Playing with setTimeout(function(){ s.update() }, 2000) proved it.
If I delay the execution for a couple of seconds then I see the update with correct changes. Delaying is not a solution though, slowing down application is plain stupid.
Then I found angular's $timeout(function() {})
This allows to skip the delay parameter which defaults to 0 and does not actually delay anything, but it does insure that callback runs on the next digest cycle so that the local scope variables can be updated to the parent.
if (s.current < s.pages) {
s.current ++;
$timeout(function() {
s.update();
});
}

Getting Angular to detect change in $scope

I am writing my first AngularJS app and I'm trying to get a directive to update its view when an array it received from the service changed.
My directive looks like this:
angular.module('Aristotle').directive('ariNotificationCenter', function (Notifications) {
return {
replace: true,
restrict: 'E',
templateUrl: 'partials/ariNotificationCenter.html',
controller: function ($scope) {
$scope.notifications = Notifications.getNotifications();
$scope.countUnread = function () {
return Notifications.countUnread();
};
}
};
});
The partial is quite simply:
<p>Unread count: {{countUnread()}}</p>
While my Notifications service looks like this:
function Notification (text, link) {
this.text = text;
this.link = link;
this.read = false;
}
var Notifications = {
_notifications: [],
getNotifications: function () {
return this._notifications;
},
countUnread: function () {
var unreadCount = 0;
$.each(this._notifications, function (i, notification) {
!notification.read && ++unreadCount;
});
return unreadCount;
},
addNotification: function (notification) {
this._notifications.push(notification);
}
};
// Simulate notifications being periodically added
setInterval(function () {
Notifications.addNotification(new Notification(
'Something happened!',
'/#/somewhere',
Math.random() > 0.5
));
}, 2000);
angular.module('Aristotle').factory('Notifications', function () {
return Notifications;
});
The getNotifications function returns a reference to the array, which gets changed by the setInterval setup when addNotification is called. However, the only way to get the view to update is to run $scope.$apply(), which stinks because that removes all the automagical aspect of Angular.
What am I doing wrong?
Thanks.
I believe the only problem with you code is that you are using setInterval to update the model data, instead of Angular built-in service $interval. Replace the call to setInterval with
$interval(function () {
Notifications.addNotification(new Notification(
'Something happened!',
'/#/somewhere',
Math.random() > 0.5
));
}, 2000);
And it should work without you calling $scope.$apply. Also remember to inject the $interval service in your factory implementation Notifications.
angular.module('Aristotle').factory('Notifications', function ($interval) {
$interval internally calls $scope.$apply.
I'm not an expert at Angular yet, but it looks like your problem may be in the partial.
<p>Unread count: {{countUnread()}}</p>
I don't think you can bind to a function's results. If this works, I believe it will only calculate the value once, and then it's finished, which appears to be the issue you are writing about.
Instead, I believe you should make a variable by the same name:
$scope.countUnread = 0;
And then update the value in the controller with the function.
Then, in your partial, remove the parentheses.
<p>Unread count: {{countUnread}}</p>
As long as $scope.countUnread is indeed updated in the controller, the changes should be reflected in the partial.
And as a side note, if you take this approach, I'd recommend renaming either the variable or the function, as that may cause issues, or confusion at the very least.

Angular timer custom countdown variable

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

Categories

Resources