Im working on angularjs 1.4. Im trying to have some frontend-cache collection that updates the view when new data is inserted. I have checked other answers from here Angularjs watch service object but I believe Im not overwriting the array, meaning that the reference is the same.
The code is quite simple:
(function(){
var appCtrl = function($scope, $timeout, SessionSvc){
$scope.sessions = {};
$scope.sessions.list = SessionSvc._cache;
// Simulate putting data asynchronously
setTimeout(function(){
console.log('something more triggered');
SessionSvc._cache.push({domain: "something more"});
}, 2000);
// Watch when service has been updated
$scope.$watch(function(){
console.log('Watching...');
return SessionSvc._cache;
}, function(){
console.log('Modified');
}, true);
};
var SessionSvc = function(){
this._cache = [{domain: 'something'}];
};
angular.module('AppModule', [])
.service('SessionSvc', SessionSvc)
.controller('appCtrl', appCtrl);
})();
I thought that the dirty checking would have to catch the changes without using any watcher. Still I put the watcher to check if anything gets executed once the setTimeout function is triggered. I just dont see that the change is detected.
Here is the jsbin. Im really not understanding sth or doing a really rockie mistake.
You need to put $scope.$apply(); at the bottom of your timeout to trigger an update. Alternatively you can use the injectable $timeout service instead of setTimeout and $apply will automatically get called.
jsbin
I have an angular controller:
.controller('DashCtrl', function($scope, Auth) {
$scope.login = function() {
Auth.login().then(function(result) {
$scope.userInfo = result;
});
};
});
Which is using a service I created:
.service('Auth', function($window) {
var authContext = $window.Microsoft.ADAL.AuthenticationContext(...);
this.login = function() {
return authContext.acquireTokenAsync(...)
.then(function(authResult) {
return authResult.userInfo;
});
};
});
The Auth service is using a Cordova plugin which would be outside of the angular world. I guess I am not clear when you need to use a $scope.$apply to update your $scope and when you don't. My incorrect assumption was since I had wrapped the logic into an angular service then I wouldn't need it in this instance, but nothing gets updated unless I wrap the $scope.userInfo = statement in a $timeout or $scope.$apply.
Why is it necessary in this case?
From angular's wiki:
AngularJS provides wrappers for common native JS async behaviors:
...
jQuery.ajax() => $http
This is just a traditional async function with a $scope.$apply()
called at the end, to tell AngularJS that an asynchronous event just
occurred.
So i guess since your Auth service does not use angular's $http, $scope.$apply() isn't called by angular after executing the Async Auth function.
Whenever possible, use AngularJS services instead of native. If you're
creating an AngularJS service (such as for sockets) it should have a
$scope.$apply() anywhere it fires a callback.
EDIT:
In your case, you should trigger the digest cycle once the model is updated by wrapping (as you did):
Auth.login().then(function(result) {
$scope.$apply(function(){
$scope.userInfo = result;
});
});
Or
Auth.login().then(function(result) {
$scope.userInfo = result;
$scope.$apply();
});
Angular does not know that $scope.userInfo was modified, so the digest cycle needs to be executed via the use of $scope.$apply to apply the changes to $scope.
Yes, $timeout will also trigger the digest cycle. It is simply the Angular version of setTimeout that will execute $scope.$apply after the wrapped code has been run.
In your case, $scope.$apply() would suffice.
NB: $timeout also has exception handling and returns a promise.
Really have no idea why this doesn't work. I must be doing something incredibly stupid.
Here is a controller:
angular.module('nightlifeApp')
.controller('TestCtrl', function($scope) {
$scope.testvar = 'before';
setTimeout(function() {
$scope.testvar = 'after';
}, 2000);
});
and here is the view that has this as the controller:
h1(ng-bind='testvar')
h1 {{testvar}}
But neither h1 element ever changes! Any thoughts?
If you're using setTimeout then you manually need to trigger apply. Like
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the angular framework we need to perform proper scope life cycle of
exception handling, executing watches.
setTimeout(function() {
$scope.$apply(function() {
$scope.testvar = 'after';
});
}, 2000);
In my opinion you should use $timeout service. So it will trigger $apply() automatically. Your code will look like
$timeout(function () {
$scope.testvar = 'after';
}, 2000);
Make sure you've injected $timeout service in your controller. In your HTML you don't need to use ng-bind. You're doing same in controller. Only
<h1> {{testvar}} </h1>
I'm struggling unit testing a controller that watches a couple variables. In my unit tests, I can't get the callback for the $watch function to be called, even when calling scope.$digest(). Seems like this should be pretty simple, but I'm having no luck.
Here's what I have in my controller:
angular.module('app')
.controller('ClassroomsCtrl', function ($scope, Classrooms) {
$scope.subject_list = [];
$scope.$watch('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
$scope.classrooms = Classrooms.search(ctrl.functions.search_params());
}
});
});
And here's my unit test:
angular.module('MockFactories',[]).
factory('Classrooms', function(){
return jasmine.createSpyObj('ClassroomsStub', [
'get','save','query','remove','delete','search', 'subjects', 'add_subject', 'remove_subject', 'set_subjects'
]);
});
describe('Controller: ClassroomsCtrl', function () {
var scope, Classrooms, controllerFactory, ctrl;
function createController() {
return controllerFactory('ClassroomsCtrl', {
$scope: scope,
Classrooms: Classrooms
});
}
// load the controller's module
beforeEach(module('app'));
beforeEach(module('MockFactories'));
beforeEach(inject(function($controller, $rootScope, _Classrooms_ ){
scope = $rootScope.$new();
Classrooms = _Classrooms_;
controllerFactory = $controller;
ctrl = createController();
}));
describe('Scope: classrooms', function(){
beforeEach(function(){
Classrooms.search.reset();
});
it('should call Classrooms.search when scope.subject_list changes', function(){
scope.$digest();
scope.subject_list.push(1);
scope.$digest();
expect(Classrooms.search).toHaveBeenCalled();
});
});
});
I've tried replacing all the scope.$digest() calls with scope.$apply() calls. I've tried calling them 3 or 4 times, but I can't get the callback of the $watch to get called.
Any thoughts as to what could be going on here?
UPDATE:
Here's an even simpler example, that doesn't deal with mocks, stubbing or injecting factories.
angular.module('edumatcherApp')
.controller('ClassroomsCtrl', function ($scope) {
$scope.subject_list = [];
$scope.$watch('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
console.log('testing');
$scope.test = 'test';
}
});
And unit test:
it('should set scope.test', function(){
scope.$digest();
scope.subject_list.push(1);
scope.$digest();
expect(scope.test).toBeDefined();
});
This fails too with "Expected undefined to be defined." and nothing is logged to the console.
UPDATE 2
2 more interesting things I noted.
It seems like one problem is that newValue and oldValue are the same when the callback is called. I logged both to the console, and they are both equal to []. So, for example, if I change my $watch function to look like this:
$scope.$watch('subject_list', function(newValue, oldValue){
console.log('testing');
$scope.test = 'test';
});
the test passes fine. Not sure why newValue and oldValue aren't getting set correctly.
If I change my $watch callback function to be a named function, and just check to see if the named function is ever called, that fails as well. For example, I can change my controller to this:
$scope.update_classrooms = function(newValue, oldValue){
$scope.test = 'testing';
console.log('test');
};
$scope.watch('subject_list', $scope.update_classrooms);
And change my test to this:
it('should call update_classrooms', function(){
spyOn(scope,'update_classrooms').andCallThrough();
scope.$digest();
scope.subject_list.push(1);
scope.$digest();
expect(scope.update_classrooms).toHaveBeenCalled();
});
it fails with "Expected spy update_classrooms to have been called."
In this case, update_classrooms is definitely getting called, because 'test' gets logged to the console. I'm baffled.
I just ran into this problem within my own code base and the answer turned out to be that I needed a scope.$digest() call right after instantiating the controller. Within your tests you have to call scope.$digest() manually after each change in watched variables. This includes after the controller is constructed to record the initial watched value(s).
In addition, as Vitall specified in the comments, you need $watchCollection() for collections.
Changing this watch in your simple example resolves the issue:
$scope.$watch('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
console.log('testing');
$scope.test = 'test';
}
});
to:
$scope.$watchCollection('subject_list', function(newValue, oldValue){
if(newValue !== oldValue) {
console.log('testing');
$scope.test = 'test';
}
});
I made a jsfiddle to demonstrate the difference:
http://jsfiddle.net/ydv8k4zy/ - original with failing test - fails due to using $watch, not $watchCollection
http://jsfiddle.net/ydv8k4zy/1/ - functional version
http://jsfiddle.net/ydv8k4zy/2/ - $watchCollection fails without the initial scope.$digest.
If you play around With console.log on the second failing item you'll see that the watch is called with the same value for old and new after the scope.$digest() (line 25).
The reason that this isn't testable is because the way that the functions are passed into the watchers vs the way that spyOn works. The spyOn method takes the scope object and replaces the original function on that object with a new one... but most likely you passed the whole method by reference into the $watch, that watch still has the old method. I created this non-AngularJS example of the same thing:
https://jsfiddle.net/jonhartmann/9bacozmg/
var sounds = {};
sounds.beep = function () {
alert('beep');
};
document.getElementById('x1').addEventListener('click', sounds.beep);
document.getElementById('x2').addEventListener('click', function () {
sounds.beep = function () {
alert('boop');
};
});
document.getElementById('x3').addEventListener('click', function () {
sounds.beep();
});
The difference between the x1 handler and the x3 handler is that in the first binding the method was passed in directly, in the second the beep/boop method just calls whatever method is on the object.
This is the same thing I ran into with my $watch - I'd put my methods on a "privateMethods" object, pass it out and do a spyOn(privateMethods, 'methodname'), but since my watcher was in the format of $scope.$watch('value', privateMethods.methodName) it was too late - the code would get executed by my spy wouldn't work. Switching to something like this skipped around the problem:
$scope.$watch('value', function () {
privateMethods.methodName.apply(null, Array.prototype.slice.call(arguments));
});
and then the very expected
spyOn(privateMethods, 'methodName')
expect(privateMethods.methodName).toHaveBeenCalled();
This works because you're no longer passing privateMethods.methodName by reference into the $watch, you're passing a function that in turn executes the "methodName" function on "privateMethods".
Your problem is that you need to call $scope.$watchCollection instead of plain $watch.
$watch will only respond to assignments (i.e scope.subject_list = ['new item']).
In addition to assignments, $watchCollection will respond to changes to lists (push/splice).
I've searched high and low, tried many 'solutions' and cannot get angular to watch a service correctly.
Service:
self = {}; // or self = this;
self.testVar = null;
$interval(function() {
self.testVar = Math.random();
}, 1000);
return self;
Controller:
$scope.testVar = myService.testVar;
$scope.$watch('testVar', function(newVal, oldVal) {
console.log(newVal, oldVal);
})
This code doesn't work. I'd like to avoid using $broadcast as it seems like angular is supposed to handle service updates magically. Any insights are appreciated.
EDIT: Solution in controller:
$scope.$watch(
function() { return myService.testVar },
function(newVal) {
console.log(newVal);
}
)
If anyone cares to explain this behaviour, it would benefit those experiencing the same problem.
Upon further investigation, changing the $interval timer to 100ms still results in the controller printing to the console every 1000ms which suggests $scope.$watch is set at a max rate of 1 second. I suppose it's best to use $broadcast or a observer/callback method instead O_o
$scope.testVar = myService.testVar;
$scope.$watch('testVar', function(newVal, oldVal) {
console.log(newVal, oldVal);
})
In this code you set $scope.testVar from the service. Then you watch it. Nothing changes it again. In your "solution", it works because you are watching the "live" value from the service. You do not need to use $broadcast or observer or callbacks. This will work as expected when you are watching the service value and updating the scope.