Angular JS - Scope Watch Not Updating - javascript

I might be using this wrong, but I have a function that watches a variable and when that variable is changed from the view, the function runs.. but when a sibling function changes that variable the watch doesn't run. Am I coding something wrong?
scope.$watch (settings.number, function(val){
alert('test');
})
scope.exampleObj = {
exampleFunc : function(){
settings.number += 5;
}
};
so when I call scope.exampleObj.exampleFunc(); shouldn't scope watch get called?

Replace string to a function or use $watchCollection, like this:
Using var:
angular.module('TestApp', [])
.controller('MainCtrl', function($scope){
// Object
var settings = {
number: 1,
foobar: 'Hello World'
};
$scope.$watch(function(){ return settings.number; }, function(newValue, oldValue){
console.log('New value detected in settins.number');
});
$scope.$watchCollection(function(){ return settings; }, function(newValue, oldValue){
console.log('New value detected in settings');
});
});
Using $scope:
angular.module('TestApp', [])
.controller('MainCtrl', function($scope){
// Object
$scope.settings = {
number: 1,
foobar: 'Hello World'
};
$scope.$watch('settings.number', function(newValue, oldValue){
console.log('New value detected in $scope.settings.number');
});
});
Example: http://jsfiddle.net/Chofoteddy/2SNFG/
*Note: $watchCollection that is available in AngularJS version 1.1.x and above. It can be really useful if you need to watch multiple values in a array or multiple properties in a object.

The watcher expects the name(ie. string) of a property of inside the scope, not the object itself.
scope.$watch ('settings.number', function(newval,oldval){
alert('test');
});
scope.exampleObj = {
exampleFunc : function(){
scope.settings.number += 5;
}
};
I'm assuming the somewhere you declare scope.settings.number first, and that you meant to set scope.settings.number not settings.number as you can only watch variables which are properties of the angular scope.
Though you may be doing the right thing with just settings.number += 5;, I may just be tired.

You need to set the objectEquality to true . so you can compare for object equality using angular.equals instead of comparing for reference equality.
for more infos visit the documentation
scope.$watch (settings.number, function(val){
alert('test');
},true)
scope.exampleObj = {
exampleFunc : function(){
settings.number += 5;
}
};

Related

AngularJS two controllers with shared model, controller 2 not seeing change to model

Hitting the ceiling of my Angular knowledge and I have been going around in circles on this.
Basically I have video player and chapter list directives, each with a controller. The controllers use the same model service which looks like this:
.service('VideoPlayerModel', function(){
var model = this;
model.chapters = {
chapterPos: 0,
targetChapter:null,
data: []
},
model.getVideoData = function() {
return model.videoData;
};
model.setVideoData = function(vData){
...
...
...
};
});
In the video player controller as the time of the player updates it finds the needed chapter data and updates the model.chapters data like this:
updateChapter: function(currentTime){
var chapters = VideoPlayerModel.chapters;
var chaptersCtrl = videoPlayerCtrl.chapters;
if (chapters.nextChapter.start <= currentTime) {
chapters.chapterPos = chapters.chapterPos + 1;
chaptersCtrl.setChapter(); //This finds and sets the Target Chapter
}
},
After setChapter runs I call console.log(VideoPlayerModel.chapters) and I can see the data model has updated with a result like this:
Object {chapterPos: 1, targetChapter: Object, data: Array[6], nextChapter: Object}
However the watch in the ChapterListCtrl doesn't fire and any of the onscreen items displaying the ChapterPos still show just the initial val of 0.
The controller looks like this:
.controller("ChapterListCtrl", ['$scope', 'VideoPlayerModel', function($scope, VideoPlayerModel) {
$scope.chapters = VideoPlayerModel.chapters;
$scope.$watch(function() { return VideoPlayerModel.chapters; }, function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
});
}])
I have tried different ways and ended up with this, not sure if I am in the complete wrong direction now. Can anyone please help?
You don't need to use $watch, $broadcast or $on. This is best solved by regular JavaScript thinking.
Your problem is $scope.chapters = newValue; That is where you break the binding that your controllers use by introducing a new object unrelated to your service.
What you should to instead is to think about your service model.chapters = {..} and say hey! This is THE ONE object that I will use. And if I want to change the data in this object anywhere, I will switch the data inside the object and NOT assign a new object to the reference I use.
To do this I use the following methods:
transferDataList = function (from, to) {
/*
http://stackoverflow.com/questions/1232040/empty-an-array-in-javascript
*/
to.length = 0;
for (var i = 0; i < from.length; i++) { to.push(from[i]); }
};
transferDataMap = function (from, to) {
/*
http://stackoverflow.com/questions/684575/how-to-quickly-clear-a-javascript-object
*/
var member;
for (member in to) { delete to[member]; }
for (member in from) { to[member] = from[member]; }
};
And when I want to change the data in my object I DON'T do:
$scope.chapters = newValue;
Instead I do:
transferDataMap(newValue, $scope.chapters);
Or:
transferDataList(newValue, $scope.chapters);
This way you will keep your binding and your Angular interfaces will always be updated.
You can use $broadcast() and $on() function to achieve your requirement.
$broadcast() will flush an event to all it's child controller. So, you can $broadcast() an event with your new value to all controllers when you set a new value to your shared model.
Add a broadcast method in your shared service.
model.setVideoData = function(vData){
UpdateYourModel();
// Inform that your model is updated
$rootScope.$broadcast('modelUpdated');
}
And now you can add a listener for the event modelUpdated in all your controllers.
$scope.$on('modelUpdated', function () {
$scope.controllerModel = VideoPlayerModel.getVideoData(); // Update your controller model
}
And also, inject $rootScope to your service,
.service("VideoPlayerModel", ["$rootScope", function($rootScope){
// define your service here.
}] );
That's all !!!
I hope this will help you.
Try changing your watcher to:
$scope.$watch('chapters', function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
});
Alternatively if that doesn't achieve what you want, you can enable a deep watch by passing the third argument:
$scope.$watch('chapters', function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
}, true);
Your watcher doesn't fire because it always returns the same chapters which Angular considers as not-changed because it checks by reference. Your watcher can also be refactored as:
$scope.$watch(function() { return VideoPlayerModel.chapters.length; }, function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
});

