user1569339
user1569339

Reputation: 683

Flattening JS objects with nested arrays into a single HTML table row with Knockout.js

I have an array of payment data that looks similar to:

[{
  amount: "$202.12",
  date: "10/13/2013",
  items: [{type: "Service", amount: "$190.00"}, {type: "Fee", amount: "12.12"}],
  status: "Paid"
},
// More of the same...
]

The type of the items are arbitrary and can differ for each payment.

Now I want to display each payment in the array as a row in a table. However, I want the elements of the items array to be flattened out on the same row, so that the table would look like

| Date       | Amount | Fee   | Service | Status |
|------------|--------|-------|---------|--------|
| 10/13/2013 | 202.12 | 12.02 | 190.00  | Paid   |
|------------|--------|-------|---------|--------|
| So on and so forth ....                        |

I am using knockout.js to generate these tables, but I cannot figure out how to flatten the objects into a single row using the foreach binding.

Turning each item type into a property of the payment object like

var payment = {
  amount: "$202.12",
  date: "10/13/2013",
  items: [{type: "Service", amount: "$190.00"}, {type: "Fee", amount: "12.12"}],
  status: "Paid"
};
var flatPayment = {
  amount: payment.amount,
  status: payment.status,
  date: payment.date
};

for (var i = 0; i < payment.items.length; i++) 
  flatPayment[payment.items[i].type] = payment.items[i].amount;

could work, but however, I do not know the item types beforehand and therefore cannot bind the text of each table cell.

Any suggestions?

Upvotes: 1

Views: 1446

Answers (2)

Aaron Carlson
Aaron Carlson

Reputation: 5762

A way to do this is to keep track of the payment types that are unknown at development time in the root of your vm in an array. This array can then be shared between your root vm and your payment rows to be used for binding.

In the following code I have an array called allPaymentTypes that is exposed at the root of the vm and each at each payment.

When building up the vm I process each payment by adding each unique payment item to the allPaymentTypes array, create a paymentAmountLook on each payment and add a helper function to find each payment amount for a specific payment type.

var vm = function (payments) {
    var self = this;
    self.payments = payments;
    self.allPaymentTypes = [];

    ko.utils.arrayForEach(payments, processPayment);

    function processPayment(payment) {
        payment.allPaymentTypes = self.allPaymentTypes;
        payment.paymentAmountLookUp = {};

        ko.utils.arrayForEach(payment.items, function (paymentItem) {
            processPaymentItem(payment, paymentItem);
        });

        //Helper function to get the payment amount for a payment type
        //Will handle situations where a specific payment does not have a payment amount
        payment.getPaymentAmount = function (paymentType) {
            return payment.paymentAmountLookUp[paymentType] || '$0';
        };
    }

    function processPaymentItem(payment, paymentItem) {
        payment.paymentAmountLookUp[paymentItem.type] = paymentItem.amount;

        if (self.allPaymentTypes.indexOf(paymentItem.type) === -1) {
            self.allPaymentTypes.push(paymentItem.type);
        }
    }

    return self;
};

In the following html markup I use foreach bindings to the allPaymentTypes array both in the Header and for each table row to dynamically create the th and td elements for each dynamic payment type.

<table class="table">
    <thead>
        <tr>
            <th>Date</th>
            <th>Amount</th>
            <!-- ko foreach: allPaymentTypes -->
            <th data-bind="text: $data"></th>
            <!-- /ko -->
            <th>Status</th>
        </tr>
    </thead>
    <tbody data-bind="foreach: payments">
        <tr>
            <td data-bind="text: date"></td>
            <td data-bind="text: amount"></td>
            <!-- ko foreach: allPaymentTypes -->
            <td data-bind="text: $parent.getPaymentAmount($data)"></td>
            <!-- /ko -->
            <td data-bind="text: status"></td>
        </tr>
    </tbody>
</table>

JSFiddle Demo

JSFiddle Demo with observable properties and collections

FYI, I plagiarized/improved upon Nathan Fisher's post.

Upvotes: 0

Nathan Fisher
Nathan Fisher

Reputation: 7941

Start out by creating a ko.computed for each on the known types on a payment viewmodel object. I

var PaymentType = function (data) {
    var self = this;
    self.type = ko.observable(data.type || '');
    self.amount = ko.observable(data.amount || 0);
    return self;
};

var Payment = function (data) {
    var self = this;
    self.types = ko.observableArray();
    self.amount = ko.observable(data.amoumt || 0);
    self.date = ko.observable(data.date || '');
    self.status = ko.observable(data.status || '');
    self.service = ko.computed(function () {
        var paymentType = ko.utils.arrayFirst(self.types(), function (item) {
            return item.type() === 'Service';
        });
        if (paymentType) {
            return paymentType.amount();
        }
        return 0;
    });
    self.fee = ko.computed(function () {
        var paymentType = ko.utils.arrayFirst(self.types(), function (item) {
            return item.type() === 'Fee';
        });
        if (paymentType) {
            return paymentType.amount();
        }
        return 0;
    });

    ko.utils.arrayForEach(data.items, function (item) {
        self.types.push(new PaymentType(item));
    });
    return self;
};

var vm = function () {
    var self = this;
    self.payments = ko.observableArray();
    ko.utils.arrayForEach(data, function (item) {
        self.payments.push(new Payment(item));
    });
    return self;
};


ko.applyBindings(new vm());

then you can reference each field off the Payment and it should either return an amount or a 0.

JSFiddle Demo

Upvotes: 1

Related Questions