Question

I'm trying to create a set of AngularJS directives that will process an array of objects and perform specific operations using either the objects themselves or perhaps a property or sub-property of the each instance.

For example, if the array contains strings, one such directive might render a comma-separated list of those strings. I anticipate using such a directive like this:

<csv-list items="myArray" />

However, as stated above, I want the implementation to be flexible enough to pass an array of objects to the directive, whereby the directive can be instructed to act on a specific property or sub-property of each instance. If I could pass a lambda expression to the directive, I would imagine using it something like this:

<csv-list items="myArray" member="element => element.name" />

I guess there's a recommended AngularJS pattern to solve such problems, but I am quite new to AngularJS, so I haven't found it yet. Any suggestions would be appreciated.

Thanks, Tim

Was it helpful?

Solution

There are several ways to do this, Using the $parse service may be the easiest

var parser = $parse("name");
var element = {name:"thingA"};
var x = parser(element);
console.log(x); // "thingA"

Parse has been optimized to act quickly in these scenarios (single property look-ups). You can keep the same "parser" function around and invoke it on each element.

You could also split on the '.' and do the simple look-up yourself (reading in 'member' to your directive as a string), in simple form:

var paths = myPath.split('.');
var val = myObj;
for(var i = 0; i < paths.length; i++){
    val = val[paths[i]];
}
return val;

There are also various linq-like libraries that support lambda expressions as strings (linqjs, fromjs). If you've gotta have a fat arrow function.

OTHER TIPS

Your directive can look at other attributes, so you could add a property-name attribute and have your directive manually check that property. To be fancy you could use $parse like ng-repeat does to parse an expression.

<csv-list items="element in myArray" member="element.name">

Another way would be to create a 'property' filter that takes an array of objects and returns an array of property values from that object that you could use like so:

<csv-list items="myArray|property:name">

Here's what you're asking for syntactically (Show me the code - plunkr):

member="'e' |lambda: 'e.name'"

You can do this with something like (I wrote this just for the question, what I do in my apps is outlined below)

app.filter('lambda', [
  '$parse',
    function ($parse) {
        return function (lambdaArgs, lambdaExpression, scope) {
          var parsed = $parse(lambdaExpression);
          var split = lambdaArgs.split(',');
          var result = function () {
            var args = {};
            angular.extend(args, scope || {});
            for (var i = 0; i < arguments.length && i < split.length; i++) {
              args[split[i]] = arguments[i];
            }
            return parsed(args);
          };
          return result;
        }
    }
]);

Advanced usage:

(x, y, z) => x * y * z + a // a is defined on scope
'x,y,z' |lambda: 'x * y * z + a':this

The :this will pass the scope along to the lambda so it can see variables there, too. You could also pass in an aliased controller if you prefer. Note that you can also stick filters inside the first argument to the lambda filter, like:

('x'|lambda:'x | currency')(123.45) // $123.45 assuming en-US locale

HOWEVER I have thus far avoided a lambda filter in my apps by the following:

The first approach I've taken to deal with that is to use lodash-like filters.

So if I have an array of objects and your case and I want to do names, I might do:

myArray | pluck:'name'

Where pluck is a filter that looks like:

angular.module('...', [
]).filter('pluck', [
  function () {
    return function (collection, property) {
      if (collection === undefined) {
        return;
      }

      try {
        return _.pluck(collection, property);
      } catch (e) {
      }
    }
  }
]);

I've implemented contains, every, first, keys, last, pluck, range (used like [] | range:6 for [0,1,2,3,4,5]), some, and values. You can do a lot with just those by chaining them. In all instances. I literally just wrapped the lodash library.


The second approach I've taken is to define functions inside a controller, expose them on the scope.

So in your example I'd have my controller do something like:

$scope.selectName = function (item) { return item.name };

And then have the directive accept an expression - & - and pass selectName to the expression and call the expression as a function in the directive. This is probably what the Angular team would recommend, since in-lining in the view is not easily unit-test-able (which is probably why they didn't implement lambdas). (I don't really like this, though, as sometimes (like in your case) it's strictly a presentation-thing - not a functionality-thing and should be tested in an E2E/Boundary test, not a unit test. I disagree that every little thing should be unit tested as that often times results in architecture that is (overly) complicated (imho), and E2E tests will catch the same thing. So I do not recommend this route, personally, though again I think the team would.)

3.

The third approach I've taken would be to have the directive in question accept a property-name as a string. I have an orderableList directive that does just that.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top