Search code examples
javascriptangularjsrefreshng-options

Re-render ng-options after second-level change in collection


Problem

I have a combo box, basically a select element that is filled with an array of complex objects by ng-options. When I update any object of the collection on second-level, this change is not applied to the combo box.

This is also documented on the AngularJS web site:

Note that $watchCollection does a shallow comparison of the properties of the object (or the items in the collection if the model is an array). This means that changing a property deeper than the first level inside the object/collection will not trigger a re-rendering.

Angular view

<div ng-app="testApp">
    <div ng-controller="Ctrl">
        <select ng-model="selectedOption"
                ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by selectedOption.id">
        </select>
        <button ng-click="changeFirstLevel()">Change first level</button>
        <button ng-click="changeSecondLevel()">Change second level</button>
        <p>Collection: {{ myCollection }}</p>
        <p>Selected: {{ selectedOption }}</p>
    </div>
</div>

Angular controller

var testApp = angular.module('testApp', []);

testApp.controller('Ctrl', ['$scope', function ($scope) {

    $scope.myCollection = [
        {
            id: '1',
            name: 'name1',
            nested: {
              value: 'nested1'
            }
        }
    ];

    $scope.changeFirstLevel = function() {
        var newElem = {
            id: '1',
            name: 'newName1',
            nested: {
              value: 'newNested1'
            }
        };
        $scope.myCollection[0] = newElem;
    };

    $scope.changeSecondLevel = function() {
        var newElem = {
            id: '1',
            name: 'name1',
            nested: {
              value: 'newNested1'
            }
        };
        $scope.myCollection[0] = newElem;
    };

}]);

You can also run it live in this JSFiddle.

Question

I do understand that AngularJS does not watch complex objects within ng-options for performance reasons. But is there any workaround for this, i.e. can I manually trigger re-rendering? Some posts mention $timeout or $scope.apply as a solution, but I could utilize neither.


Solution

  • Yes, it's a bit ugly and needs an ugly work-around.

    The $timeout solution works by giving AngularJS a change to recognise that the shallow properties have changed in the current digest cycle if you set that collection to [].

    At the next opportunity, via the $timeout, you set it back to what it was and AngularJS recognises that the shallow properties have changed to something new and updates its ngOptions accordingly.

    The other thing I added in the demo is to store the currently selected ID before updating the collection. It can then be used to re-select that option when the $timeout code restores the (updated) collection.

    Demo: http://jsfiddle.net/4639yxpf/

    var testApp = angular.module('testApp', []);
    
    testApp.controller('Ctrl', ['$scope', '$timeout', function($scope, $timeout) {
    
      $scope.myCollection = [{
        id: '1',
        name: 'name1',
        nested: {
          value: 'nested1'
        }
      }];
    
      $scope.changeFirstLevel = function() {
        var newElem = {
          id: '1',
          name: 'newName1',
          nested: {
            value: 'newNested1'
          }
        };
        $scope.myCollection[0] = newElem;
      };
    
      $scope.changeSecondLevel = function() {
    
        // Stores value for currently selected index.
        var currentlySelected = -1;
    
        // get the currently selected index - provided something is selected.
        if ($scope.selectedOption) {
          $scope.myCollection.some(function(obj, i) {
            return obj.id === $scope.selectedOption.id ? currentlySelected = i : false;
          });
        }
    
        var newElem = {
          id: '1',
          name: 'name1',
          nested: {
            value: 'newNested1'
          }
        };
        $scope.myCollection[0] = newElem;
    
        var temp = $scope.myCollection; // store reference to updated collection
        $scope.myCollection = []; // change the collection in this digest cycle so ngOptions can detect the change
    
        $timeout(function() {
          $scope.myCollection = temp;
          // re-select the old selection if it was present
          if (currentlySelected !== -1) $scope.selectedOption = $scope.myCollection[currentlySelected];
        }, 0);
      };
    
    }]);