AngularJS - $watch() Properties of Dynamic Objects
I've been working on an AngularJS project, and recently came across this interesting problem. We have an object sent to us from the server. This object contains zero or more properties which might be anything, and need to be editable by the user. Now, setting up the form was one thing, which perhaps I'll save for another post. But the interesting part is that, for reasons, I need to watch each property of the object to keep track of which ones have changed.
For instance, if the server sends down the following object:
{
"foo": 1
"bar": "test"
}
then I need to be able to know when either "foo"
or "bar"
have changed.
But, I don't know that the property names are "foo"
or "bar"
ahead of
time. They could be anything! They might not exist, or there could be
hundreds of others.
My solution, which I think is elegant, was inspired by a post by
Alex Choroshin
involving $watch-ing for changes to objects in an array of objects. The
$watch
method, which is usually shown taking in a string as its first
parameter, can also take a function as the first parameter.
With this in mind, we can enumerate the object's properties, and set a watch on
each property individually.
Here is my solution:
$scope.dataChangedFlags = [];
for (var dataKey in $scope.data) {
(function (key) {
$scope.$watch(function ($scope) {
return $scope.data[key];
}, function () {
$scope.dataChangedFlags[key] = true;
});
})(dataKey)
}
So let's break it down to figure out what this actually does:
$scope.dataChangedFlags = [];
First thing's first, we set up an array to hold the flags which we will set to true for a given property when that property is changed.
for (var dataKey in $scope.data) {
// ...
}
This is pretty obvious to anyone with a bit of JavaScript experience. This loop
will set dataKey
to the property name ("foo" or "bar" in the example object
above.)
for (var dataKey in $scope.data) {
(function (key) {
// ...
})(dataKey)
}
This is a workaround for a common scoping issue that people have while creating anonymous functions inside of a loop. We are basically using another anonymous function as a factory method which will spit out a function with the proper scope. See that last link for a longer explanation of why this is needed.
for (var dataKey in $scope.data) {
(function (key) {
$scope.$watch(function ($scope) {
return $scope.data[key];
}, function () {
$scope.dataChangedFlags[key] = true;
});
})(dataKey)
}
And finally, we set up the watch. Notice that we're using the key
variable
instead of dataKey
inside of both our watch expression (the first
argument/function) and the handler (the second argument/function.) This is to
prevent the scoping issue mentioned before.
The first function (the watch expression) returns the value of the property
being watched. This is useful as passing the string "data[" + key + "]"
doesn't seem to work for some reason even though we are using
ng-model="data[key]"
in the template. Perhaps someone else can elaborate on
why this is so, though I guess that it has to do with the complexity of parsing
"data[key]"
such that we can know that it means the same thing as
"data[" + key + "]"
(which would actually be passed to $watch
as
"data[foo]"
.)
The second function (the handler) simply sets the "changed" flag for the
property. Since the scope which key
is in wraps both anonymous functions
passed to $watch
, we can use key
in both methods. This is really convenient
since the property name that's being watched does not get passed into the
handler.
Hopefully this will be of some use to somebody out there. Leave a comment if you have any questions!