45

I want to use something similar to the Knockout foreach construct to iterate over the properties of an object. Here is what I am trying to create...

DESIRED RESULT

<table>
    <tr>
        <td>Name 1</td>
        <td>8/5/2012</td>
    </tr>
    <tr>
        <td>Name 2</td>
        <td>2/8/2013</td>
    </tr>
</table>

However, my model looks like this...

JS

function DataModel(){
    this.data = ko.observableArray([{
                        entityId: 1,
                        props: {
                            name: 'Name 1',
                            lastLogin: '8/5/2012'
                        }
                    },
                    {
                        entityId: 2,
                        props: {
                            name: 'Name 2',
                            lastLogin: '2/8/2013'
                        }
                    }]);
}

var dataModel = new DataModel();
ko.applyBindings(dataModel);

Each row has an entityId and props which is an object itself. This template doesn't work, but how would I change it to generate the desired table above?

EDIT: The props in this example are name and lastLogin, but I need a solution that is agnostic to what is contained inside props.

I have this FIDDLE going as well.

HTML

<div data-bind="template: { name: 'template', data: $data }"></div>

<script type="text/html" id="template">
    <table>
        <tr data-bind="foreach: data()">
            <td data-bind="text: entityId"></td>  
        </tr>
    </table> 
</script>
John Livermore
  • 30,235
  • 44
  • 126
  • 216

8 Answers8

66

In a modern browser (or with an appropriate polyfill) you can iterate over Object.keys(obj) (the method returns only own enumerable properties, meaning that there is no need for an additional hasOwnProperty check):

<table>
  <tbody data-bind="foreach: {data: data, as: '_data'}">
    <tr data-bind="foreach: {data: Object.keys(props), as: '_propkey'}">
      <th data-bind="text: _propkey"></th>
      <td data-bind="text: _data.props[_propkey]"></td>
    </tr>
  </tbody>
</table>

Fiddled.

NB: I was simply curious to see if this would work, the template body above is more polluted than what I'd like to use in production (or come back to a few months later and be like "wtf").

Custom binding would be a better option, my personal preference though would be to use a computed observable or a writeable computed observable (the latter would be handy when working with json responses a-la restful api).

Oleg
  • 24,465
  • 8
  • 61
  • 91
  • This answer is banging. Just ran into this and thought I would check what SO had to say. Custom Bindings for a specific purpose are not as valid for production large scale sites as a simple inline solutions such as this. – MattSizzle Nov 23 '14 at 04:57
  • I wish that I could upvote this multiple times. I like this for the reason that I don't have to create a bindinghandler. – valdetero Jan 02 '15 at 19:41
  • That's the best solution! I don't understand why it's not the first one. – jbartolome Jan 29 '15 at 21:49
  • Slightly different approach where a template is not needed - leveraging the $parent object: http://jsfiddle.net/skrile/87aywb93/ – skrile Sep 29 '15 at 12:29
46

You could always create a binding handler to handle the transformation.

ko.bindingHandlers.foreachprop = {
  transformObject: function (obj) {
    var properties = [];
    ko.utils.objectForEach(obj, function (key, value) {
      properties.push({ key: key, value: value });
    });
    return properties;
  },
  init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
    var properties = ko.pureComputed(function () {
      var obj = ko.utils.unwrapObservable(valueAccessor());
      return ko.bindingHandlers.foreachprop.transformObject(obj);
    });
    ko.applyBindingsToNode(element, { foreach: properties }, bindingContext);
    return { controlsDescendantBindings: true };
  }
};

Then apply it:

<div data-bind="template: { name: 'template', data: $data }"></div>

<script type="text/html" id="template">
    <table>
        <tbody data-bind="foreach: data">
            <tr data-bind="foreachprop: props">
                <td data-bind="text: value"></td>
            </tr>
        </tbody>
    </table> 
