AngularJS - $interval not recognising variables - angularjs

I am trying to make a basic function that adds 1 to the variable 'wood' every second.
In javascript, a simple
setInterval(function(){
wood++;
}, 1000);
would do the trick.
In Angular, I've been shown
app.controller('RandomCtrl', function($interval){
this.wood = 0;
$interval(function(){
this.wood++;
}, 1000);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('RandomCtrl', function($interval){
this.wood = 0;
$interval(function(){
this.wood++;
document.getElementById('p').innerHTML = this.wood;
}, 1000);
});
</script>
<div ng-app='app' ng-controller='RandomCtrl as rand'>
Wood: {{ rand.wood }}
<br><br>Wood's value straight from the $interval:<p id='p'></p>
So, the interval is fine, but the variable is undefined inside it, which is the whole point of me using this interval.
<br><br>Also, I want this.wood to hold the value, nothing else.
</div>
However, the code above for some reason doesn't work.
It treats this.wood+1 as 'NaN' and this.wood as 'undefined'
Here's the snippet:

From http://ryanmorr.com/understanding-scope-and-context-in-javascript/ :
Context is most often determined by how a function is invoked. When a
function is called as a method of an object, this is set to the object
the method is called on
When called as an unbound function, this will default to the global
context or window object in the browser. However, if the function is
executed in strict mode, the context will default to undefined.
Just use angulars $scope or a variable declared in the outer function scope:
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('RandomCtrl', function($interval, $scope){
var self = this;
self.wood = 0;
$scope.wood = 0;
$interval(function(){
$scope.wood++;
self.wood++;
}, 1000);
});
</script>
<div ng-app='app' ng-controller='RandomCtrl as rand'>
Wood: {{ wood }} {{ rand.wood }}
<br><br>Wood's value straight from the $interval:<p id='p'></p>
So, the interval is fine, but the variable is undefined inside it, which is the whole point of me using this interval.
<br><br>Also, I want this.wood to hold the value, nothing else.
</div>

The problem here is that you are trying to access your scope in a new context, where 'this' no longer refers to your angular controller.
In addition, Angular allows variables to be accessible through your controller's scope. If you want to use your variable wood in your template, you have to assign it on the scope object.
Your code should read:
app.controller('RandomCtrl', function($interval, $scope){
$scope.wood = 0;
$interval(function(){
$scope.wood++;
}, 1000);
});
The reason why this fails silently is that this.TEST++ returns NaN and not an error.

Related

Angular : ng-init does not run on load

I have seen a few exmaples on stack overflow about this ng-init issue, although I cant seem to find one which references it with the use of a controller.
I have called the function in the controller by having the following in the html file
<div class="tab-container" ng-controller = "ExampleController" ng-init = "init()" >
In the controller:
$scope.init = function(){
alert("do something");
};
It does run, but it runs before the components have loaded on the screen.
Am i missing something?
Thanks
ng-init is supposed to work like this, because it's used to initialize data.
A very simple example:
<ul ng-init="list = [1,2,3,4]">
<li ng-repeat="l in list"></li>
</ul>
If you are trying to run something while your controller loads, it's actually much simpler than you thought:
app.controller('mainCtrl', function ($scope) {
var init = function ($scope) {
// do whatever you need to do to initialize your controller
$scope.someData = ["Hey", "I'm", "Alive"]
$scope.otherData = localStorage.getItem('myBackup')
}
init()
})
Or even simpler, if you don't need the function (no closures or whatever)
app.controller('mainCtrl', function ($scope) {
// do whatever you need to do to initialize your controller
$scope.someData = ["Hey", "I'm", "Alive"]
$scope.otherData = localStorage.getItem('myBackup')
})
Edit - assuming you're using ngView:
To have the code run on when the page is fully loaded you should set a watcher on the event $viewContentLoaded, like this:
$scope.$on('$viewContentLoaded', function(){
//Here your view content is fully loaded !!
});
app.controller('mainCtrl', function ($scope) {
// This event is triggered when the view has finished loading
$scope.$on('$viewContentLoaded', function() {
$scope.someData = ["Hey", "I'm", "Alive"]
$scope.otherData = localStorage.getItem('myBackup')
})
})
another option is using jquery. It would fit if you depend on many elements. But make sure to load jquery with a version of your choice to project.
loading jquery (insert version where it's ...):
<script src="https://code.jquery.com/jquery-..."></script>
the js code:
$(document).ready(function() {
alert("do something");
});

AngularJS - Why does $scope.$apply affect other scopes

In my AngularJS app, I have 2 controllers which are not nested. Calling the $scope.$apply method seems to affect the other sibling scope as well.
In the jsfiddle below, it seems that the ControllerOne's {{doubleMe(x)}} expression is evaluated whenever ControllerTwo updates the clock every second. This can be shown from the console message.
I can understand why that expression is evaluated whenever the text input (on the same scope) changes, but why would $scope.$apply on another scope cause that expression to be re-evaluated as well?
Note that I could have avoided $scope.$apply by using $timeout, but the outcome is observed.
<!-- HTML file -->
<div ng-app>
<h1>Root</h1>
<div ng-controller="ControllerOne">
<h2>Scope One</h2>
1 * 2 = {{doubleMe(1)}}<br/>
2 * 2 = {{doubleMe(2)}}<br/>
3 * 2 = {{doubleMe(3)}}<br/>
<input ng-model="text">
</div>
<div ng-controller="ControllerTwo">
<h2>Scope Two</h2>
{{clock.now | date:'yyyy-MM-dd HH:mm:ss'}}
</div>
</div>
// js file
function ControllerOne($scope) {
var counter=1;
$scope.doubleMe = function(input) {
console.log(counter++);
return input*2;
}
$scope.text = "Change Me";
}
function ControllerTwo($scope) {
$scope.clock = {
now: new Date()
};
var updateClock = function() {
$scope.clock.now = new Date()
};
setInterval(function() {
$scope.$apply(updateClock);
}, 1000);
}
as you can see $scope.$apply = $rootScope.$digest //+ some error handling and since $scope.$apply uses $rootScope it affects all its descendants.
so If you update a child scope, you can call $scope.$digest to dirty-check only that scope and its descendants and as a result you reduce the number of dirty-checks and increase your performance.
Example
I changed your code and added $digest.
setInterval(function() {
$scope.clock.now = new Date();
$scope.$digest();
}, 1000);
Live example:
http://jsfiddle.net/choroshin/JY5sb/4/
From the AngularJS docs in the source code
* # Pseudo-Code of `$apply()`
* <pre>
function $apply(expr) {
try {
return $eval(expr);
} catch (e) {
$exceptionHandler(e);
} finally {
$root.$digest();
}
}
* </pre>
Alex is correct that if you just want to have your changes reflected in the current scope and its children, scope.$digest() is the thing to use. However, AngularJS often discourages this since expressions often have side-effects that modify global objects like services which can in turn modify sibling scopes.

Why the modifications i apply on a service don't impact the DOM on all the controllers it is involved

I am new using AngularJS, i am interesting about the fact that when we update a data, Angular automatically impacts the modifications everywhere the data is involving.
But unfortunately, i can't make it works.
The simple thing i am trying to do is to make a change on a controller B, and i want the changes to be achieve on the controller A, since the data is referering to the same Service.
The data is correctly impacting on the both controllers, but the DOM is not updating according to this modification, here is the test:
HTML
<body>
<div ng-controller="ACrtl">
<h1>{{is_logged}}</h1> <!-- Always false -->
<button ng-click="check()">Check</button> <!-- true/false -->
</div>
<div ng-controller="BCrtl">
<button ng-click="{{is_logged=!is_logged}}">Toggle throught the DOM</button> <!-- Doesn't change anything on the Javascript -->
<button ng-click="toggle()">Toggle throught the controller</button> <!-- Change the Javascript but doesn't impact the other controller's scope -->
</div>
</body>
JS
var app = angular.module('MyApp', []);
app.controller('ACrtl', function($scope, UserService) {
$scope.is_logged = UserService.is_logged;
$scope.check = function() {
console.log('is_logged='+UserService.is_logged); //The change is correctly made when changin is_logged on the controller B.
$scope.is_logged = UserService.is_logged;
};
});
app.controller('BCrtl', function($scope, UserService) {
$scope.is_logged = UserService.is_logged;
$scope.toggle = function() {
UserService.is_logged = !UserService.is_logged;
};
});
app.factory('UserService', function() {
var User = {
is_logged: false
};
return User;
});
I hope AngularJS is able to do this and it's something i am doing wrong in my code !
Here is a plunker
Primitive variables (like boolean) are passed by value in Javascript, and the variables $scope.is_logged are just copies of their values in the service. So, if the original service value is changed, then this won't affect any copies on the scopes.
A standard way or re-factoring this would be to share an object between the controllers, and not a primitive, so
app.factory('UserService', function() {
return {
status: {
is_logged: false
}
};
});
And then used in the controllers
$scope.status = UserService.status;
So the controller can change $scope.status.is_logged, and the changes will be seen in all the controllers.
You can see this at:
http://plnkr.co/edit/GLZmdsAnn3T5Xw80h4sV?p=preview
When you assign is_logged to the scope on each controller you are creating a new property on each controller, both of which are initialised to the value from UserService.
In your case what you can do is expose the service on the scope of each controller like so:
$scope.data = UserService
and in your view:
<h1>{{data.is_logged}}</h1>
Have a look at this answer and the links that it mentions.

Update scope value when service data is changed

I have the following service in my app:
uaInProgressApp.factory('uaProgressService',
function(uaApiInterface, $timeout, $rootScope){
var factory = {};
factory.taskResource = uaApiInterface.taskResource()
factory.taskList = [];
factory.cron = undefined;
factory.updateTaskList = function() {
factory.taskResource.query(function(data){
factory.taskList = data;
$rootScope.$digest
console.log(factory.taskList);
});
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.startCron = function () {
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.stopCron = function (){
$timeout.cancel(factory.cron);
}
return factory;
});
Then I use it in a controller like this:
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
$scope.taskList = uaProgressService.taskList;
});
}
);
So basically my service update factory.taskList every 5 seconds and I linked this factory.taskList to $scope.taskList. I then tried different methods like $apply, $digest but changes on factory.taskList are not reflected in my controller and view $scope.taskList.
It remains empty in my template. Do you know how I can propagate these changes ?
While using $watch may solve the problem, it is not the most efficient solution. You might want to change the way you are storing the data in the service.
The problem is that you are replacing the memory location that your taskList is associated to every time you assign it a new value while the scope is stuck pointing to the old location. You can see this happening in this plunk.
Take a heap snapshots with Chrome when you first load the plunk and, after you click the button, you will see that the memory location the scope points to is never updated while the list points to a different memory location.
You can easily fix this by having your service hold an object that contains the variable that may change (something like data:{task:[], x:[], z:[]}). In this case "data" should never be changed but any of its members may be changed whenever you need to. You then pass this data variable to the scope and, as long as you don't override it by trying to assign "data" to something else, whenever a field inside data changes the scope will know about it and will update correctly.
This plunk shows the same example running using the fix suggested above. No need to use any watchers in this situation and if it ever happens that something is not updated on the view you know that all you need to do is run a scope $apply to update the view.
This way you eliminate the need for watchers that frequently compare variables for changes and the ugly setup involved in cases when you need to watch many variables. The only issue with this approach is that on your view (html) you will have "data." prefixing everything where you used to just have the variable name.
Angular (unlike Ember and some other frameworks), does not provide special wrapped objects which semi-magically stay in sync. The objects you are manipulating are plain javascript objects and just like saying var a = b; does not link the variables a and b, saying $scope.taskList = uaProgressService.taskList does not link those two values.
For this kind of link-ing, angular provides $watch on $scope. You can watch the value of the uaProgressService.taskList and update the value on $scope when it changes:
$scope.$watch(function () { return uaProgressService.taskList }, function (newVal, oldVal) {
if (typeof newVal !== 'undefined') {
$scope.taskList = uaProgressService.taskList;
}
});
The first expression passed to the $watch function is executed on every $digest loop and the second argument is the function which is invoked with the new and the old value.
I'm not sure if thats help but what I am doing is bind the function to $scope.value. For example
angular
.module("testApp", [])
.service("myDataService", function(){
this.dataContainer = {
valA : "car",
valB : "bike"
}
})
.controller("testCtrl", [
"$scope",
"myDataService",
function($scope, myDataService){
$scope.data = function(){
return myDataService.dataContainer;
};
}]);
Then I just bind it in DOM as
<li ng-repeat="(key,value) in data() "></li>
This way you can avoid to using $watch in your code.
No $watch or etc. is required. You can simply define the following
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
});
$scope.getTaskList = function() {
return uaProgressService.taskList;
};
});
Because the function getTaskList belongs to $scope its return value will be evaluated (and updated) on every change of uaProgressService.taskList
Lightweight alternative is that during controller initialization you subscribe to a notifier pattern set up in the service.
Something like:
app.controller('YourCtrl'['yourSvc', function(yourSvc){
yourSvc.awaitUpdate('YourCtrl',function(){
$scope.someValue = yourSvc.someValue;
});
}]);
And the service has something like:
app.service('yourSvc', ['$http',function($http){
var self = this;
self.notificationSubscribers={};
self.awaitUpdate=function(key,callback){
self.notificationSubscribers[key]=callback;
};
self.notifySubscribers=function(){
angular.forEach(self.notificationSubscribers,
function(callback,key){
callback();
});
};
$http.get('someUrl').then(
function(response){
self.importantData=response.data;
self.notifySubscribers();
}
);
}]);
This can let you fine tune more carefully when your controllers refresh from a service.
Like Gabriel Piacenti said, no watches are needed if you wrap the changing data into an object.
BUT for updating the changed service data in the scope correctly, it is important that the scope value of the controller that uses the service data does not point directly to the changing data (field). Instead the scope value must point to the object that wraps the changing data.
The following code should explain this more clear. In my example i use an NLS Service for translating. The NLS Tokens are getting updated via http.
The Service:
app.factory('nlsService', ['$http', function($http) {
var data = {
get: {
ressources : "gdc.ressources",
maintenance : "gdc.mm.maintenance",
prewarning : "gdc.mobMaint.prewarning",
}
};
// ... asynchron change the data.get = ajaxResult.data...
return data;
}]);
Controller and scope expression
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.get.maintenance}}</span>
</div>
The above code works, but first i wanted to access my NLS Tokens directly (see the following snippet) and here the values did not become updated.
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService.get;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.maintenance}}</span>
</div>

