Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Knockout recursive template selective binding

Tags:

knockout.js

In this example, I've got a nested template to display tree-view like indented output, but the rendered HTML on leaf nodes is not what I would expect. How do I get the leaf nodes to render the expected html and NOT contain a child container div?

If I put the if binding outside the template binding, I get a javascript error:

Uncaught Error: Multiple bindings (if and template) are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.

Note: Using an if binding comment <!-- ko if: children().length > 0 --> does work to remove the unwanted element, but the resulting html is riddled with the comments and I'd prefer to keep it clean as I could have potentially hundreds of leaf nodes.

I've tried using 2 templates, one with the container and one without, and putting in a condition for the name, but the template always rendered the nodeTempl: data-bind="template: { name: (children().length > 0) ? 'nodeTempl' : 'leafTempl', foreach: children }"

I guess I really want to apply a different template based on the state of the child it's currently processing.

A leaf node is rendering like this

<div>
    <div data-bind="text: name">Node 1-1-1</div>            
    <div class="indent-1" data-bind="template: {if: children().length &gt; 0,  name: 'nodeTempl', foreach: children }"></div>
</div>

But I want it to render like this:

<div>
    <div data-bind="text: name">Node 1-1-1</div>            
</div>

Here's the setup:

HTML

<style type="text/css">
    .indent-1 {
        margin-left: 20px;
    }
</style>

<script id="nodeTempl" type="text/html">
    <div>
        <div data-bind="text: name"></div>            
        <div class="indent-1" data-bind="template: {if: children().length > 0,  name: 'nodeTempl', foreach: children }"></div>
    </div>
</script>

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

Script

var node = function (config) {
    var self = this;
    self.name = ko.observable(config.name);
    self.children = ko.observableArray([]);
    if ($.isArray(config.children)) {
        for (var i = 0; i < config.children.length; i++) {
            self.children.push(new node(config.children[i]));
        }
    }
};

ko.applyBindings(new node(
    {
        name: 'root',
        children: [
            { name: 'Node1', children: [{name: 'Node 1-1', children: [{name: 'Node 1-1-1'}]},{name: 'Node 1-2'}]},
            {name: 'Node2',children: [{name: 'Node 2-1'},{name: 'Node 2-2'}]},
            {name: 'Node3'},
        ]
    }
    ));

Output

<div data-bind="template: { name: 'nodeTempl', foreach: children }">
    <div>
        <div data-bind="text: name">Node1</div>            
        <div class="indent-1" data-bind="template: {if: children().length &gt; 0,  name: 'nodeTempl', foreach: children }">
    <div>
        <div data-bind="text: name">Node 1-1</div>            
        <div class="indent-1" data-bind="template: {if: children().length &gt; 0,  name: 'nodeTempl', foreach: children }">
    <div>
        <div data-bind="text: name">Node 1-1-1</div>            
        <div class="indent-1" data-bind="template: {if: children().length &gt; 0,  name: 'nodeTempl', foreach: children }"></div>
    </div>
</div>
    </div>

    <div>
        <div data-bind="text: name">Node 1-2</div>            
        <div class="indent-1" data-bind="template: {if: children().length &gt; 0,  name: 'nodeTempl', foreach: children }"></div>
    </div>
</div>
    </div>

    <div>
        <div data-bind="text: name">Node2</div>            
        <div class="indent-1" data-bind="template: {if: children().length &gt; 0,  name: 'nodeTempl', foreach: children }">
    <div>
        <div data-bind="text: name">Node 2-1</div>            
        <div class="indent-1" data-bind="template: {if: children().length &gt; 0,  name: 'nodeTempl', foreach: children }"></div>
    </div>

    <div>
        <div data-bind="text: name">Node 2-2</div>            
        <div class="indent-1" data-bind="template: {if: children().length &gt; 0,  name: 'nodeTempl', foreach: children }"></div>
    </div>
</div>
    </div>

    <div>
        <div data-bind="text: name">Node3</div>            
        <div class="indent-1" data-bind="template: {if: children().length &gt; 0,  name: 'nodeTempl', foreach: children }"></div>
    </div>
</div>
like image 502
Lee Greco Avatar asked Mar 24 '23 06:03

Lee Greco


1 Answers

You're on the right track suggesting that you need two templates. The template name parameter can be a function that returns the template name for each item in the array.

You can create a function like this:

function nodeTemplate(node) {
    return node.children().length > 0 ? 'nodeTempl' : 'nodeLeafTempl';
}

And change your view to this:

<script id="nodeTempl" type="text/html">
    <div>
        <div data-bind="text: name"></div>            
        <div class="indent-1" data-bind="template: {name: nodeTemplate, foreach: children }"></div>
    </div>
</script>

<script id="nodeLeafTempl" type="text/html">
    <div>
        <div data-bind="text: name"></div>            
    </div>
</script>

<div data-bind="template: { name: nodeTemplate, foreach: children }"></div> 
like image 174
Michael Best Avatar answered Mar 28 '23 06:03

Michael Best