Changing controller $scope vars dynamically

So. I have simple controller and service:
angular
.module('field', [])
.controller('FieldController', function(FieldService) {
var vm = this;
vm.name = FieldService.getName();
})
.service('FieldService', function() {
var name = 'John'
this.getName = function() {
return name;
};
this.setName = function(newName) {
name = newName;
};
})
;
Then i have some $interval in anotherService, that getting data every 1 second and calling FieldService.setName:
.service('UpdateService', function($http, FieldService) {
$interval(function() {
$http.get('/somUrl')
.then(function(response) {
FieldService.setName(response.name);
});
});
})
But it won't change my HTML.
If i switch from primitive to object in returning value getName, then it's working.
Is there another approach? I personally think, that this structure i created is bad, but can't understand how it should be done.
JavaScript is always pass-by-value, but when your variable is an object, the 'value' is actually a reference to the object. So in your case, you are getting a reference to the object, not the value. So when the object changes, that change isn't propagated like a primitive would be.
Your code seems a bit incorrect, too. You are setting the value of response.name to FieldService.setName, which is actually a function.
If you want to use the getter/setter approach you have listed, then you could use events to let the controller know that name has changed.
.service('UpdateService', function($http, FieldService, $rootScope) {
$interval(function() {
$http.get('/somUrl')
.then(function(response) {
FieldService.setName(response.name);
$rootScope.$broadcast('nameChanged', {
name : response.name
});
});
});
})
.controller('FieldController', function(FieldService, $scope) {
var vm = this;
vm.name = FieldService.getName();
$scope.$on('nameChanged', function (evt, params) {
vm.name = params.name;
});
})
Another way to accomplish this is to use a $scope.$watch on the service variable in the controller:
.controller('FieldController', function($scope, FieldService) {
$scope.name = FieldService.getName();
$scope.$watch(function () {
return FieldService.getName();
}, function (newVal, oldVal) {
if (newVal !== oldVal) {
$scope.name = newVal;
}
});
})
I would move my $interval function inside a controller and then just update a $scope attribute every second. Then Angular will take care of the rendering.. Or you must also use an $interval function in your controller which gets the service content (ie FieldService.getName) every second.
I would use it this way:
angular
.module('field', [])
.controller('FieldController', function($scope, FieldService) {
$scope.name = function(){
FieldService.getName();
};
})
.service('FieldService', function() {
var name = 'John'
this.getName = function() {
return name;
};
this.setName = function(newName) {
name = newName;
};
});
Use name() in your html to see the update value.And your other service:
.service('UpdateService', function($http, FieldService) {
$interval(function() {
$http.get('/somUrl')
.then(function(response) {
FieldService.setName(response.name);
});
}, 1000);
})
There are numerous ways in which you can achieve this. No way is the best way. Depends on person to person.
Hope this helps.
There are several ways to solve that problem.
1) Move the $interval to controller.Then you will have a variable, which holds that data and you can bind it in view
2) You can use AngularJs Events.$rootScope will help you to send signal and catch it wherever you want.
If you want more info about this solutions, you can see it here:
http://www.w3docs.com/snippets/angularjs/bind-value-between-service-and-controller-directive.html

