6

I would like to render dynamically rows and columns using knockout. The idea is that I would like to populate each row with some cells and dynamically add more rows if needed. lets assume that totall number of cells equals 4*number of rows, then I tried:

<table>
    <tbody data-bind="foreach: model">
        <!--ko if: $index() % 4 == 0--><tr><!--/ko-->
         <td>
              <label data-bind="text: Value"></label>
         </td>
         <td>
              <input type="checkbox" data-bind="checked: IsChecked"/>
         </td>
         <!--ko if: $index() % 4 == 0--></tr><!--/ko-->
     </tbody>
 </table>

but it works like it was:

<table>
    <tbody data-bind="foreach: model">
        <!--ko if: $index() % 4 == 0-->
         <td>
              <label data-bind="text: Value"></label>
         </td>
         <td>
              <input type="checkbox" data-bind="checked: IsChecked"/>
         </td>
         </tr><!--/ko-->
     </tbody>
 </table>

by not rendering whole row with content, is it possible with knockout to render all cells and add rows only when needed?

As a workaround I thinking about nested foreach, but it would require my model to change from single dimensional to two dimensional which seems odd.

0lukasz0
  • 3,155
  • 1
  • 24
  • 40
  • 2
    Are you sure you want to use an HTML table for this? If you have a flat list of items as your data could you not just render them as divs and use CSS to have them flow naturally inside a container, 4 across? – Tom W Hall Dec 17 '12 at 23:45
  • Yeah, it's tabular data and tables are for displaying tabular data using div would be workaround not a solution – 0lukasz0 Dec 18 '12 at 13:30

2 Answers2

14

Add another computed property that structures your data into rows:

<table>
    <tbody data-bind="foreach: rows">
        <tr>
            <!-- ko foreach: $data -->
            <td data-bind="text:$index"></td>
            <td data-bind="text:fname"></td>
            <td data-bind="text:lname"></td>
            <!-- /ko -->
        </tr>
    </tbody>
</table>

with code:

var vm = {

    people: ko.observableArray([
        { fname: 'fname', lname: 'lname' },
        { fname: 'fname', lname: 'lname' },
        { fname: 'fname', lname: 'lname' },
        { fname: 'fname', lname: 'lname' }
    ])
};

vm.rows = ko.computed(function () {

    var itemsPerRow = 3, rowIndex = 0, rows = [];

    var people = vm.people();
    for (var index = 0; index < people.length; index++) {
        if (!rows[rowIndex])
            rows[rowIndex] = [];

        rows[rowIndex].push(people[index]);

        if (rows[rowIndex].length == itemsPerRow)
            rowIndex++;
    }

    return rows;
}, vm);

$(function () {
    ko.applyBindings(vm);
});
Rustam
  • 746
  • 3
  • 10
3

Your syntax will not work with knockout default templating engine just because it uses DOM. If you need to do this, use string-based external templating engine (it will treat your template as string and will use regex and string manipulations, so you will be able to do this trick with conditional rendering of start/end tag). Your example using underscore js:

http://jsfiddle.net/2QKd3/5/

HTML

<h1>Table breaking</h1>
<ul data-bind="template: { name: 'peopleList' }"></ul>

<script type="text/html" id="peopleList">
    <table>
    <tbody>
    {{ _.each(model(), function(m, idx) { }}
        {{ if (idx % 4 == 0) { }}
            <tr>
        {{ } }}
         <td>

              <label>{{= m.Value }}</label>
         </td>
         <td>
             <input type="checkbox" data-bind="checked: m.IsChecked"/>
         </td>
        {{ if (idx % 4 == 3) { }}
        </tr>
        {{ } }}
    {{ }) }}
            </tbody>
            </table>
</script>

Javascript (this includes underscore integration decribed here - http://knockoutjs.com/documentation/template-binding.html

_.templateSettings = {
    interpolate: /\{\{\=(.+?)\}\}/g,
    evaluate: /\{\{(.+?)\}\}/g
};

/* ---- Begin integration of Underscore template engine with Knockout. Could go in a separate file of course. ---- */
    ko.underscoreTemplateEngine = function () { }
    ko.underscoreTemplateEngine.prototype = ko.utils.extend(new ko.templateEngine(), {
        renderTemplateSource: function (templateSource, bindingContext, options) {
            // Precompile and cache the templates for efficiency
            var precompiled = templateSource['data']('precompiled');
            if (!precompiled) {
                precompiled = _.template("{{ with($data) { }} " + templateSource.text() + " {{ } }}");
                templateSource['data']('precompiled', precompiled);
            }
            // Run the template and parse its output into an array of DOM elements
            var renderedMarkup = precompiled(bindingContext).replace(/\s+/g, " ");
            return ko.utils.parseHtmlFragment(renderedMarkup);
        },
        createJavaScriptEvaluatorBlock: function(script) {
            return "{{ " + script + " }}";
        }
    });
    ko.setTemplateEngine(new ko.underscoreTemplateEngine());
/* ---- End integration of Underscore template engine with Knockout ---- */

var viewModel = {
    model: ko.observableArray([
        { Value: '1', IsChecked: 1 },
        { Value: '2', IsChecked: 0 },
        { Value: '3', IsChecked: 1 },
        { Value: '4', IsChecked: 0 },
        { Value: '5', IsChecked: 1 },
    ])        
};

ko.applyBindings(viewModel);

P.S.: but better avoid using tables for html layout. Your example can be rendered using inline-block elements with much cleaner code.

Community
  • 1
  • 1
Artem
  • 3,700
  • 1
  • 27
  • 35