Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to render tr conditionally in knockout.js foreach binding

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.

like image 496
0lukasz0 Avatar asked Dec 17 '12 22:12

0lukasz0


2 Answers

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);
});
like image 126
Rustam Avatar answered Nov 14 '22 22:11

Rustam


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.

like image 27
Artem Avatar answered Nov 14 '22 20:11

Artem