</script>
Jeff Mercado
  • 129,526
  • 32
  • 251
  • 272
  • Value comes back as an object, why can't it just be a string like key? –  May 21 '13 at 15:25
  • Just wondering whether it is possible with this foreachprop bind to use `$parent.` binding context? – Thewads Jan 09 '14 at 08:56
  • @Thewads: of course. You can use it with any plain object that has properties. Though I'm not sure I understand what you're referring to. – Jeff Mercado Jan 23 '14 at 02:10
  • @JeffMercado can you see my question, I think I have the same issue: http://stackoverflow.com/questions/21558075/how-do-i-update-my-model-using-ajax-and-mapping-plugin – Vyache Feb 04 '14 at 17:48
  • 1
    One more thing to add. In order to access the context from within the template the `bindingContext` has to be sent as an parameter: `ko.applyBindingsToNode(element, { foreach: properties }, bindingContext);` – Razvan Mar 18 '14 at 12:46
  • @Thewads: See my version of the answer below – Jitters Jun 18 '14 at 22:01
  • @Thewads: Sorry, after a couple of months had gone by, I finally understood what you were asking. At the time of writing this, I didn't realize that the binding context of the parent was not preserved or that `ko.applyBindingsToNode()` accepted a third parameter. Passing the context there should propagate the context to the children. – Jeff Mercado Sep 17 '14 at 01:16
  • Awesome! Is there any way of displaying the property name along with the value? I can print the value--I'm just having a hard time getting the property name out. Thanks and may God bless you with eternal life through Jesus Christ. – Brandon Jul 07 '15 at 15:31
  • 1
    @Brandon: The name can be accessed through the property `key`. – Jeff Mercado Jul 07 '15 at 15:33
  • too complicated to perform something so rudimentary and simple. check the below answer that uses `#Object.keys` – Calvintwr Apr 23 '16 at 16:04
21

This is a modification of Jeff's answer, with the binding context preserved

ko.bindingHandlers.eachProp = {
    transformObject: function (obj) {
        var properties = [];
        for (var key in obj) {
            if (obj.hasOwnProperty(key)) {
                properties.push({ key: key, value: obj[key] });
            }
        }
        return ko.observableArray(properties);
    },
    init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var value = ko.utils.unwrapObservable(valueAccessor()),
            properties = ko.bindingHandlers.eachProp.transformObject(value);

        ko.bindingHandlers['foreach'].init(element, properties, allBindingsAccessor, viewModel, bindingContext)
        return { controlsDescendantBindings: true };
    },
    update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var value = ko.utils.unwrapObservable(valueAccessor()),
            properties = ko.bindingHandlers.eachProp.transformObject(value);

        ko.bindingHandlers['foreach'].update(element, properties, allBindingsAccessor, viewModel, bindingContext)
        return { controlsDescendantBindings: true };
    }
};

Now apply with parent and root:

<table>
    <tbody data-bind="foreach: data">
        <tr data-bind="eachProp: props">
            <td data-bind="text: value, click: $root.doSomething"></td>
        </tr>
    </tbody>
</table> 
Jitters
  • 326
  • 3
  • 5
  • 2
    This answer is better than accepted IMO as it includes the update function.. meaning it will behave like an observable not just a one time iteration. – Steve Cadwallader Sep 04 '14 at 17:31
  • @SteveCadwallader: it doesn't make it better that there's an explicit `update` function, it's calling the update function on the `foreach` handler directly. In other words, it's manually propagating the update calls. The other answer applied the foreach handler to the element so everything that `foreach` does will happen (including updates). The explicit update function is just not needed. – Jeff Mercado Apr 21 '17 at 00:59
7

Simplified answer to work with any basic object, worked for me:

<!-- ko foreach: {data: Object.keys(myObj)} -->
    <span data-bind="text: $data"></span> 
    <span data-bind="text: $parent.myObj[$data]"></span>
<!-- /ko -->
Andrew
  • 18,680
  • 13
  • 103
  • 118
4

I am a bit late, But I think this should work, a simple solution without using any template.

