如何使用knockout遍历一个对象(而不是数组)

45

我希望能够使用类似Knockout foreach结构的东西来迭代对象的属性。以下是我想要创建的内容...

期望结果

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

然而,我的模型长这样...
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);

每一行都有一个实体ID和属性,这些属性本身也是一个对象。这个模板不起作用,但我该如何更改它以生成上面所需的表格?
编辑:在这个例子中,props是name和lastLogin,但我需要一个对props内部内容不加区分的解决方案。
我也有一个FIDDLE
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>

道具的数量是始终为2还是可以互不相同? - Loïc Faure-Lacroix
8个回答

66
在现代浏览器中(或使用适当的polyfill),您可以迭代 Object.keys(obj)(该方法仅返回自身可枚举属性,因此无需进行额外的hasOwnProperty检查):

在现代浏览器中(或使用适当的polyfill),您可以迭代Object.keys(obj)(该方法仅返回自身可枚举属性,因此不需要额外的hasOwnProperty检查):

<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:我只是好奇这是否有效。上面的模板主体比我想在生产中使用的要污染得多(或者几个月后回来会像“ wtf”一样)。

自定义绑定将是更好的选择,但我的个人偏好是使用计算观察器或可写计算观察器(后者在使用restful api的json响应时非常方便)。


这个答案很棒。刚看到它,想来看看SO上有什么说法。为特定目的创建自定义绑定对于大规模生产网站而言并不像简单的内联解决方案那样有效。 - MattSizzle
我希望我能够多次点赞这个。我喜欢它的原因是我不必创建一个绑定处理程序。 - valdetero
这是最好的解决方案!我不明白为什么它不是第一个。 - jbartolome
稍微不同的方法,无需模板-利用$parent对象:http://jsfiddle.net/skrile/87aywb93/ - skrile

46

你可以随时创建一个绑定处理程序来处理转换。

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 };
  }
};

然后应用它:

<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>

@Thewads:当然可以。您可以将其与具有属性的任何普通对象一起使用。不过我不确定您指的是什么。 - Jeff Mercado
1
还有一件事要补充。为了从模板内部访问上下文,bindingContext必须作为参数发送:ko.applyBindingsToNode(element, { foreach: properties }, bindingContext); - Razvan
@Thewads:抱歉,几个月过去了,我终于明白你在问什么。在我写这篇文章的时候,我没有意识到父级的绑定上下文没有被保留,也没有意识到 ko.applyBindingsToNode() 接受第三个参数。在那里传递上下文应该会将上下文传播到子级。 - Jeff Mercado
太棒了!有没有办法显示属性名称以及其值?我可以打印出来值 - 我只是难以得到属性名称。感谢并愿上帝通过耶稣基督赐予你永恒的生命。 - Brandon
1
@Brandon:可以通过属性 key 访问该名称。 - Jeff Mercado
显示剩余5条评论

21

这是对Jeff答案的修改,保留了绑定上下文

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 };
    }
};
现在使用 parent 和 root 进行应用:
<table>
    <tbody data-bind="foreach: data">
        <tr data-bind="eachProp: props">
            <td data-bind="text: value, click: $root.doSomething"></td>
        </tr>
    </tbody>
</table> 

2
在我看来,这个答案比被接受的答案更好,因为它包括更新函数...这意味着它将表现得像一个可观察对象,而不仅仅是一次迭代。 - Steve Cadwallader
@SteveCadwallader:有一个显式的“update”函数并不会使它变得更好,因为它直接在“foreach”处理程序上调用了更新函数。换句话说,它手动传播更新调用。另一个答案将“foreach”处理程序应用于元素,因此“foreach”执行的所有操作都会发生(包括更新)。显式的更新函数是不必要的。 - Jeff Mercado

7

以下是适用于任何基本对象的简化答案,对我很有用:

<!-- ko foreach: {data: Object.keys(myObj)} -->
    <span data-bind="text: $data"></span> 
    <span data-bind="text: $parent.myObj[$data]"></span>
<!-- /ko -->

有用、简单和快速。 - Alesis Joan

4
我有点晚了,但我认为这个方法应该可行,它是一个简单的解决方案,不使用任何模板。

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>
    
    


这是对原问题唯一直接的答案,而且完美地运作。 - Andresa Martins

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);

希望这对您有所帮助(请原谅简短)。
附录:
以下是一个已经经过测试的可行示例...
<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>

2
据说,存在一个更深层次的问题(请参见Google groups上的此线程),即foreach将对象视为参数字典而不是要迭代的集合。
目前我最好的解决方案是将foreachObject.keys(myobject)和'with'绑定上下文相结合。

0

(虽然不是严格迭代属性,但确实创建了上面的表)

<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>

更新:http://jsfiddle.net/cwnEE/7/


2
是的,但我需要它遍历属性,因为“props”中的名称对于不同的表是不同的。我编辑了问题以澄清这一点。 - John Livermore

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接