34

I'm using ng-repeat on a table row with data from a JSON array retrieved from a server. My goal is to have the list update automatically whenever an item is added, removed, or modified on the server, without affecting the unmodified items. In the final implementation, these table rows will also contain bidirectionally bound <input> and <select> elements to send updates back to the server. Some of the available options in the <select> elements will also be generated using ng-repeat directives from another list that may also change.

So far, every time a new array comes from the server (currently polled every two seconds), the entire ng-repeat list is deleted and regenerated. This is problematic because it interferes with text selection, destroys input fields even if they are currently being edited by the user, and probably runs a lot more slowly than necessary.

I've written other web apps that do what I want using jQuery and DOM manipulation, but the code ends up being really hairy and development is time consuming. I'm hoping to use AngularJS and data binding to accomplish this in a fraction of the code and time.

So here's the question: is it possible to update the backing array this way, but only modify the DOM elements corresponding to the items/properties that actually changed?


Here's a minimal test case that simulates periodic polling using a hard-coded array in a timer (see it live at http://jsfiddle.net/DWrmP/). Notice that the text selection is cleared every 500ms due to the elements being deleted and recreated.

HTML

<body ng-app="myApp">
    <table ng-controller="MyController">
        <tr ng-repeat="item in items | orderBy:'id'">
            <td>{{item.id}}</td>
            <td>{{item.data}}</td>
        </tr>
    </table>
</body>

JavaScript

angular.module('myApp', []).controller(
    'MyController', [
        '$scope', '$timeout',
        function($scope, $timeout) {
            $scope.items = [
                { id: 0, data: 'Zero' }
            ];

            function setData() {
                $scope.items = [
                    { id: 1, data: 'One' },
                    { id: 2, data: 'Two' },
                    { id: 5, data: 'Five' },
                    { id: 4, data: 'Four' },
                    { id: 3, data: 'Three' }
                    ];
                $timeout(setData, 500);
            }
            $timeout(setData, 500);
        }
        ]
);
vittore
  • 17,449
  • 6
  • 44
  • 82
nitrogen
  • 1,559
  • 1
  • 14
  • 26

3 Answers3

38

For those finding this from google, the page below describes a feature in AngularJS 1.2 that helps with this problem:

http://www.bennadel.com/blog/2556-Using-Track-By-With-ngRepeat-In-AngularJS-1-2.htm


Edit to add: The most important sentences from the linked post, in case the link ever dies:

With the new "track by" syntax, I can now tell AngularJS which object property (or property path) should be used to associate a JavaScript object with a DOM node. This means that I can swap out JavaScript objects without destroying DOM nodes so long as the "track by" association still works.

nitrogen
  • 1,559
  • 1
  • 14
  • 26
dasho
  • 396
  • 3
  • 2
  • 4
    While the link is informative, then a link will eventually die -- adding the essential parts is better. – Soren May 04 '14 at 14:12
  • 1
    That only works if your entire collection is loaded, if parts of the collection are unloaded you'd still get the issue. – Archimedes Trajano Apr 17 '16 at 04:34
  • Link also provides a smart way to debug and log your DOM information. Thanks! I really love the guy - Ben Nadel. Most of his articles are very helpful. – Anmol Saraf Aug 03 '16 at 19:27
7

I belive this article will explain how ngRepeat works

http://www.bennadel.com/blog/2443-Rendering-DOM-Elements-With-ngRepeat-In-AngularJS.htm

So if you are keeping objects in collection - then yes ( i e $hashKey persist )

Otherwise - no

vittore
  • 17,449
  • 6
  • 44
  • 82
  • I had come across that page before asking this question. While the explanation of $hashKey is helpful, it still isn't clear how to make sure a single property change on a deeply nested object will result in the corresponding component being updated, without having to rebuild the whole ng-repeat section. -- I tried using `angular.extend()` and `jQuery.extend(true)`, but that didn't have the desired effect, and would mean I have to manage addition/removal manually. -- I was hoping AngularJS kept an internal copy of the list it could use to track changes; looks like I'll have to do it myself. – nitrogen Jun 13 '13 at 19:51
  • @nitrogen can you add example code of changes that angular is not tracking for you ? – vittore Jun 13 '13 at 23:24
  • also you example code recreates not only list items, but list itself, every 500 msecs, so angular cant really do anything here, and you will have just constant page rerendering. – vittore Jun 13 '13 at 23:26
  • The example code's intention was to simulate parsing an array from JSON, which would create a new array every time. I'll update the question or post an answer with a better example once I've figured out how well my new approach works (storing an intermediate copy in an object, then sorting into an array whenever items are added or removed). – nitrogen Jun 14 '13 at 01:09
0

I'm planning to build the following solution myself eventually though it's still in my product backlog.

The problem with ng-repeat is it will remove items from the DOM when it needs to so for a table it would mean it will resize and such, but if the data is dynamic, it may flicker because the data changed around and the table size is shifting. Particularly during paging because the whole page may not have loaded yet.

To get around this flickering, the table must not change its number of rows. Instead have an ng-repeat of "displayed" data and just change it as needed without adding or removing items from the array.

Archimedes Trajano
  • 35,625
  • 19
  • 175
  • 265