6

Really hoping someone can help me with a problem I've had a couple of times recently.

Let's say I have two objects in AngularJS.

$scope.fields = ['info.name', 'info.category', 'rate.health']

$scope.rows = [{
    info: { name: "Apple", category: "Fruit"},
    rate: { health: 100, ignored: true}
},{
    info: { name: "Orange", category: "Fruit"},
    rate: { health: 100, ignored: true}
},{
    info: { name: "Snickers", category: "Sweet"},
    rate: { health: 0, ignored: true}
}]

I would then like to display a table in a view that shows only the fields in the $scope.fields. This would be super easy if the table was flat, and I know I could flatten it using JavaScript, but there must be a way to do this by converting the dot notation to the path.

I have added a JSFiddle to demonstrate the problem I'm having:

JSFiddle: http://jsfiddle.net/7dyqw4ve/1/

I have also tried doing something as suggested below, but the problem is its terrible practice to use functions in the view: Convert JavaScript string in dot notation into an object reference

If anyone has any ideas I would greatly appreciate it.

Community
  • 1
  • 1
Mark
  • 2,184
  • 1
  • 16
  • 28
  • The approach suggested in your linked question seems sound - can you not just wrap that in a filter? – Simon Robb Mar 11 '15 at 00:13
  • 1
    I thought about that too but (and excuse me if I'm incorrect), from a performance point of view, would you be sending the full dataset to a filter multiple times therefore decreasing the speed at which the entire collection of rows would be run? Genuin question – Mark Mar 11 '15 at 00:15
  • 1
    Plus you should be really careful while using filters on DOM especially if it does extensive operation, filters run as many times as it takes to stabilize during a single digest cycle. You could as well have the controller set up the view model on the scope appropriately. – PSL Mar 11 '15 at 00:19
  • Sure thing - they're good points and you're absolutely right. I'd typically keep an in-memory cache in the filter for this kind of thing. – Simon Robb Mar 11 '15 at 00:29

2 Answers2

5

In this case you can make use of angular's built in evaluator exposed via the property $eval on the scope (not the vanilla eval) to evaluate any angular expression (not for evaluating javascript as eval does). Angular internally uses this to evaluate the expression on the scope while resolving the bindings.

Example:-

{{$eval(field, row)}}

You could do:-

 <th ng-repeat="field in fields">{{$eval(field, row)}}</th>

or wrap the logic in the controller and expose it via a method on the scope.

  <th ng-repeat="field in fields">{{getValue(field, row)}}</th>

and

$scope.getValue = function(exp, row){
    return $scope.$eval(exp, row);
}

Demo

You could as well use $parse (inject $parse) service.

$scope.getValue = function(exp, row){
    return $parse(exp)(row);
}

and

<th ng-repeat="field in fields">{{getValue(field, row)}}</th>

Demo

Community
  • 1
  • 1
PSL
  • 123,204
  • 21
  • 253
  • 243
  • Thanks @PSL. Do you know if this would be considered under **eval is evil**? Is it bad practice, or acceptable under these circumstances? – Mark Mar 11 '15 at 00:11
  • 2
    @Mark This is not the `eval` – PSL Mar 11 '15 at 00:11
0

A minor variation on PSL's answer that also works:

<th ng-repeat="field in fields">{{$eval('row.'+field)}}</th>

The $eval('row.'+field) first results in row.info.name for fields[0], for example. And then the binding {{row.info.name}} results in Apple.

JSFiddle: http://jsfiddle.net/tbmpls/xhon86p7/8/

Nice, simple and easily understandable, I think.

It seems that performance should be roughly equivalent to a straight {{row.info.name}} binding because creating the string should be incredibly quick - but interested in anyone's comments on this.

(Btw, this also works inside an ng-repeat where your list is nested in an object like :

<span ng-repeat="val in $eval('myobj.'+myPathInDotNotation) track by $index">

but as with all things ng-repeat, you will definitely want to check performance on this)

tbmpls
  • 221
  • 3
  • 8