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

Node.js module loader compared to client-side AMD loader such as RequireJS

Today, let's talk about loading Javascript modules on the client-side. There are two popular ways to do this: Using RequireJS Utilizing NPM (Node Package Manager) for exporting and requiring files I've always found the first option to work wel ...

Expanding and collapsing DIV elements using JavaScript upon clicking navigation menu items

At present, the current code unfolds the DIVs whenever a user clicks on a menu item. This results in the DIV folding and unfolding the same number of times if clicked repeatedly on the same link, without staying closed. My desired functionality is to have ...

Implement a new list field to an object using javascript

I am facing a challenge in converting a JSON object to a list of JSON objects and then adding it back to JSON. Here is an example: config = { existing_value: 'sample' } addToListing = (field, value, index=0) => { config = { ...confi ...

Combining Rails 4 with coffeescript while encountering an issue with the jQuery sortable detatch functionality

Currently, I am using Ruby on Rails 4 along with the jquery-ui-rails gem. Within my application, there are certain lists that have a sortable jQuery function implemented. $ -> $('#projects').sortable handle: '.handle' ...

Adjust the dimensions of the bootstrap dropdown to match the dimensions of its textbox

The second textbox features a bootstrap dropdown with extensive content that is overflowing and extending to other textboxes below it. I am looking for a way to resize the dropdown to fit the size of its corresponding textbox. UPDATE: I want the dropdown ...

Loading content dynamically with AJAX and enabling smooth page scrolling

Looking for a solution to this issue. I have a button on my webpage that, when clicked, uses AJAX to load content into a "div" element below the button. Everything functions properly except for one problem - every time the button is pressed, the page scrol ...

How can I ensure the button remains disabled until all inputs are completed in Redux form?

I am currently studying a react-redux form example that you can find at the following link: In this example, the submit button is enabled if the user fills in at least one input field. I am curious to know if there is a way to enable the button only when ...

Issue of displaying buttons based on sibling's height under certain conditions

OBJECTIVE I have undertaken a project to enhance my skills in React and TypeScript by developing a UI chat interface. The design requirement is that when a chat message has enough vertical space, its action buttons should appear stacked vertically to the ...

Guide on leveraging event delegation to trigger a function depending on the id of the clicked element

Although I have experience with event delegation, I am currently facing a challenge in setting up a single event listener that can execute one of three functions based on the ID of the element clicked. Here is the existing code without event delegation: ...

Tips for creating a curved shape using fabric.js

I am encountering an issue while trying to draw an arc using a circle with a start and end angle in my code. Here is the snippet I am working with: var circle = new fabric.Circle({ radius: 30, left: 20, top: 20, fill: " ...

What is the best way to extend the functionality of npm script with command line arguments

After defining multiple scripts in my package.json file, I encountered an issue when attempting to run the $ npm run lint:fix command. The argument --fix did not get passed down to ./node_modules/.bin/standard, resulting in an error. { "name": "project" ...

Storing the array with the highest length in a temporary array using Javascript

I am currently working with two arrays that can be of equal length or one may be longer than the other. My goal is to determine the longest array if they are not equal in length, and then use this length to control a loop. $.ajax({ url: "/static/Dat ...

Activate all chosen CSS circles

I'm currently working on creating a progress bar using CSS circles. The idea is that when you click on each circle in sequence (1, 2, 3), all three circles and the line connecting them will fill up with red color. If you then go back (3, 2, 1), the co ...

I'm experiencing an issue where my drum app element does not refresh after performing an action dispatch

Struggling with my React/Redux drum app, I'm at a roadblock with the final component that should update with the selected object's ID through an action dispatch. It baffles me why the element won't reflect the changes even though I've c ...

Identify the individual who accessed a hyperlink within an email

My team is planning a small experiment involving sending an email to around 50 employees. The email will contain a link to a website stored on our local server, and I want to be able to track exactly who clicks the link by matching it with their email addr ...

Maximum Age Setting for Iron Session Cookie

Currently, I am attempting to configure a Next JS application with iron-session and a 'remember me' feature. The objective is for the maxAge of the iron-session cookie to be extended to a week if the user selects the remember me option on the log ...

Analyzing the differences in environment management between express and dotenv

Lately, I've noticed an abundance of tutorials and articles discussing NodeJS and the dotenv library. One common practice that keeps coming up is defining an ENV_MODE=development variable within the config.env file. However, it got me thinking - does ...

What is the correct method for downloading an Excel file in a Vue.js application?

I am having difficulty downloading an Excel file in xlsx format using my Vue.js application. The Vue.js application sends a post request to the Node.js application which then downloads the Excel file from a remote SFTP server. The backend application is fu ...

When attempting to add an item to an array within a sub-document using Mongoose and MongoDB, the error message "Property 'messages' not found" is returned

I am working with four different models: teacher, student, teacherMessageSchema, and studentMessageSchema. The teacherMessageSchema is a subdocument within the 'teacher' model under the messages: [teacherMessageSchema] property, while the student ...

When the page is resized for mobile, the Bootstrap modal shifts downwards

I am encountering an issue with a modal that pops up on my webpage when clicked. It is perfectly centered in a browser view and smaller views. However, when I resize the page for phones, the modal shifts down the page and requires scrolling to see it. How ...