2

I have a simple ngRepeat like the following:

<some-element ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id">
    <!-- stuff -->
</some-element>

arrayOfRecords is updated from a server and may contain new data.

ngRepeat's track by feature can figure out when a new element is added to the array and automatically updates the DOM without changing the existing elements. I would like to hook into that code and execute a callback function when there's new data coming in or old data is removed. Is it possible to easily do this via Angular?

From what I understand, there's a $$watchers which triggers callbacks whenever there's changes to certain variables, but I don't know how to go about hacking that. Is this the right direction?


NOTE: I know I can manually save the arrayOfRecords and compare it with the new values when I fetch them to see what changed. However, since Angular already offers a track by feature which has this logic, it would be nice if I can have Angular automatically trigger an event callback when an element is added or removed from the array. It doesn't make sense to duplicate this logic which already exists in Angular.

user193130
  • 7,360
  • 4
  • 34
  • 55
  • its is hard to check that object has changed or not? – Pankaj Parkar Jun 04 '15 at 17:55
  • @pankajparkar Nope, by why reinvent the wheel when it already exists within Angular? – user193130 Jun 04 '15 at 17:56
  • 1
    You mean `$scope.$watchCollection(..)`? `track by` doesn't figure out anything. – a better oliver Jun 04 '15 at 17:57
  • @zeroflagL Is that what `track by` executes under the hood? (I haven't dug into the Angular source yet...) – user193130 Jun 04 '15 at 17:59
  • Again, `track by` doesn't execute anything, the `ngRepeat` directive does. And yes, it executes `$watchCollection`. – a better oliver Jun 04 '15 at 18:12
  • i'd suggest not to add a watch for that, it is a server refresh and it happens only when it is needed. So you could just DIF the items. – PSL Jun 04 '15 at 18:14
  • This feels like an XY Problem to me. Why would you need to have Angular tell you when the collection has been modified? Wouldn't you already know that the collection has been modified in the controller that did the modification in the first place? – Claies Jun 04 '15 at 18:17
  • @Claies Acc to OP, it is for server refresh. Controller by itself does not know about it unless it does a dif between oldList - newList to figure out. Best bet would be do a dif in the controller, OP is trying to get away from doing the dif and trying to figure out if there is already a built in way to do it. – PSL Jun 04 '15 at 18:21
  • If you are refreshing data from the server correctly, Angular should know that the data has been updated; If you are using `$http`, `resource`, restangular, or the like, Angular will definitely know. If you are using JQuery or some other service, the controller can be updated with `$scope.$apply()`. If the server is rewriting entire sections of JavaScript behind Angular's back, that's a design flaw. Either way, the controller **must** know the `arrayOfRecords` has changed, else angular wouldn't know to re-render it in the first place. – Claies Jun 04 '15 at 18:36
  • 1
    @Claies i think you misunderstood (or me). As far as i understand the Question has nothing to do with all that you mentioned :) . To simplify OP wants to know diff between array1 - array2 for some logic perhaps... So what OP is saying angular is figuring out in `track by` logic so can i use something like that built in which gives me difference between 2 lists without me having to write any code. – PSL Jun 04 '15 at 18:39
  • @PDL the OP specifically said that he wants to know when angular detects the change and updates the DOM, my point is that the change can't happen without the controller knowing, so trying to have angular tell you something you already know makes no sense at all. – Claies Jun 04 '15 at 18:41
  • 1
    @Claies That is the approach of OP's thought i believe or in otherwords `he wants to know when angular detects the change and updates the DOM` --> when angular finds out when an item is removed/added when _entire list is refreshed_ can it notify me about it so that i know which all items are unique and i can do something on that list of unique items (i can save myself from writing the Diffing logic). Anyways that is how i interpreted it. OP should be able to clarify. – PSL Jun 04 '15 at 18:43
  • @Claies Also `my point is that the change can't happen without the controller knowing` --> not really because if the controller just assigns a new list (from an ajax call) to the scope property (which is bound to ng-repeat) it by default does not know what has changed unless it itself has a logic to figure out the difference between old list and newly refreshed list. I think you are talking about controller running a function to add item /remove item in which case yes ofcourse it knows about it. – PSL Jun 04 '15 at 18:46
  • @PSL hmm, I don't see how that is possible; something in the angular code had to make the change somewhere; the server can't magically change the data in the client's memory; again, that's why I feel like this is an XY problem, and we are missing some piece of the puzzle. even if it were an update in an ajax call, if you need to know what changed in the list, do it in the ajax call, don't try to rely upon angular to tell you that you missed something..... – Claies Jun 04 '15 at 18:48
  • @Claies This definitely is an XY problem, but OP has a good(valid) thoughts on what track by _(`something in the angular code had to make the change somewhere` to figure out what items is added and removed and refresh only those DOM nodes for ng-repeat)_ does and whether it can be leveraged (which unfortunately can't directly). – PSL Jun 04 '15 at 18:51
  • @Claies Thanks for your thoughts but I think you're misunderstanding the problem. @PSL seems to understand my situation pretty clearly. **Yes**, my code will know *WHEN* `arrayOfRecords` will be modified. **But that's not the point.** It needs to know *IF* the new data contains **new records**, or if old records were **removed**. As I've already said in the question, I understand it's possible to diff the changes when I fetch new data into `arrayOfRecords`, but the diff logic already exists in the `track by` feature of `ngRepeat`, and it really makes no sense to duplicate it if not necessary. – user193130 Jun 04 '15 at 21:22
  • @Claies "so trying to have angular tell you something you already know makes no sense at all": It does actually... if I process (diff) the data to do a certain thing, and Angular does the same processing to the data again for its own need, then you end doing extra work. Not only for the extra code written (not DRY), but also for the extra execution time required ~O(n*n) work twice. "my point is that the change can't happen without the controller knowing": This can actually happen as not all `$scope` changes must be made within a controller. But then this has nothing to do with the problem... – user193130 Jun 04 '15 at 21:43
  • Perhaps this analog would help? You are renovating a house and you hire an electrician to rewire all the rooms for you. You also need to install new pipes but you are a plumber so you can do this yourself. Now the electrician (Angular) is very good at what he does, but also very rigid in his strategy. The only way he works is if he tears out all the existing drywalls, install the wires, and reinstall the drywall. Wouldn't it be nice if you can ask the electrician to let you know when he finished installing the wires so he can wait on you to install the pipes (a callback event)? – user193130 Jun 04 '15 at 21:55
  • If not, you would have to once again tear out the drywall, install your pipes, then reinstall new drywall. As the plumber, I think you would also be quite upset knowing that the electrician could've waited for you. BUT he didn't :( **YES, YOU DO KNOW** that all the rooms will have its drywalls torn out. However, this doesn't change the fact that you cannot halt the electrician from putting the drywalls back in and forcing you to do the same work again. – user193130 Jun 04 '15 at 21:57
  • Except that isn't Angular at all... Angular isn't going around updating your data without your knowledge, you have to tell it what to use, which assumes you know what the data is before Angular does... – Claies Jun 04 '15 at 22:04
  • We should probably just agree to disagree here, because my take is definitely that you should never need Angular to tell you when it's ok to work with your data. – Claies Jun 04 '15 at 22:06
  • As the person who hired the electrician, yes, I would know the electrician would be updating my rooms..... – user193130 Jun 04 '15 at 22:06
  • I think you're still misunderstanding the problem, but thanks for your input anyway. – user193130 Jun 04 '15 at 22:07
  • I'm sure I don't. Perhaps if you can illustrate in some way how you think angular is updating your data before your controller has a chance to see the change? Or why you feel it's necessary to wait for angular to update the DOM before you do whatever you need? – Claies Jun 04 '15 at 22:25
  • As I read this over again, I can see where I probably misinterpreted your intent. You aren't necessarily worried about ng-repeat specifically, more you are interested in using it's monitoring techniques. Angular does have some features for dealing with changes in collections, maybe this is what you are looking for? http://www.bennadel.com/blog/2566-scope-watch-vs-watchcollection-in-angularjs.htm – Claies Jun 04 '15 at 22:41
  • 1
    I think you understand my problem now -- I'm interested in how `ngRepeat`'s `track by` keeps track of effective changes to the model (analogy: how the electrician pulls out the drywall). The link you provided talks about how Angular tracks changes, but doesn't talk about how it knows which elements effectively changed in a collection. I believe I have enough understanding of that part of Angular already to know that it's not what I am looking for though. Anyway, thanks for trying to understand. I realize it's probably not a common problem as most people just don't care about optimization. – user193130 Jun 04 '15 at 22:53
  • @Claies there you go back to watchcollection.. :) (somebody had commented it earlier already). Even if OP had to just find DIF why would you even create an unwanted watch when you have the oldList and newList at hand. – PSL Jun 04 '15 at 23:23

2 Answers2

3

Probably you could create a directive and add it along with ng-repeat, so the directive when created(when item is added by ng-repeat) will emit an event and similarly when the item is destroyed it will emit another event.

A simple implementation here:

.directive('tracker', function(){
  return{
    restrict:'A',
    link:function(scope, el, attr){
      scope.$emit('ITEM_ADDED', scope.$eval(attr.tracker))
      scope.$on('$destroy', function(){
        scope.$emit('ITEM_REMOVED', scope.$eval(attr.tracker))
      });
    }
  }
});

and use it as:

<some-element 
       ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id"
       tracker="item">

and listen for these events at the parent controller for example.

Demo

Or using function binding but in a different way, without using isolate scope for that.

.directive('tracker', function() {
  return {
    restrict: 'A',
    link: function(scope, el, attr) {
      var setter = scope.$eval(attr.tracker);
      if(!angular.isFunction(setter)) return;
      setter({status:'ADDED', item:scope.$eval(attr.trackerItem)});


      scope.$on('$destroy', function() {
         setter({status:'REMOVED', item:scope.$eval(attr.trackerItem)});
      })
    }
  }
});

Demo


The one above was specific to your question since there is no other built in way, Note that if you were to really find out the items added/removed, you could as well do it in your controller by diffing the 2 lists. You could try use lodash api like _.unique or even simple loop comparisons to find the results.

  function findDif(oldList,newList){
       return {added:_.uniq(newList, oldList), removed:_.uniq(oldList, newList)};
 }

Demo

PSL
  • 120,386
  • 19
  • 245
  • 237
  • Ah thanks I will try this out later. BTW do you know off the top of your head if there's a huge performance hit for something like this? – user193130 Jun 04 '15 at 18:01
  • @user193130 You will have to try it out. But definitely best way would be just dif it in your controller itself. – PSL Jun 04 '15 at 18:10
  • @user193130 How exactly did you hope? or in otherwords how do you expect the notification to happen? function invocation over event or what else? **[Like this?](http://codepen.io/pramodsankarl/pen/rVybXb?editors=101)** – PSL Jun 04 '15 at 23:48
  • 1
    Ah you read my mind regarding how I want the event-handling function passed as an attribute lol. Mainly though, I was hoping for an event that gets triggered only once when the `arrayOfRecords` was changed, with arguments `arrayOfAddedRecords` and `arrayOfRemovedRecords`, instead of individually triggered for each modified record. Now that I think about it more, I guess it's probably impossible to do without modifying `ngRepeat`? I have to say... I think tracking by DOM change is actually a pretty ingenious way to go about solving this problem, even though it wasn't exactly what I wanted. – user193130 Jun 05 '15 at 18:06
  • 1
    Such an excellent answer that taught me a lot more about Angular that I have no choice but to accept it :) I don't have time to dig into Angular to see if it's possible to do what I wanted originally, but I will post back if I ever do find a solution in case you are interested. – user193130 Jun 05 '15 at 18:08
  • @user193130 to be frank i though it was a good idea to utilize track by logic (i'd never though about it until i saw this question from you :) ). But yes if it is not so critical as we talked about Do diffing odd 2 lists yourself. If it is so critical to you and if you have UI Services concept you could as well have your server return the added/removed stuff. Other way (from top of my head) would be to associate a service which works with this directive and use `$first/$last` property of ng-repeat to accumulate changed and callback once after $last. – PSL Jun 05 '15 at 18:11
  • Yeah I initially considered server-side diffing, but it's not possible with the current architecture without heavy modifications (can't -- time constraint). I think you're right though -- maybe the best way is probably to diff the list myself rather than have Angular do it. I actually came across [this post](https://www.airpair.com/angularjs/posts/angularjs-performance-large-applications) first which lead me to `track by`. The diff code in section 7.3 would probably work for me but I just thought if `track by` can handle it, it'll be cleaner (and faster according to the author but unconfirmed) – user193130 Jun 05 '15 at 18:19
0

You can change it to:

<div ng-model="arrayOfRecords">
<some-element ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id">
    <!-- stuff -->
</some-element>
</div>

The model will change as soon as arrayOfRecords will change.

Avigail
  • 1
  • 5