Angular, watch specific keys in object? - javascript

I'm wondering if it's possible to just watch a specific key in an array of objects, so instead of
$scope.$watch('data', function (newValue) {
would it be possible to do something like
$scope.$watch('data.name', function (newValue) {
This would be in an array, so the desired functionality woud be if the any of the data[i].name items change it would fire?
Don't know of something like this would be possible. Thanks for reading!

You have 2 options:
1) Shallow watch over the Array (with $watchCollection), like this:
$scope.$watchCollection('data.name', function (newValue) {...});
2) Deep watch over the Array, like this:
$scope.$watch('data.name', function (newValue) {...}, true);
With option 1 the $watch function will only get triggered if you are adding/removing elements, but not if you are making changes into one of the Array elements.
On the other hand with option 2, the $watch function will run every time that there is any change.

Here's an example of what you're trying to do from http://blogs.microsoft.co.il/choroshin/2014/03/26/angularjs-watch-for-changes-in-specific-object-property/.
var app=angular.module('App', []);
function ctrl($scope){
$scope.count=0;
$scope.people = [{id:1,name: "bill"}, {id:2,name: "jim"}, {id:3,name: "ryan"}]
$scope.$watch(function($scope) {
return $scope.people.
map(function(obj) {
return obj.name
});
}, function (newVal) {
$scope.count++;
$scope.msg = 'person name was changed'+ $scope.count;
}, true);
}
This watches the name property on each object as you can see from the map function returning obj.name.

Related

Angular deep compare objects except for specific properties

Use case:
$scope.$watch('settings', function (newVal, oldVal) {
if (!angular.equals(oldVal, newVal)) {
setDirty();
}
}, true);
Now this is practically the same as
$scope.$watch('settings', function (newVal, oldVal) {
if (oldVal) {
setDirty();
}
}, true);
Since the $watch already compares the two.
However, there is one property that even though it changes i don't want to setDirty().
This is my working (hacky) solution so far:
$scope.$watch('settings', function (newVal, oldVal) {
if (!oldVal) return;
var editedOldVal = angular.copy(oldVal, {});
var editedNewVal = angular.copy(newVal, {});
delete editedOldVal.propertyIDontWannaWatch;
delete editedNewVal.propertyIDontWannaWatch;
if (!angular.equals(editedOldVal, editedNewVal)) {
setDirty();
}
}, true);
Is there a cleaner way to make angular.equals() or the $watch ignore specific properties?
EDIT:
This answer does not solve my problem since option 1 is not a solution at all, says that if the property that i don't want to watch hasn't changed - do nothing, but this is exactly the property i want to ignore (don't care if it changed or not). option 2 makes that property not comparable at all, i only want to ignore it on this specific case and not make it uncomparable by definition.
You can prefix properties with a $ and it will not be included in the equals comparison. The angular.equals method ignores properties that start with a $.
From the Angular.js code:
if (key.charAt(0) === '$' || isFunction(o1[key])) continue;
For example:
var object1 = {propertyOne: '1', $myCustomHiddenField: 'something'};
var object2 = {propertyOne: '1', $myCustomHiddenField: 'something else'};
var result = angular.equals(object1, object2);//this is true
The equals method can be referenced here.

How to create dependency with Mithril.js?

I have a long list of items that I want to show in a <ul>. I want to add a "filter" input, so the user can narrow down the list of items to those matching the filter.
My controller contains a filter prop and a list array:
function Ctrl() {
this.filter = m.prop('');
this.list = [];
}
I've added an update method to the controller, which looks at the filter prop and updates the contents of the list array:
Ctrl.prototype.update = function (value) {
var _this = this;
if (this.filter()) {
searchItems(this.filter(), function (items) {
_this.list = items;
});
} else {
this.list = [];
}
};
Finally, my view iterates over the list array and renders the items. Additionally, it displays an input on top, bound to the filter prop:
var view = function (ctrl) {
return m('#content', [
m('input', {
oninput: m.withAttr("value", ctrl.filter),
value: ctrl.filter()
}),
m('ul', [
ctrl.list.map(function (item, idx) {
return m('li', m('span', item.getName()));
})
])
]);
};
My question is, how to make the update function fire when the value of filter changes, so that I get the updated list of items?
Do I need to position two oninput events? One to update filter and one to fire update?
Should I use a single oninput event and update the filter property within he update function?
Anything else?
When you use m.withAttr, what you're saying is that when the event handler fires (oninput), you will take some attribute of the element (value) and pass it into your second argument, which is a function (ctrl.filter). Your current sequence of events:
filter property gets updated
mithril redraws
What you want to do, is call the update function (instead of the getter/setter ctrl.filter function), and bind it so you can retain the proper context in the function:
m('input', {
oninput: m.withAttr("value", ctrl.update.bind(ctrl)),
value: ctrl.filter()
}),
Then, in your update function the value will be passed to the function and you can set it there.
Ctrl.prototype.update = function (value) {
this.filter(value);
...
Now what'll happen:
ctrl.filter property gets updated
ctrl.list gets filtered based on ctrl.filter
mithril redraws
Another way to handle this is to not have any "list" property in your controller / model, but to let the view grab a filtered list instead. There's only one thing really changing, after all, and that's the "filter" property. The filtered list is derived from that, so by creating another property on the controller, you're effectively duplicating the same state.
Additionally, you could keep m.withAttr('value', ctrl.filter) and benefit from that simplicity.
Something like:
var filteredItems = ctrl.getFilteredItems();
var view = function (ctrl) {
return m('#content', [
m('input', {
oninput: m.withAttr("value", ctrl.filter),
value: ctrl.filter()
}),
m('ul', [
filteredItems.map(function (item, idx) {
return m('li', m('span', item.getName()));
})
])
]);
};

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.
});

Bidirectional Binding not updating within ng-repeat

I'm having trouble with a bi-directional binding in an ng-repeat. I would expect for the below $watch to be triggered when you select a color from the list.
$scope.$watch('favoriteColors', function (newValue) {
console.log('example-favoriteColors', newValue);
});
I would expect for Orange to appear in $scope.favoriteColors when checked.
Example: http://plnkr.co/edit/k5SEQw4XFnxriD2I8ZG7?p=preview
directive('checkBox', function () {
return {
replace: true,
restrict: 'E',
//require: '^ngModel',
scope: {
'externalValue': '=ngModel',
'value': '&'
},
template: function (el, attrs) {
var html =
'<div class="ngCheckBox">'+
'<span ng-class="{checked: isChecked}">' +
'<input type="checkbox" ng-model="isChecked"/>'+
'</span>'+
'</div>';
return html;
},
controller: ['$scope', '$timeout', function ($scope, $timeout) {
var initialized = false;
console.log($scope.value());
if (angular.isArray($scope.externalValue)) {
$scope.isChecked = $scope.externalValue.indexOf($scope.value()) > 0;
} else {
$scope.isChecked = !!$scope.externalValue;
}
$scope.$watch('isChecked', function (newValue) {
if (angular.isDefined(newValue)) {
//add or remove items if this is an array
if (angular.isArray($scope.externalValue)) {
var index = $scope.externalValue.indexOf($scope.value());
if(index > -1) {
$scope.externalValue.splice(index, 1);
} else if (initialized) {
$scope.externalValue.push($scope.value());
}
} else {
//simple boolean value
$scope.externalValue = newValue;
}
if (initialized)
console.log($scope.externalValue);
}
});
$timeout(function () {
initialized = true;
});
}],
link: function (scope, el, attrs) {
}
};
});
Please check out this plunk: http://plnkr.co/edit/pbHz4ohBPi7iYq6uJI8X?p=preview
There were lots of changes. Some of them are:
The template needs not be a function, since it is static.
The initialized (and consequently the $timeout) is not needed.
I implemented my own indexOf function; there is a chance the objects are not the same in == sense, but equals in the x.name === y.name sense; (I have some doubts about this though)
The add or remove items if this is an array part was wrong; you need to update the array based on the value of isChecked, not based on whether the item already exists in the array (indexOf).
Initialize favoriteColors as an array, not as a single object, to be consistent, i.e. $scope.favoriteColors = [$scope.colors[1]];
(minor) Added a little more descriptive log when favoriteColors change.
Use $watch("favoriteColors", function() {...}, true) to watch for changes inside the array (not the true last argument).
I think it's because you need to be referencing a property on an object instead of the flat array. When you pass a primitive data structure like an array, it gets passed by reference and thus the updates aren't passed along properly. (Post by Mark Rajcok.)
I went ahead and showed this by hacking your plunkr a little bit. I changed $scope.favoriteColors = $scope.colors[1]; to $scope.favoriteColors = {value:$scope.colors[1]}; and changed <check-box ng-model="favoriteColors" value="color"> to <check-box ng-model="favoriteColors.value" value="color">.
Plunkr
You can see in the plunkr that when you hit the checkboxes the console.log statements now go off under the $watch function.
I see that you're using angular-form-ui's checkbox directive.
Use $watchCollection (link to documentation) instead of $watch for watching arrays for changes
Initialize $scope.favoriteColors as an array containing the values that should be checked
I've reverted your changes to angular-form-ui.js as those changes broke the directive. They are now exactly as the code appears in the latest commit on Github (checkbox.js). Only one thing has changed, the initialization of the angular-form-ui module by adding [] as the second argument to that first line.
Here is the updated plunker: http://plnkr.co/edit/mlUt46?p=preview

Watch multiple $scope attributes

Is there a way to subscribe to events on multiple objects using $watch
E.g.
$scope.$watch('item1, item2', function () { });
Starting from AngularJS 1.3 there's a new method called $watchGroup for observing a set of expressions.
$scope.foo = 'foo';
$scope.bar = 'bar';
$scope.$watchGroup(['foo', 'bar'], function(newValues, oldValues, scope) {
// newValues array contains the current values of the watch expressions
// with the indexes matching those of the watchExpression array
// i.e.
// newValues[0] -> $scope.foo
// and
// newValues[1] -> $scope.bar
});
Beginning with AngularJS 1.1.4 you can use $watchCollection:
$scope.$watchCollection('[item1, item2]', function(newValues, oldValues){
// do stuff here
// newValues and oldValues contain the new and respectively old value
// of the observed collection array
});
Plunker example here
Documentation here
$watch first parameter can also be a function.
$scope.$watch(function watchBothItems() {
return itemsCombinedValue();
}, function whenItemsChange() {
//stuff
});
If your two combined values are simple, the first parameter is just an angular expression normally. For example, firstName and lastName:
$scope.$watch('firstName + lastName', function() {
//stuff
});
Here's a solution very similar to your original pseudo-code that actually works:
$scope.$watch('[item1, item2] | json', function () { });
EDIT: Okay, I think this is even better:
$scope.$watch('[item1, item2]', function () { }, true);
Basically we're skipping the json step, which seemed dumb to begin with, but it wasn't working without it. They key is the often omitted 3rd parameter which turns on object equality as opposed to reference equality. Then the comparisons between our created array objects actually work right.
You can use functions in $watchGroup to select fields of an object in scope.
$scope.$watchGroup(
[function () { return _this.$scope.ViewModel.Monitor1Scale; },
function () { return _this.$scope.ViewModel.Monitor2Scale; }],
function (newVal, oldVal, scope)
{
if (newVal != oldVal) {
_this.updateMonitorScales();
}
});
Why not simply wrap it in a forEach?
angular.forEach(['a', 'b', 'c'], function (key) {
scope.$watch(key, function (v) {
changed();
});
});
It's about the same overhead as providing a function for the combined value, without actually having to worry about the value composition.
A slightly safer solution to combine values might be to use the following as your $watch function:
function() { return angular.toJson([item1, item2]) }
or
$scope.$watch(
function() {
return angular.toJson([item1, item2]);
},
function() {
// Stuff to do after either value changes
});
$watch first parameter can be angular expression or function. See documentation on $scope.$watch. It contains a lot of useful info about how $watch method works: when watchExpression is called, how angular compares results, etc.
how about:
scope.$watch(function() {
return {
a: thing-one,
b: thing-two,
c: red-fish,
d: blue-fish
};
}, listener...);
$scope.$watch('age + name', function () {
//called when name or age changed
});
Here function will get called when both age and name value get changed.
Angular introduced $watchGroup in version 1.3 using which we can watch multiple variables, with a single $watchGroup block
$watchGroup takes array as first parameter in which we can include all of our variables to watch.
$scope.$watchGroup(['var1','var2'],function(newVals,oldVals){
console.log("new value of var1 = " newVals[0]);
console.log("new value of var2 = " newVals[1]);
console.log("old value of var1 = " oldVals[0]);
console.log("old value of var2 = " oldVals[1]);
});

Categories

Resources