Unlock the full potential of knockout.js by mastering how to leverage templates recursively

Given the following model and view model for nested categories:

function Category(id, name) {
    var self = this;
    self.Id = ko.observable(id || '00000000-0000-0000-0000-000000000000');
    self.Name = ko.observable(name);
    self.children = ko.observableArray();

    self.addCategory = function () {
        self.children.push(new Category("", ""));
    };

    self.removeCategory = function(category) {
        self.children.remove(category);
    };
}

var CategoryManagerViewModel = function() {
    var self = this;
    self.children = ko.observableArray();

    self.addCategory = function () {
        self.children.push(new Category("", ""));
    };

    self.removeCategory = function (category) {
        self.children.remove(category);
    };

    self.save = function() {
        self.lastSavedJson(JSON.stringify(ko.toJS(self.children), null, 2));
    };

    self.lastSavedJson = ko.observable("");
};

I am trying to figure out how to create a template that can adapt as more child categories are nested within each other. Currently, my template looks like this:

 <table class='categoriesEditor'>
                <tr>
                    <th>Category</th>
                    <th>Children</th>
                </tr>
                <tbody data-bind="foreach: children">
                    <tr>
                        <td>
                            <input data-bind='value: Name' />

                            <div><a href='#' style="color:black" data-bind='click: removeCategory'>Delete</a></div>
                        </td>
                        <td>
                            <table>
                                <tbody data-bind="foreach: children">
                                    <tr>
                                        <td><input data-bind='value: Name' /></td>
                                        <td><a href='#' style="color:black"  data-bind='click: removeCategory'>Delete</a></td>
                                        <td>
                                            <table>
                                                <tbody data-bind="foreach: children">
                                                    <tr>
                                                        <td><input data-bind='value: Name' /></td>
                                                        <td><a href='#' style="color:black" data-bind='click: removeCategory'>Delete</a></td>
                                                    </tr>
                                                </tbody>
                                            </table>
                                            <a href='#' style="color:black" data-bind='click: addCategory'>Add cat</a>
                                        </td>
                                    </tr>
                                </tbody>
                            </table>
                            <a href='#' style="color:black"  data-bind='click: addCategory'>Add cat</a>
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>

        <p>
            <button data-bind='click: addCategory'>Add root category</button>
            <button data-bind='click: save, enable: children().length > 0'>Save to JSON</button>
        </p>

This currently allows for 3 levels of nesting within categories, but what if I need more levels? Do I simply copy and paste the markup? It feels like there should be a better way.

Update:

The binding is set up as follows:

  var viewModel = new CategoryManagerViewModel();
        viewModel.addCategory();
        ko.applyBindings(viewModel);

Answer №1

To handle a situation like this, it would be best to utilize a named template that can be invoked recursively. Here is a simplified setup:

<table>
    <tbody data-bind="template: { name: 'itemTmpl', data: root }"></tbody>
</table>

<script id="itemTmpl" type="text/html">
        <tr>
            <td data-bind="text: name"></td>
            <td>
                <table>
                    <tbody data-bind="template: { name: 'itemTmpl', foreach: children }"></tbody>
                </table>
            </td>   
        </tr>
</script>

By calling the template on a root node, you can then call it on the children as well (which will in turn call it on their respective children, creating a recursive structure).

For a live demonstration, refer to this sample: http://jsfiddle.net/rniemeyer/ex3xt/

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Ways to eliminate duplicate names within an object that contains multiple arrays

I have a set of data with different arrays and I need to remove any duplicate names that are being displayed. How can I accomplish this? Example of duplicate names The data is currently being output like this View all data The dsUserId values are repea ...

Obtaining JSON Data Using WinJS.xhr():

I'm encountering difficulties with retrieving chat messages using winjs.xhr: function getMessage() { var time = MESSAGE_RETURNED.unixtime; if (time == 0) { time= window.parent.SESSION.unixtime; } WinJS.x ...

Unable to create selected buttons in Vue.js

I'm having trouble creating buttons that can select all options when clicking on either size or color. The buttons aren't showing up at all. Can someone help me identify the issue? I've attempted various solutions but none seem to work. Any ...

I noticed that while my shareService is effectively sending values in Angular 2, I encounter an issue where my other components are displaying default values instead

I'm in the process of developing an application using angular 2. I have a UserService that sends a value to other components indicating whether a user is logged in or not, and it's working correctly in terms of data transmission. The issue I&apos ...

