In our angularjs projects we are often dealing with existing models that do not always fit to angularjs expectations.
Here is an example.
There is a model consisting of two arrays: for data, and for associated data. How to create an ng-repeat that displays data from both sources?
ng-repeat
Consider a test controller (see a github sources, and a rawgit working sample):
model.controller( "Test", function() { this.records = [ { name: "record 1", state: "Draft" }, { name: "record 2", state: "Public" }, { name: "record 3", state: "Disabled" }, { name: "record 4", state: "Public" }, { name: "record 5", state: "Public" } ]; this.more = [ { value: 1, selected: true, visible: true }, { value: 2, selected: false, visible: true }, { value: 3, selected: true, visible: true }, { value: 4, selected: false, visible: false }, { value: 5, selected: false, visible: true } ]; this.delete = function(index) { this.records.splice(index, 1); this.more.splice(index, 1); }; });
Basically there are three approaches here:
$index
We argued like this:
This is an example of ng-repeat use:
<table border="1"> <tr> <th>[x]</th> <th>Name</th> <th>Value</th> <th>State</th> <th>Actions</th> </tr> <tr ng-repeat="item in test.records track by $index" ng-if="test.more[$index].visible"> <td> <input type="checkbox" ng-model="test.more[$index].selected"/> </td> <td>{{item.name}}</td> <td>{{test.more[$index].value}}</td> <td>{{item.state}}</td> <td> <a href="#" ng-click="test.delete($index)">Delete</a> </td> </tr> </table>
Look at how associated data is accessed: test.more[$index]... Our goal was to optimize that repeating parts, so we looked at ng-init directive.
test.more[$index]...
ng-init
Though docs warn about its use: "the only appropriate use of ngInit is for aliasing special properties of ngRepeat", we thought that our use of ng-init is rather close to what docs state, so we tried the following:
... <tr ng-repeat="item in test.records track by $index" ng-init="more = test.more[$index]" ng-if="more.visible"> <td> <input type="checkbox" ng-model="more.selected"/> </td> <td>{{item.name}}</td> <td>{{more.value}}</td> <td>{{item.state}}</td> <td> <a href="#" ng-click="test.delete($index)">Delete</a> </td> </tr> ...
This code just does not work, as it shows empty table, as if ng-if is always evaluated to false. From docs we found the reason:
ng-if
false
terminal
$scope.more
more.visible
To workaround ng-init/ng-if problem we refactored ng-if as ng-if-start/ng-if-end:
ng-if-start
ng-if-end
... <tr ng-repeat="item in test.records track by $index" ng-init="more = test.more[$index]"> <td ng-if-start="more.visible"> <input type="checkbox" ng-model="more.selected"/> </td> <td>{{item.name}}</td> <td>{{more.value}}</td> <td>{{item.state}}</td> <td ng-if-end> <a href="#" ng-click="test.delete($index)">Delete</a> </td> </tr> ...
This code works much better and shows a correct content. But then click "Delete" for a row with Name "record 2" and you will find that updated table is out of sync for all data that come from test.more array.
test.more
So, why the data goes out of sync? The reason is in the way how the ng-init is implemented: its expression is evaluated just once at directive's pre-link phase. So, the value of $scope.more will persist for the whole ng-init's life cycle, and it does not matter that test.mode[$index] may have changed at some point.
test.mode[$index]
At this point we have decided to introduce a small directive named ui-eval that will act in a way similar to ng-init but that:
ui-eval
This is it:
module.directive( "uiEval", function() { var directive = { restrict: 'A', priority: 700, link: { pre: function(scope, element, attr) { scope.$watch(attr["uiEval"]); } } }; return directive; });
The ui-eval version of the markup is:
... <tr ng-repeat="item in test.records track by $index" ui-eval="more = test.more[$index]" ng-if="more.visible"> <td> <input type="checkbox" ng-model="more.selected"/> </td> <td>{{item.name}}</td> <td>{{more.value}}</td> <td>{{item.state}}</td> <td> <a href="#" ng-click="test.delete($index)">Delete</a> </td> </tr> ...
It works as expected both during initial rendering and when model is updated.
We consider ui-eval is a "better" ng-init as it solves ng-init's silent limitations. On the other hand it should not try to evaluate any complex logic, as it can be often re-evaluated, so its use case is to alias a sub-expression. It can be used in any context and is not limited to items of ng-repeat.
Source code can be found at github, and a working sample at rawgit.
Remember Me
a@href@title, b, blockquote@cite, em, i, strike, strong, sub, super, u