Creating a recursive template in KnockoutJS

Aug 28, 2014 - 3 min read

Sooner or later you will face a problem of rendering flat data as hierarchical representation in your KnockoutJS application. This data can come from the database, or a flat file (.txt, .csv, etc.). Recursive template are here to help! Check out the live sample on jsFiddle.

We have the data presented below:

// ItemModel constructor takes in 3 parameters: ID, Parent ID, Label
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")
];

Note: The values are coming from a table that references itself - Parent ID points to an ID in the same table. In case the Parent ID is null, it is a top level item.

We start with the Knockout template that references itself, going into recursion. The ViewModel is accessed through $root variable, and $data holds the current item being rendered. In order to avoid adding an extra <ul> where there are no sub-items, containerless control flow is used:

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

Main part of the code are the models:

  • ItemModel - a simple value holder
  • RecursiveListViewModel - ViewModel that has a list of ItemModels and a couple of utility functions:
    • subitemsOf - uses Knockout utility function arrayFilter in order to find the items with a specific parent
    • hasSubitems - uses Knockout utility function arrayFirst in order to determine whether an item has at least one child
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);

  // Gets the sub-items of an item, if any
  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;
  };

  // Returns a bool value indicating whether an item has sub-items
  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
  };
}

To kick off the whole sample, bind an unordered list to our recursive template with top level items (null parent id):

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

Voila! The template renders itself - see jsFiddle here.