Introducing a pause in the function while rendering objects

After inserting setInterval into the code, it is causing all lasers to be delayed by one second. I am looking to have them fired in this order: - initially fire laser1 and laser2. - then take a 1-second break before firing another set of lasers, a ...

Is there a way to eliminate the line that appears during TypeScript compilation of a RequireJS module which reads: Object.defineProperty(exports, "__esModule", { value: true });?

Here is the structure of my tsconfig.json file: { "compileOnSave": true, "compilerOptions": { "module": "amd", "noImplicitAny": false, "removeComments": false, "preserveConstEnums": true, "strictNullChecks": ...

Tips for being patient while waiting for a function to return a value

I'm working on a React class and I'm facing an issue. The problem is that the variable isTokenActive is returning undefined, and I suspect it's because I need to wait for the function checkIfRefreshTokenWorking to return a value. componentD ...

Ways to speed up the initial loading time in Angular 7 while utilizing custom font files

Storing the local font file in the assets/fonts folder, I have utilized 3 different types of fonts (lato, raleway, glyphicons-regular). https://i.stack.imgur.com/1jsJq.png Within my index.html under the "head" tag, I have included the following: <lin ...

Clearing a leaflet layer after a click event: Step-by-step guide

When working with my map, I attempt to toggle the layer's selection using a mouse click. Initially, my map looks like this: Upon clicking a layer, my goal is to highlight and select it: If I click on a previously selected layer again, I want to dese ...

Unable to modify variable to play audio

As I work on constructing my website, I am incorporating various sounds to enhance user experience when interacting with buttons and other elements. However, managing multiple audio files has proven challenging, as overlapping sounds often result in no aud ...

Disabling a specific tab in an array of tabs using Angular and Typescript

Displayed below are 5 tabs that can be clicked by the user. My goal is to disable tabs 2 and 3, meaning that the tab names will still be visible but users will not be able to click on them. I attempted to set the tabs to active: false in the TypeScript fi ...

Using JQuery to Update Text, Link, and Icon in a Bootstrap Button Group

I have a Bootstrap Button group with a split button dropdown. My goal is to change the text, href, and icon on the button when an option is selected from the dropdown. I am able to change the text successfully, but I'm having trouble updating the HREF ...

Issue with NPM identifying my module (demanding unfamiliar module)

Currently, I am developing a React-Native application and organizing my components into separate files. Most of the time, this method works perfectly fine except for one specific instance where I keep encountering a 'Requiring unknown module' err ...

How to execute a system command or external command in Node.js

I am encountering an issue with Node.js. When using Python, I would typically perform an external command execution like this: import subprocess subprocess.call("bower init", shell=True) Although I have explored child_process.exec and spawn in Node.js, I ...

Exploring the power of Jasmine with multiple spy functionalities

I'm currently working on writing unit tests for an Angular application using Jasmine, specifically focusing on testing different scenarios within a function. The main challenge I am facing is structuring the test to accommodate various conditions such ...

Retrieving object key value from an array using Underscore.js

Hey there, I'm facing a challenge where I need to extract the values of wave1 and wave2 from an array using underscore.js. array = [{"id":1,"name":"Monoprix", "pdv":16,"graph":[{"wave1":22,"wave2":11}]} ; I attempted the following: $scope.wave1 = a ...

Ways to troubleshoot the "TypeError: Cannot read property 'value' of null" issue in a ReactJS function

I keep encountering a TypeError: Cannot read property 'value' of null for this function and I'm struggling to pinpoint the source of the issue. Can someone help me figure out how to resolve this problem? By the way, this code is written in R ...

"click on the delete button and then hit the addButton

I have created a website where users can save and delete work hours. I am facing an issue where I can save the hours at the beginning, but once I delete them, I cannot save anything anymore. Additionally, when I reload the page, the deleted data reappears. ...

Collecting information from JSON files

Within the cells of my calendar are placeholders for events, dates, participants, and descriptions. Now, I am looking to populate these placeholders with data previously stored using localStorage. Unfortunately, my current code is not achieving this. How ...

What is the method for storing API status JSON in the state?

Can anyone help me figure out why the json in the register state is returning an empty object after console.log(register); from that state? import React, { useEffect, useState } from "react"; import { Formik } from "formik"; // * icons ...