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.
<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>
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.
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.
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);
};
}]);