var json = [
 {
  "PortfolioCompanyId":240,
  "dt":"2018-12-31 00:00:00.0",
  "ValuationDate":"2017-09-30 00:00:00.0",
  "capitalexpenditure":-5555660.0,
  "workingcapitalchange":-812350.0
 },
 {
  "PortfolioCompanyId":240,
  "dt":"2019-12-31 00:00:00.0",
  "ValuationDate":"2017-09-30 00:00:00.0",
  "capitalexpenditure":-5613520.0,
  "workingcapitalchange":-893530.0
 },
 {
  "PortfolioCompanyId":240,
  "dt":"2020-12-31 00:00:00.0",
  "ValuationDate":"2017-09-30 00:00:00.0",
  "capitalexpenditure":-5674130.0,
  "workingcapitalchange":-982850.0
 },
 {
  "PortfolioCompanyId":240,
  "dt":"2021-12-31 00:00:00.0",
  "ValuationDate":"2017-09-30 00:00:00.0",
  "capitalexpenditure":-6241543.0,
  "workingcapitalchange":-1081135.0
 },
 {
  "PortfolioCompanyId":240,
  "dt":"2022-12-31 00:00:00.0",
  "ValuationDate":"2017-09-30 00:00:00.0",
  "capitalexpenditure":-6865697.3,
  "workingcapitalchange":-1189248.5
 }
];

var DataModel = function () {
            this.jsonArray = ko.observable(json);
        };
ko.applyBindings(new DataModel());
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<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.4.2/knockout-min.js"></script>
<table class="table" data-bind="foreach:jsonArray">
       <tr data-bind="foreach:Object.keys($data)"> <!-- JSON Object -->
         <td data-bind="text : $parent[$data]"></td>
       </tr>
    </table>
    
    
Sufiyan Ansari
  • 1,780
  • 20
  • 22
3
<table>
    <tr data-bind="foreach: {data: data, as: 'item'}">
        <td data-bind="foreach: { data: Object.keys(item), as: 'key' }">
            <b data-bind="text: item[key]"></b>
        </td>  
    </tr>
</table>

function DataModel(){
this.data = ko.observableArray([{
                    entityId: 1,
                    props: {
                        name: 'Name 1',
                        lastLogin: '8/5/2012'
                    }
                },
                {
                    entityId: 2,
                    props: {
                        name: 'Name 2',
                        lastLogin: '2/8/2013'
                    }
                }]);
}

var dataModel = new DataModel();
ko.applyBindings(dataModel);

Hope that's helpful (pardon the brevity)

appendix:

Here's a working example which has been testing...

<table class="table table-hover">
    <thead>
        <tr>
            <!-- ko foreach: gridOptions.columnDefs -->
            <th data-bind="text: displayName"></th>
            <!-- /ko -->
        </tr>
    </thead>
    <tbody>
        <!-- ko foreach: {data: gridOptions.data, as: 'item'} -->
        <tr>
            <!-- ko foreach: {data: Object.keys(item), as: 'key'} -->
            <td>
                <span data-bind="text: item[key]"></span>
            </td>
            <!-- /ko -->
        </tr>
        <!-- /ko -->
    </tbody>
</table>
Cody
  • 9,785
  • 4
  • 61
  • 46
2

Supposedly, there is a deeper problem (see this thread at Google groups) that is that foreach treats the object as a dictionary of parameters, not as the collection to iterate.

My best solution so far is to combined foreach in Object.keys(myobject) and 'with' binding context.

Mohamad Shiralizadeh
  • 8,329
  • 6
  • 58
  • 93
0

(not strictly iterating over the properties, but does create the table above)

<div data-bind="template: { name: 'template', data: $data }"></div>

<script type="text/html" id="template">
    <table data-bind="foreach: data()">
        <tr>
            <td data-bind="text: props.name"></td>  
            <td data-bind="text: props.lastLogin"></td>  
        </tr>
    </table>
</script>

updated: http://jsfiddle.net/cwnEE/7/

paul
  • 21,653
  • 1
  • 53
  • 54
  • 2
    True, but I need it to iterate over the properties as the names in 'props' will be different for different tables. I edited the question to clarify this. – John Livermore Feb 12 '13 at 17:31