18

I have a table and the user can chose to filter rows in the table based on certain columns and certain values for these columns. The object structure to keep track of this filter looks like:

$scope.activeFilterAttributes = [
    {
        "columnName": "city",
        "values": ["LA", "OT", "NY"]
    },
    {
        "columnName": "weather",
        "values": ["humid", "sunny"]
    }
];

So the objects in the array contain the "columnName" and "values" key. "columnName" signifies the column to consider for the filter while "values" contains the filter values. Basically, the above array will result in rows in the table for which the city column contains "LA", "OT" or "NY" as values and the weather column contains "humid" or "sunny" as the values. Other rows which do not contain these values are not shown.

To help understand this object better, if the user wishes to see only those rows who have "LA" or "NY" in the column for "city", the resultant array will look like:

$scope.activeFilterAttributes = [
    {
        "columnName": "city",
        "values": ["LA", "NY"]
    },
    {
        "columnName": "weather",
        "values": []
    }
];

The user sets or removes these filters. Whenever this happens, the above array is updated. This update happens correctly and I have verified it - no problem here.

The problem lies with the $watch(). I have the following code:

$scope.$watch('activeFilterAttributes', function() {
    //Code that should update the rows displayed in the table based on the filter
}}

While $scope.activeFilterAttributes is updated properly as and when the user updated the filter in the UI, the $watch is not triggered when this is updated. It is triggered the first time when the application loads but future updates have no effect on this.

I have created a fiddle to demonstrate this: http://jsfiddle.net/nCHQV/

In the fiddle, $scope.info represents the rows of the table, so to speak;
$scope.data represents the filter.
Clicking on the button is equivalent to updating the filter(in the case of the fiddle - the data) and thus updating the rows displayed in the table(in the case of the fiddle - the info). But as can be seen, the info is not updated on clicking the button.

Shouldn't $scope.$watch be triggered when the array of objects changes?

user109187
  • 5,265
  • 7
  • 22
  • 25

2 Answers2

66

The $watch method takes an optional third parameter called objectEquality that checks that the two objects are equal, rather than just share the same reference. This is not the default behavior because it is a more expensive operation than the reference check. Your watch isn't being triggered because it still refers to the same object.

See the docs for more info.

$scope.$watch('activeFilterAttributes', function() {
  // ...
}, true); // <-- objectEquality

Add that parameter and all is well.

Cole Tobin
  • 9,206
  • 15
  • 49
  • 74
