Recursive template in KnockoutJS

by headsigned

HTML

<ul data-bind="template: { name: 'item-template', foreach: $root.subitemsOf(null) }"></ul>

<script type="text/html" id="item-template">
    <li>
        <span data-bind="text: label"></span>
        
        <!-- ko if: $root.hasSubitems($data) -->
            <ul data-bind="template: {name: 'item-template', foreach: $root.subitemsOf($data)}"></ul>
        <!-- /ko -->
    </li>
</script>

JavaScript

function ItemModel(id, parent_id, label) {
    var self = this;

    self.id = ko.observable(id);
    self.parentId = ko.observable(parent_id);
    self.label = ko.observable(label);
}

function RecursiveListViewModel(tasks) {
    var self = this;

    self.items = ko.observableArray(tasks);

    self.subitemsOf = function (item) {
        var children = ko.utils.arrayFilter(self.items(), function (arrayItem) {
            var parentItemId = (null === item) ? null : item.id();
            return arrayItem.parentId() == parentItemId;
        });

        return children;
    };

    self.hasSubitems = function (item) {
        var firstMatch = ko.utils.arrayFirst(self.items(), function (arrayItem) {
            return (arrayItem.parentId() == item.id());
        });

        return (null !== firstMatch); // At least one item found in array
    };
}

var items = [
    new ItemModel(0, null, "Item 1"),
    new ItemModel(1, null, "Item 2"),
    new ItemModel(2, 0, "Item 1-1"),
    new ItemModel(3, 0, "Item 1-2"),
    new ItemModel(4, 3, "Item 1-2-1"),
    new ItemModel(5, 1, "Item 2-1"),
    new ItemModel(6, 1, "Item 2-2"),
];

ko.applyBindings(new RecursiveListViewModel(items));