14

Say I have an object with keys corresponding to products and values corresponding to objects which in turn have keys corresponding to price points at which those products have sold, and values corresponding to amount sold.

For example, if I sold 10 widgets at $1 and 5 widgets at $2, I'd have the data structure:

{ 'widget': {'1': 10, '2': 5} }

I'd like to loop over this structure and generate rows in a table such as this one:

thing   price  amount
---------------------
widget  $1     10
widget  $2     5

In Python it's possible to nest list comprehensions to traverse lists data structures like this. Would such a thing be possible using ng-repeat?

Andrey Fedorov
  • 9,148
  • 20
  • 67
  • 99
  • Just for the search engines, i will add for the programmers from c# background, "how doing SelectMany" in angularjs" – dovid Jun 12 '17 at 11:09

4 Answers4

18

How about this?

http://plnkr.co/edit/ZFgu8Q?p=preview

Controller:

$scope.data = {
  'widget1': {
    '1': 10,
    '2': 5
  },
  'widget2': {
    '4': 7,
    '6': 6
  }
};

View:

<div ng-controller="MyCtrl">
    <table>
      <thead>
        <tr>
          <td>thing</td>
          <td>price</td>
          <td>amount</td>
        </tr>
      </thead>
      <tbody ng-repeat="(productName, productData) in data">
        <tr ng-repeat="(price, count) in productData">
            <td>{{productName}}</td>
            <td>{{price|currency}}</td>
            <td>{{count}}</td>
        </tr>
      </tbody>
    </table>
</div>

Output:

thing   price   amount
----------------------
widget1 $1.00   10
widget1 $2.00   5
widget2 $4.00   7
widget2 $6.00   6

This would output a tbody per product (thanks to Sebastien C for the great idea). If needed, you can differentiate between the first, middle and last tbody (using ng-repeat's $first, $middle and $last) and style them with ng-class (or even native CSS selectors such as :last-child -- I would recommend ng-class though)

holographic-principle
  • 19,688
  • 10
  • 46
  • 62
  • 2
    I would use a multiple tbody instead of multiple table – Sebastien C. Aug 24 '13 at 10:49
  • That's definitely something like what I'm looking for, but I'm afraid using separate tables/tbodies isn't really an option. If it were magically possible to repeat an attribute on an element, that would be ideal. – Andrey Fedorov Aug 26 '13 at 23:45
  • Oh, if you mean just 'magically' shove the *outer* repeat under the rug, that's not possible I'm afraid... Would you mind elaborating on what the restriction with `tbody` is? – holographic-principle Aug 27 '13 at 00:04
13

ng-repeat does not currently have a possible way to complex iterate inside objects (the way it's possible in python). Check out the ng-repeat source code and note that the regex expression matched is:

(key, value) in collection - and that they push into the key array and assign to the value list, and so you cannot possibly have a complex ng-repeat sadly...

There are basically 2 types of solutions which were already answered here:

  1. Nested ng-repeat like the first answer suggested.
  2. Rebuilding your data object to fit 1 ng-repeat like the second answer suggested.

I think solution 2 is better as I like to keep my sorting & coding logic inside the controller, and not deal with it in the HTML document. This will also allow for more complex sorting (i.e based on price, amount, widgetName or some other logic).

Another thing - the second solution will iterate over possible methods of a dataset (as hasOwnProperty wasn't used there).

I've improved the solution in this Plunker (based on the finishingmove Plunker) in order to use angular.forEach and to show that the solution is rather simple but allows for complex sorting logic.

$scope.buildData = function() {

  var returnArr = [];

  angular.forEach($scope.data, function(productData, widget) {
      angular.forEach(productData, function( amount, price) {
        returnArr.push( {widgetName: widget, price:price, amount:amount});            
      });
  });
   //apply sorting logic here
  return returnArr;
};
$scope.sortedData = $scope.buildData();

and then in your controller:

<div ng-controller="MyCtrl">
    <table>
      <thead>
        <tr>
          <td>thing</td>
          <td>price</td>
          <td>amount</td>
        </tr>
      </thead>
      <tbody>
      <tr ng-repeat="item in sortedData">
        <td>{{ item.widgetName }}</td>
        <td>{{ item.price|currency }}</td>
        <td>{{ item.amount }} </td>
        </tr>
      </tbody>
    </table>
</div>
Gilad Peleg
  • 2,010
  • 16
  • 29
3

Just transform your object to an array... it's pretty easy in JS. Something like:

$scope.data = { 'widget': { '1': 10, '2': 5 } };

var tableData = [];
for (item in $scope.data) {
    var thing = item;
    for (subitem in $scope.data[thing]) {
        tableData.push({
            thing: thing,
            price: subitem,
            amount: $scope.data[thing][subitem]
        });
    }
}

I've created a jsfiddle with this example: http://jsfiddle.net/b7TYf/

Shay Friedman
  • 4,808
  • 5
  • 35
  • 51
  • That's certainly an option, but I'd prefer to not re-construct a data structure solely for the purpose of displaying it. Still, I'll accept your answer if no one comes up with a better one. – Andrey Fedorov Aug 21 '13 at 04:30
1

I used a simple directive which has a recursive function to loop over my nested object and create nested elements. This way you can keep your nested object structure.

Code:

angular.module('nerd').directive('nestedItems', ['$rootScope', '$compile', function($rootScope, $compile) {
    return {
        restrict: 'E',
        scope: false,
        link: function(scope, element, attrs, fn) {

            scope.addElement = function(elem, objAr) {

                var ulElement = angular.element("<ul></ul>");

                if (objAr ==  undefined || objAr.length == 0) {
                    return [];
                }

                objAr.forEach(function(arrayItem) {
                    var newElement = angular.element("<li>"+arrayItem.val+"</li>");
                    ulElement.append(newElement);
                    scope.addElement(newElement,arrayItem.sub);
                });

                elem.append(ulElement);
            };
            
            scope.addElement(element,scope.elements);

        }
    };
}]);

My Object :

$scope.elements = [

      {
          id: 1,
          val: "First Level",
          sub: [

              {
                  id: 2,
                  val: "Second Level - Item 1"
              },
              {
                  id: 3,
                  val: "Second Level - Item 2",
                  sub: [{
                      id: 4,
                      val: "Third Level - Item 1"
                  }]
              }
          ]
      }
  ];

My HTML

<nested-items></nested-items>
Aravind
  • 550
  • 7
  • 17