AngularJS: Is it possible to watch a global variable (I.e outside a scope)?

Let me start by saying that what I am trying to do is probably not considered good practice. However, I need to do something like this in order to migrate a large web app to AngularJs in small incremental steps.
I tried doing
$scope.$watch(function () { return myVar; }, function (n, old) {
alert(n + ' ' + old);
});
Where myVar is a global variable (defined on window)
And then changing myVar from the console.
But it only fires when first setting up the watcher.
It works if I update myVar from within the controller (see http://jsfiddle.net/rasmusvhansen/vsDXz/3/, but not if it is updated from some legacy javascript
Is there any way to achieve this?
Update
I like Anders' answer if the legacy code is completely off limits. However, at the moment I am looking at this approach which seems to work and does not include a timer firing every second:
// In legacy code when changing stuff
$('.angular-component').each(function () {
$(this).scope().$broadcast('changed');
});
// In angular
$scope.$on('changed', function () {
$scope.reactToChange();
});
I am awarding points to Anders even though I will go with another solution, since his solution correctly solves the problem stated.
The issue here is probably that you're modifying myVar from outside of the Angular world. Angular doesn't run digest cycles/dirty checks all the time, only when things happen in an application that should trigger a digest, such as DOM events that Angular knows about. So even if myVar has changed, Angular sees no reason to start a new digest cycle, since nothing has happened (at least that Angular knows about).
So in order to fire your watch, you need to force Angular to run a digest when you change myVar. But that would be a bit cumbersome, I think you would be better of to create a global observable object, something like this:
<!doctype html>
<html ng-app="myApp">
<head>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="http://code.angularjs.org/1.0.5/angular.min.js"></script>
<script>
// Outside of Angular
window.myVar = {prop: "value1"};
var myVarWatch = (function() {
var watches = {};
return {
watch: function(callback) {
var id = Math.random().toString();
watches[id] = callback;
// Return a function that removes the listener
return function() {
watches[id] = null;
delete watches[id];
}
},
trigger: function() {
for (var k in watches) {
watches[k](window.myVar);
}
}
}
})();
setTimeout(function() {
window.myVar.prop = "new value";
myVarWatch.trigger();
}, 1000);
// Inside of Angular
angular.module('myApp', []).controller('Ctrl', function($scope) {
var unbind = myVarWatch.watch(function(newVal) {
console.log("the value changed!", newVal);
});
// Unbind the listener when the scope is destroyed
$scope.$on('$destroy', unbind);
});
</script>
</head>
<body ng-controller="Ctrl">
</body>
</html>

Resources