How to test two-way binding with directive's isolateScope in jasmine+angular?

I want to test that parent scope gets new value after directive changed it, but for some reason it does not work.
This is my directive
angular.module('myModule').directive('myDirective', function(){
return {
template: 'just a template',
restrict: 'A',
scope: {
'model' : '=myDirective'
},
link: function postLink( scope ){
scope.changeModelValue = function( value ){
scope.model = value;
}
}
}
});
This is my test
describe('the test', function(){
var scope, isolateScope, element;
var reset = function(){
scope = null;
isolateScope = null;
element = null;
};
beforeEach( inject(function( $rootScope ){
scope = $rootScope.new();
reset();
}));
var setup = inject(function( $compile ){
element = angular.element('<div my-directive="item"></div>');
element = $compile(element)(scope);
scope.$digest();
isolateScope = element.children().scope();
});
it('should work', inject(function( $timeout ){
scope.item = null;
setup();
expect(typeof(isolateScope.changeModelValue)).toBe('function'); // works!
isolateScope.changeModelValue('new value');
expect(isolateScope.model).toBe('new value'); // works!
// tried all the of this - but didn't help..
waitsFor(function(){
try{ $timeout.flush(); } catch (e) { }
try{ scope.$digest.flush(); } catch (e) { }
try{ isolateScope.$digest(); } catch (e) { }
return scope.reportLesson !== null;
});
runs(function(){
expect(scope.item).toBe('new value'); //fails!!
});
}));
});
As you can see I tried some flushing and such, thinking perhaps there's some async actions that need to happen in order for it to work, but it didn't help.
The test reaches timeout on the waitsFor.
How can I make it work?
Turns out that $digest and $timeout.flush will not affect the binding.
In order to make it work, I had to call isolateScope.$apply(). I would still like to have an explanation for this.
Uh I might be wrong but it looks like the way you're doing the binding there doesn't make sense.
// 2-way-bind the value of 'model' in this scope to the myDirective attribute in the parent scope
scope: {
'model' : '=myDirective'
},
If that were '=item' instead then that makes sense given what you want, so try that.
Another thing you are doing which is a bit odd:
// Give my directive an attribute which is the 'item' element from the scope
element = angular.element('<div my-directive="item"></div>');
So that's a bit weird, read this SO answer: AngularJS Directive Value
You're overloading your directive declaration with an attribute, and you're mixing up scope variables with attributes (which can be accessed through your link function, but not what you're doing).
Edit:
As a final note, you should probably trust AngularJS is doing the binding correctly. It is time consuming to write these kind of tests and you should focus on program logic instead.

How to set a variable in different controller in AngularJS?

I'd like to do simple notifications in angular. Here is the code I've written.
http://pastebin.com/zYZtntu8
The question is:
Why if I add a new alert in hasAlerts() method it works, but if I add a new alert in NoteController it doesn't. I've tried something with $scope.$watch but it also doesn't work or I've done something wrong.
How can I do that?
Check out this plnkr I made a while back
http://plnkr.co/edit/ABQsAxz1bNi34ehmPRsF?p=preview
I show a couple of ways controllers can use data from services, in particular the first two show how to do it without a watch which is generally a more efficient way to go:
// Code goes here
angular.module("myApp", []).service("MyService", function($q) {
var serviceDef = {};
//It's important that you use an object or an array here a string or other
//primitive type can't be updated with angular.copy and changes to those
//primitives can't be watched.
serviceDef.someServiceData = {
label: 'aValue'
};
serviceDef.doSomething = function() {
var deferred = $q.defer();
angular.copy({
label: 'an updated value'
}, serviceDef.someServiceData);
deferred.resolve(serviceDef.someServiceData);
return deferred.promise;
}
return serviceDef;
}).controller("MyCtrl", function($scope, MyService) {
//Using a data object from the service that has it's properties updated async
$scope.sharedData = MyService.someServiceData;
}).controller("MyCtrl2", function($scope, MyService) {
//Same as above just has a function to modify the value as well
$scope.sharedData = MyService.someServiceData;
$scope.updateValue = function() {
MyService.doSomething();
}
}).controller("MyCtrl3", function($scope, MyService) {
//Shows using a watch to see if the service data has changed during a digest
//if so updates the local scope
$scope.$watch(function(){ return MyService.someServiceData }, function(newVal){
$scope.sharedData = newVal;
})
$scope.updateValue = function() {
MyService.doSomething();
}
}).controller("MyCtrl4", function($scope, MyService) {
//This option relies on the promise returned from the service to update the local
//scope, also since the properties of the object are being updated not the object
//itself this still stays "in sync" with the other controllers and service since
//really they are all referring to the same object.
MyService.doSomething().then(function(newVal) {
$scope.sharedData = newVal;
});
});
The notable thing here I guess is that I use angular.copy to re-use the same object that's created in the service instead of assigning a new object or array to that property. Since it's the same object if you reference that object from your controllers and use it in any data-binding situation (watches or {{}} interpolation in the view) will see the changes to the object.

Why is my $watch not working in my directive test - Angularjs

I have a directive in which I pass in an attrs and then it is watched in the directive. Once the attrs is changed, then an animation takes place. My attrs always is undefined when the $watch gets triggered.
App.directive('resize', function($animate) {
return function(scope, element, attrs) {
scope.$watch(attrs.resize, function(newVal) {
if(newVal) {
$animate.addClass(element, 'span8');
}
});
};
});
And here is my test:
describe('resize', function() {
var element, scope;
beforeEach(inject(function($compile, $rootScope) {
var directive = angular.element('<div class="span12" resize="isHidden"></div>');
element = $compile(directive)($rootScope);
$rootScope.$digest();
scope = $rootScope;
}));
it('should change to a span8 after resize', function() {
expect($(element).hasClass('span12')).toBeTruthy();
expect($(element).hasClass('span8')).toBeFalsy();
element.attr('resize', 'true');
element.scope().$apply();
expect($(element).hasClass('span8')).toBeTruthy();
});
});
When the attrs changes, my watchers newValue is undefined and so nothing happens. What do I need to do to make this work? Here is a plunker
You are not watching the value of attrs.resize; you are watching the value pointed by attrs.resize instead, in the test case a scope member called isHidden. This does not exist, thus the undefined.
For what you aare trying to do, the following would work:
App.directive('resize', function($animate) {
return function(scope, element, attrs) {
scope.$watch(
// NOTE THE DIFFERENCE HERE
function() {
return element.attr("resize");
// EDIT: Changed in favor of line above...
// return attrs.resize;
},
function(newVal) {
if(newVal) {
$animate.addClass(element, 'span8');
}
}
);
};
});
EDIT: It seems that the attrs object does NOT get updated from DOM updates for non-interpolated values. So you will have to watch element.attr("resize"). I fear this is not effective though... See forked plunk: http://plnkr.co/edit/iBNpha33e2Xw8CHgWmVx?p=preview
Here is how I was able to make this test work. I am passing in a variable as an attr to the directive. The variable name is isHidden. Here is my test with the updated code that is working.
describe('resize', function() {
var element, scope;
beforeEach(inject(function($compile, $rootScope) {
var directive = angular.element('<div class="span12" resize="isHidden"></div>');
element = $compile(directive)($rootScope);
$rootScope.$digest();
scope = $rootScope;
}));
it('should change to a span8 after resize', function() {
expect($(element).hasClass('span12')).toBeTruthy();
expect($(element).hasClass('span8')).toBeFalsy();
element.scope().isHidden = true;
scope.$apply();
expect($(element).hasClass('span8')).toBeTruthy();
});
});
I am able to access the variable isHidden through the scope that is attached to the element. After I change the variable, the I have to run $digest to update and then all is golden.
I feel that I should probably be using $observe here as was noted by package. I will look at that and add a comment when I get it working.
As Nikos has pointed out the problem is that you're not watching the value of attrs.resize so what you can try doing is this:
Create a variable to hold your data and create these $watch functions:
var dataGetter;
scope.$watch(function () {
return attrs.resize;
}, function (newVal) {
dataGetter = $parse(newVal);
});
scope.$watch(function () {
return dataGetter && dataGetter(scope);
}, function (newVal) {
// Do stuff here
});
What should happen here is that Angular's $parse function should evaluate attrs.resize and return a function like this. Then you pass it the scope and do something. As long as attrs.resize is just a boolean then newVal in the 2nd watch expression should be a boolean, I hope.

Categories

Resources