1
votes

I have a question about Knockout foreach binding to a table.

I have an array of columnNames and an array of items.

The items array has a property called ColumnName. How would I use knockout foreach to bind self.items to self.columnNames?

My view model looks like this:

var VM = function () {
    var self = this;
    self.items = ko.observableArray();
    self.columnNames = [ "Name", "Age", "Job"];
};
var vm = new VM();

ko.applyBindings(vm);

vm.items.push([
    {
        'ColumnName': 'Name',
        'Value': 'John'
    },
    {          
        'ColumnName': 'Age',
        'Value': 25'
    }
]);

vm.items.push([
    {
        'ColumnName': 'Name',
        'Value': 'Jane'
    },
    {          
        'ColumnName': 'Age',
        'Value': 26
    },
    {          
        'ColumnName': 'Job',
        'Value': 'developer'
    }

]);

In my view, I'm guessing I need something like this,

<table>
    <thead>
        <tr data-bind="foreach: columnNames">
            <th> <span data-bind="text: $data"></span>

            </th>
        </tr>
    </thead>
    <tbody data-bind="foreach: items">
        <tr data-bind="foreach: $parent.columnNames">
            <!-- how do I put the Value property here?! -->
        </tr>
    </tbody>
</table>

I started a jsfiddle here, http://jsfiddle.net/83zcwue7/1/

-update- I'm trying to render something like this:

Name    Age     Job
John    25
Jane    26      Developer

-- update 2 -- I created an ugly (but working) solution.

http://jsfiddle.net/83zcwue7/7/

I added this method call. It maps the ColumnName as the property name and Value its value, but it feels like I completely missed a simpler solution:

    self.fixedItems = ko.observableArray();   //I use this in foreach

    self.fixData = function(){
        $.each(self.items(), function(i, rows){
            var cell = new Object();
            $.each(rows, function(j, prop){
                cell[prop.ColumnName] = prop.Value;

            });
            self.fixedItems.push( cell );
        });
   }; 

Original data looked like this:

[[{ 'ColumnName': 'Name', 'Value': 'John' }, { 'ColumnName': 'Age', 'Value': 25 }], [{ 'ColumnName': 'Name', 'Value': 'Jane' }, { 'ColumnName': 'Age', 'Value': 22 }, { 'ColumnName': 'Job', 'Value': 'developer' }]]

The result (self.fixedItems) looks like this:

[{ Age: 25 Name: "John" }, { Age: 26, Job: "developer", Name: "Jane" }]

Which 'fixes' the data, so it can be used easily in the foreach. It feels like I missed an elegant solution.

3
humm couple of mistakes check this jsfiddle.net/83zcwue7/2 . hope it helpssuper cool
Thank you. Unfortunately, I get a new array per row, each array element holds a single column of data.Timmy Elliot
wow, that's a very difficult model to work with. How do you know that the object containing column name "job" with value "developer" is related to the object containing the column name "name" with the value "jane"?Anish Patel
yeah, it a legacy solution allowing for user-defined columns. I know the object containing column name "job" with value "developer" is related to the object containing the column name "name" with the value "jane", because they are in the same array: . [{ 'ColumnName': 'Name', 'Value': 'Jane' }, { 'ColumnName': 'Age', 'Value': 22 }, { 'ColumnName': 'Job', 'Value': 'developer' }]Timmy Elliot
IMHO there isn't simpler solution for this your fix look like the way to go as there is a chance data coming in (dynamic) you can use subscribe check this jsfiddle.net/83zcwue7/8 . good lucksuper cool

3 Answers

1
votes

The name of the items' properties must be the same of the columnNames. Then you can access the property of the items. Like this:

var VM = function () {
    var self = this;
    self.items = ko.observableArray();
    self.columnNames = [ "Name", "Age", "Job"];
};
var vm = new VM();

ko.applyBindings(vm);