Josh David Miller
  • 120,525
  • 16
  • 127
  • 95
  • 1
    Thank you Josh - I can't remember the number of times you have provided an awesome explanation to the cause of the issue and a solution. Thank you very much. By the way - I am testing another of your awesome solutions for the question provided in http://stackoverflow.com/questions/15316363/angularjs-how-to-display-length-of-filtered-data - as of now it does what I expect it to and will soon mark it as the correct answer after some more tests. – user109187 Mar 30 '13 at 17:39
  • @JoshDavidMiller, Thanks for the nice explanation. As you said its an expensive operation, could you please let us know how to avoid getting to a situation of using that 3rd parameter. – Rajkamal Subramanian Apr 05 '13 at 05:25
  • @rajkamal I wouldn't say it's "expensive" *per se* but it is *more* expensive than the reference check. It's not always avoidable though. But I would definitely try to avoid doing it with large objects because the larger the object, the longer the check takes. Other than that, I'm not sure there are any good general rules. It's probably one of things to sort out when benchmarking your code. If the digests are taking too long, maybe the model should be reworked a little bit. Sorry I don't have a better answer here. Perhaps someone else will share her wisdom. – Josh David Miller Apr 05 '13 at 05:32
  • That is really good! If only it new what was different. I guess I have to loop through the object until I find the difference? – Design by Adrian May 08 '13 at 23:10
  • @DesignbyAdrian It will give you the new and the old, but if you want to know which specific (potentially nested) property changed, you'll have to loop through I think. There's probably a way to avoid having to know which specific property changed though; what's your use case? – Josh David Miller May 08 '13 at 23:33
  • Just a notice, if you do $scope.$watch('property1+property2',function(){},true) it doesn't seem to work. You need it in a seperate watch. – Dofs Aug 10 '13 at 20:36
  • @JoshDavidMiller how would you handle watching multiple objects, then? E.g. you have an array of 10 or 20 objects, each rendered, and there is a checkbox for each. You want to catch when someone clicks the checkbox. Easy to render the checkbox bound to its own property / with an ng-model, but how do you efficiently catch the changes in real time? – deitch Sep 23 '13 at 13:50
  • And... how would you determine exactly which one changed? – deitch Sep 23 '13 at 13:52
  • @deitch There's often a better way to write the code in the first place. Can you post a Plnkr with a use case? – Josh David Miller Sep 23 '13 at 16:17
  • I had a jsfiddle, so I modified it. Try this, it is a simplified use case. http://jsfiddle.net/tSHvU/1/ Idea is simple. Use logs in, gets a list of events and an on-the-spot ability to change their participation. If they change it, I want to catch inside EventsCtrl and update whatever I need to. – deitch Sep 23 '13 at 17:21
  • @deitch Thanks. To what might you need to react wherein you would need to know which one changed? What's the use case *inside* the watch? – Josh David Miller Sep 23 '13 at 17:36
  • Real-time dynamic update. If someone updates their attendance, I would want to immediately validate, update perhaps their account balance due, etc. A lot of Web stuff nowadays works real-time, as opposed to make lots of changes and then click "Save". Make sense? – deitch Sep 23 '13 at 17:58
  • @deitch Yeah, but if the server sent it in an array, isn't the server going to expect an array in response? My question is really more about what the real-world case is where we would get data in one bulk block (e.g. an array) but have to process each piece individually. I'm asking for more details because, as I said, there's usually a better way to write the code in the first place. – Josh David Miller Sep 23 '13 at 18:12
  • @JoshDavidMiller yes, the server is likely (though not required necessarily) to send an array, but when sending a modification, would I really want to send back all that data for a single data point? First, I might have read access to all of them but write access only to a subset of the data. Second, it might actually be a composite view, whereas modifying a single data point is fine. Third, PUTting 20 items when I could PUT 1 is not best practice. What I really want is `GET /events?invited=true` (or similar) and then `PUT /events/25?attending=true` – deitch Sep 24 '13 at 05:21
  • Essentially, the workflow is: 1. Get all the info and present it. 2. Modify the 1 or 2 items I want to modify. – deitch Sep 24 '13 at 05:23
  • I could always do this http://jsfiddle.net/tSHvU/5/ but it seems very unAngular-ish – deitch Sep 24 '13 at 05:42
  • @deitch On the contrary, I think that's the superior solution and very Angular-ish. It's optimistic, which is good. It only feels strange to you because it's a check box, but imagine the same feature with a button that says "Attend" or "Don't attend" (or whatever), which accomplishes the same thing but will feel more natural to the developer. To the user it makes no difference. – Josh David Miller Sep 24 '13 at 20:14
  • 1
    Heh, I simplified it to a checkbox, but my implementation is exactly that, a sliding button using an ios-style btn-switch. But, I see what you mean about it being Angular-ish. I can do either implementation, and the controller and model are unchanged, only my template changes.. Thanks, @JoshDavidMiller. – deitch Sep 25 '13 at 03:39
  • Actually, @JoshDavidMiller, I figured out why it felt un-Angular-ish. Ideally, the change in the UI would cause a change in the model (ng-model), which the controller would simply catch, if it needed to, using `$watch`. But because it is embedded in an array, it cannot, it would be much better if I could watch an array and be returned just the row or item that changed. – deitch Sep 25 '13 at 05:28
  • But that's in essence what you're doing, @deitch. The view is telling the controller what changed using the `ngClick` directive. `$watch` isn't always the right solution; in fact, in this case I would argue that it's the wrong solution because it's too heavy. The only way to get it to work is to look for deep equality in your array object, which is an expensive operation to perform - it must run at least twice every digest cycle. The `ngClick` method is more performant because it's called *only* on real change, but still keeps the same separation of concerns. – Josh David Miller Sep 25 '13 at 05:35
  • @JoshDavidMiller I understand it. I just implemented it from my jsfiddle into my actual prototype (pun intended) app, and it works like a charm, while keeping proper separation of concerns. I just feel like if it were a change at a simple property level, e.g. `input` bound to `name`, it would immediately catch the change, while with a deep property, e.g. a row of items bound to `events.event.attending`, I need to **tell** Angular to notify the controller that the model has changed. You're right, it is Angular-ish, just requires extra wiring. Am I explaining well here? – deitch Sep 25 '13 at 06:34
  • @deitch Yeah, I get you - and you're right. But the alternative would be nastier. :-) – Josh David Miller Sep 25 '13 at 06:45
  • 1
    Touche. Thanks again for the help. Angular is pretty damn cool, the 2-way binding and clean separation are amazing. But hell of a learning curve! – deitch Sep 25 '13 at 06:47
  • @JoshDavidMiller You're the man. Thanks for this. Was trying to figure out how to resize screen on object change that was occurring on ng-click for Ionic. This did the trick! – Nick Nov 13 '15 at 18:11
5

Accepted answer is a bit out of date now as with AngularJS 1.1.4 the $WatchCollection function was added for use with arrays and other collections, which is far less expensive than a $Watch with the deep-equality flag set to true.

So this new function is now preferable in most situations.

See this article for more detailed differences between $watch functions

Amicable
  • 3,115
  • 3
  • 49
  • 77