vm.items([
    {
        'Name': 'Jonn',
        'Age': '10',
        'Job': 'Developer'
    },
    {
        'Name': 'Jonn',
        'Age': '25',
        'Job': 'Developer'
    }
    ]
);

HTML

<table>
    <thead>
        <tr data-bind="foreach: columnNames">
            <th> <span data-bind="text: $data"></span>

            </th>
        </tr>
    </thead>
    <tbody data-bind="foreach: items">
        <tr data-bind="foreach: $root.columnNames">
            <td data-bind="text: $parent[$data]"></td>
        </tr>
    </tbody>
</table>

http://jsfiddle.net/83zcwue7/3/

Edit

A clear solution using only knockout functions and computed observables:

var VM = function () {
    var self = this;
    self.items = ko.observableArray();
    self.columnNames = [ "Name", "Age", "Job"];

    self.fixedItems = ko.computed(function () {
        return ko.utils.arrayMap(self.items(), function (item) {
            var fixedItem = {};
            ko.utils.arrayForEach(item, function(column) {
                fixedItem[column.ColumnName] = column.Value;
            });
            return fixedItem;
        });
    });
};
var vm = new VM();

ko.applyBindings(vm);

vm.items.push([
    {
        'ColumnName': 'Name',
        'Value': 'John'
    },
    {          
        'ColumnName': 'Age',
        'Value': '25'
    }
]);

vm.items.push([
    {
        'ColumnName': 'Name',
        'Value': 'Jane'
    },
    {          
        'ColumnName': 'Age',
        'Value': 26
    },
    {          
        'ColumnName': 'Job',
        'Value': 'developer'
    }

]);

http://jsfiddle.net/83zcwue7/9/

It is cleaner, isn't it?

1
votes

This solution uses a function from underscorejs to flatten each array of dictionaries into an object to make things a little easier.

var VM = function() {
  var self = this;
  self.items = ko.observableArray([]);
  self.columnNames = ["Name", "Age", "Job"];
};
var vm = new VM();

ko.applyBindings(vm);


var reduce = function(arr){
  return _.reduce(arr, function(o, n){ o[n.ColumnName] = n.Value;return o;  }, {});
  }

vm.items.push(reduce([{
        'ColumnName': 'Name',
        'Value': 'John'
      }, {
        'ColumnName': 'Age',
        'Value': 25
    }
]));

vm.items.push(reduce([
    {
        'ColumnName': 'Name',
        'Value': 'Jane'
    },
    {          
        'ColumnName': 'Age',
        'Value': 26
    },
    {          
        'ColumnName': 'Job',
        'Value': 'developer'
    }

]));
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<table>
    <thead>
        <tr data-bind="foreach: columnNames">
            <th> <span data-bind="text: $data"></span>

            </th>
        </tr>
    </thead>
    <tbody data-bind="foreach: items">
        <tr data-bind="foreach: $parent.columnNames">
            <td data-bind="text: !!$parents[0][$data] ? $parents[0][$data] : '&nbsp;'"></td>
        </tr>
    </tbody>
</table>
0
votes

I had an array of columns that look like this:

[ "Name", "Age", "Job"]

And data that looks like this:

[[{ 'ColumnName': 'Name', 'Value': 'John' }, { 'ColumnName': 'Age', 'Value': 25 }], [{ 'ColumnName': 'Name', 'Value': 'Jane' }, { 'ColumnName': 'Age', 'Value': 22 }, { 'ColumnName': 'Job', 'Value': 'developer' }]]

My problem was to match the data with the columns using a foreach binding.

My solution, was to write a function that mapped the ColumnName as the property name and the Value as its value. The full method I used is in the jsfiddle link (below), but basically, I did this,

cell[prop.ColumnName] = prop.Value;

Which gave me a new observableArray with cleaner data that I could use in my foreach table:

[{ Age: 25 Name: "John" }, { Age: 26, Job: "developer", Name: "Jane" }]

The actual solution is here: http://jsfiddle.net/83zcwue7/7/

I don't feel like I've found a clean solution, but it does work.