KnockoutJS and multiple nested models


John Doe

I'm trying to find some tutorials on how to create a nested viewmodel with more than two levels, for example:

  • Store
    • Order
      • Order row
    • Order
      • Order row
      • Order row
  • Store
    • Order
      • Order row

All the orders are listed for the stores and when I click on an order I should see the order rows with the ability to edit and delete order rows. I've got this working somehow by following a few tutorials but it got messed up and I'm looking to start over (in the end I start using jQuery to get what I want but it feels like cheating and doing something half-done). Are there any tutorials out there for this or any pointers on where I should start (KnockoutJS or other framework? Yes I've followed the tutorials on knockoutjs.com but get stuck on the functionality for the third level.

Thanks in advance.

Edit: by following this one http://jsfiddle.net/peterf/8FMPc/light/

JS (simplified)

// required by sharepoint
ExecuteOrDelayUntilScriptLoaded(loadTeams, "sp.js");                    

ko.observable.fn.beginEdit = function (transaction) {
    var self = this;
    var commitSubscription, rollbackSubscription;

    if (self.slice) {
        self.editValue = ko.observableArray(self.slice());
    }
    else {
        self.editValue = ko.observable(self());
    }

    self.dispose = function () {        
        commitSubscription.dispose();
        rollbackSubscription.dispose(); 
    };

    self.commit = function () {
        self(self.editValue());
        self.dispose();
    };

    self.rollback = function () {
        self.editValue(self());        
        self.dispose();
    };

    commitSubscription = transaction.subscribe(self.commit, self, "commit");    
    rollbackSubscription = transaction.subscribe(self.rollback, self, "rollback");

    return self;
}

 function TeamModel (){
    var self = this;
    self.Team = function(title, members) {
        this.title = title;
        this.members = members;
    }
    self.editingItem = ko.observable();   
    self.editTransaction = new ko.subscribable();        
    self.isItemEditing = function(task) {
        return task == self.editingItem();
    };

    self.editTask = function (task) {
        if (self.editingItem() == null) {
            task.beginEdit(self.editTransaction);
            self.editingItem(task);
        }
    };

    self.removeTask = function (task) {
        if (self.editingItem() == null) {
            var answer = confirm('Are you sure you want to delete this task? ' + task.title());
            if (answer) {
                // SharePoint client object model to delete task
            }
        }
    };        

    self.applyTask = function (task) {
        self.editTransaction.notifySubscribers(null, "commit");        
        // SharePoint client object model to update task                    
        //  hides the edit fields
        self.editingItem(null);                                         
    };

    self.cancelEdit = function (task) {
        self.editTransaction.notifySubscribers(null, "rollback");        
        self.editingItem(null);
    };

    self.Member = function(name, id) {
        this.name = name;
        this.Tasks = ko.observableArray([]);
        this.Task = function(id, title, priority, userComment, managerComment) {
            this.id = ko.observable(id);
            this.title = ko.observable(title);
            this.priority = ko.observable(priority);
            this.userComment = ko.observable(userComment);
            this.managerComment = ko.observable(managerComment);
            this.beginEdit = function(transaction) {
                //this.title.beginEdit(transaction);
                //this.userComment.beginEdit(transaction);
            }                                                                                                                               
        }
        this.id = id;
        this.retrieveTasks = function() {
            if(this.Tasks().length === 0) {
                // First click, expand                                                    
                // SharePoint client object model to get tasks
            } else {
                // Collapse
                //this.Tasks.removeAll();                   
            }       
        }
    }
    self.Teams = ko.observableArray([]);                                 
    self.retrieveTeams = function() {  
            // SharePoint client object model to get a list of teams and their members
            self.Teams.push(new self.Team(oListItem.get_item('Title'), members));  
    }               
}

function loadTeams() {
    var VM = new TeamModel();
    VM.retrieveTeams();     
    VM.availableRankings = ["1","2","3","4","5","6","7","8","9","10"]
    ko.applyBindings(VM);           
}

HTML

<div id="Workload" data-bind="visible: Teams().length>0">
    <div data-bind="foreach: Teams" class="teams">                         
        <div >
            <h3 data-bind="text: title"></h3>
                <div data-bind="foreach: members">
                    <div class="member">
                        <div data-bind="click: retrieveTasks">
                            <span data-bind="text: name" class="name"></span>
                        </div>
                        <table class="tasks" data-bind="visible: Tasks().length>0">
                            <tr>
                                <td class="title">Title</td>
                                <td class="priority">Priority</td>
                                <td class="user-comment">User Comment</td>
                                <td class="manager-comment">Manager Comment</td>                                                                                                                                                                                                                                                                                                                          
                            </tr>
                            <tbody data-bind="foreach: Tasks">
                                <tr class="row">                                                                                                                                    
                                    <td class="rowItem">
                                        <input type="text" class="edit" data-bind="value: title, visible: $root.isItemEditing($data)"/>
                                        <label class="read" data-bind="text: title, visible: !$root.isItemEditing($data)"/>
                                    </td>
                                    <td class="rowItem">
                                        <select class="edit priority" data-bind="options: $root.availableRankings, value: priority, visible: $root.isItemEditing($data)"></select>
                                        <label class="read" data-bind="text: priority, visible: !$root.isItemEditing($data)" />                                                                                                                                   
                                    </td>
                                    <td class="rowItem">
                                        <textarea rows="3" cols="25" class="edit userComment" data-bind="value: userComment, visible: $root.isItemEditing($data)"></textarea>
                                        <label class="read" data-bind="text: userComment, visible: !$root.isItemEditing($data)"/>                                                                                                                         
                                    </td>
                                    <td class="rowItem">
                                        <textarea rows="3" cols="25" class="edit managerComment" data-bind="value: managerComment, visible: $root.isItemEditing($data)"></textarea>
                                        <label class="read" data-bind="text: managerComment, visible: !$root.isItemEditing($data)"/>                                                                                                                               
                                    </td>
                                    <td class="tools">
                                        <a class="button toolButton" href="#" data-bind="click: $root.editTask.bind($root), visible: !$root.isItemEditing($data)">
                                        Edit</a>
                                        <Sharepoint:SPSecurityTrimmedControl runat="server" Permissions="DeleteListItems">
                                        <a class="button toolButton" href="#" data-bind="click: $root.removeTask.bind($root), visible: !$root.isItemEditing($data)">
                                        Remove</a>
                                        </SharePoint:SPSecurityTrimmedControl>                                                                                                                                  
                                        <a class="button toolButton" href="#" data-bind="click: $root.applyTask.bind($root), visible: $root.isItemEditing($data)">
                                        Apply</a>
                                        <a class="button toolButton" href="#" data-bind="click: $root.cancelEdit.bind($root), visible: $root.isItemEditing($data)">
                                        Cancel</a>
                                    </td>                                                                                                                                    
                                </tr>                                         
                            </tbody>
                        </table>
                </div>
            </div>
        </div>   
    </div>
</div>
Robert Westerlund

Databinding nested view models with multiple levels is just the same as databinding nested view models with a single level.

In the below examples, I'll use your example with Store -> Order -> OrderRow, assuming that each Store has a storeName property, each Order has an orderNumber and each OrderRow has a runningNumber. I've also rendered the items inside an ul on each level.

Without using templates

To databind nested view models a single level, the stores list in this example, could be done similar to:

<ul data-bind="foreach: stores">
    <li>
        Store Name: <span data-bind="text: storeName"></span>
    </li>
</ul>

To databind nested view models a single level, from Store -> Order could be done similar to:

Store Name: <span data-bind="text: storeName"></span>
<ul data-bind="foreach: orders">
    <li data-bind="text: orderNumber"></li>
</ul>

To databind nested view models a single level, from Order -> OrderRow could be done like:

Order number: <span data-bind="text: orderNumber"></span>
<ul data-bind="foreach: rows">
    <li>
        A row with running number: <span data-bind="text: runningNumber"></span>
    </li>
</ul>

To do this nested in multiple levels, it's as simple as combining the above, moving the third code in to replace the contents of the li in the second and then the new second code to replace the li contents in the first.

<ul data-bind="foreach: stores">
    <li>
        Store Name: <span data-bind="text: storeName"></span>
        <ul data-bind="foreach: orders">
            <li>
                Order number: <span data-bind="text: orderNumber"></span>
                <ul data-bind="foreach: rows">
                    A row with running number: <span data-bind="text: runningNumber"></span>
                </ul>
            </li>
        </ul>
    </li>
</ul>

I have basically the above code running (though buttons are added for adding new Store, Order and OrderRow objects) at http://jsfiddle.net/8yF6c/.

With templates

To make the code easier to maintain you could do it with templates instead. Of course, as always, the benefit might not be as clear with such a small example as this.

In the case with templates, the code will basically look pretty much like the first three cases in the above sample; before merging the html. First, the template for the store:

<script type="text/html" id="storeTemplate">
    Store Name: <span data-bind="text: storeName"></span>
    <ul data-bind="foreach: orders">
        <li data-bind="template: 'orderTemplate'"></li>
    </ul>
</script>

Then the template for the orders:

<script type="text/html" id="orderTemplate">
    Order number: <span data-bind="text: orderNumber"></span>
    <ul data-bind="foreach: rows">
        <li data-bind="template: 'orderRowTemplate'"></li>
    </ul>
</script>

And finally the template for the order row.

<script type="text/html" id="orderRowTemplate">
    A row with running number: <span data-bind="text: runningNumber"></span>
</script>

Observe that the above three code parts are just the same as the first examples single level bindings, only wrapped in a script element with type text/html (to ensure that the browser doesn't try to execute it as script). Then we just need something at the root level to start using the storeTemplate.

<ul data-bind="foreach: stores">
    <li data-bind="template: 'storeTemplate'"></li>
</ul>

And that's pretty much it. Just as before, I have the above code running (though buttons are added for adding new Store, Order and OrderRow objects) at http://jsfiddle.net/Ag8U3/.

Adding edit and delete functionality

Adding editing functionality to the above templates (or bindings without templates) is as simple as changing the span elements to input boxes (if you want other bindings to be aware of the change you will of course need to change some properties to be observables). If you want different 'modes', an edit mode and a view mode, you could look into dynamically choosing your template, which you can find examples of in the knockout documentation.

To add deletion functionality, just add a function which removes the items from the list when clicking the delete button (e.g. adding a deleteOrder function on the Store object could be self.removeOrder = function(order){ self.orders.remove(order); }; and then add a button to the order, like <button data-bind="click: $parent.removeOrder">Remove Order</button>. I've added delete functionality to the template sample at http://jsfiddle.net/Ag8U3/1/.

Related


KnockoutJS and multiple nested models

John Du I'm trying to find some tutorials on how to create nested view models with more than two levels like: shop order PO line order PO line PO line shop order PO line All orders for the store are listed and when I click on an order I should see an order lin

KnockoutJS and multiple nested models

John Du I'm trying to find some tutorials on how to create nested view models with more than two levels like: shop Order PO line Order PO line PO line shop Order PO line All orders for the store are listed and when I click on an order I should see an order lin

KnockoutJS and multiple nested models

John Du I'm trying to find some tutorials on how to create nested view models with more than two levels like: shop Order PO line Order PO line PO line shop Order PO line All orders for the store are listed and when I click on an order I should see an order lin

Querying Nested Models with Multiple Picks

they are old I'm just wondering if the following is possible: I have models with nested associative models. I would like to be able render json:to current_user.reports.minnedand have it eager_loadswipe values from each model. what should I do? Here, I only use

Querying Nested Models with Multiple Picks

they are old I'm just wondering if the following is possible: I have models with nested associative models. I would like to be able render json:to current_user.reports.minnedand have it eager_loadswipe values from each model. what should I do? Here, I only use

Querying Nested Models with Multiple Picks

they are old I'm just wondering if the following is possible: I have models with nested associative models. I would like to be able render json:to current_user.reports.minnedand have it eager_loadswipe values from each model. what should I do? Here, I only use

django rest framework nested fields with multiple models

Momokjaaaaa This is django and django rest framework. I have 2 models: User and Phone. first question: I want to be able to update user data (email) as well as phone data (phone number) in 1 api update response. Phone numbers can be 0 or many. Well, it's actua

Web API 2 Multiple Nested Models

kwv84 I am creating a web service where I can post new orders with multiple lines. role model public class Order { public int OrderID { get; set; } public string Description { get; set; } public string Account { get; set; } public ICollection<

Django insert performance with multiple nested models

Thanh Nguyen I have the following model definition: class Workflow(models.Model): name = models.CharField(max_length=255) class Step(models.Model): workflow = models.ForeignKey(Section, on_delete=models.CASCADE, related_name='steps') title = model

Django - Handling multiple nested models at different levels

CH Andre Meloa I have some questions about Django nested models. As far as I know, to handle nested data structures, I have to deserialize each data and then create objects that will be concatenated with the ForeignKeyField. I can handle this by overriding the

django rest framework nested fields with multiple models

Momokjaaaaa This is django and django rest framework. I have 2 models: User and Phone. first question: I want to be able to update user data (email) as well as phone data (phone number) in 1 api update response. Phone numbers can be 0 or many. Well, it's actua

django rest framework nested fields with multiple models

Momokjaaaaa This is django and django rest framework. I have 2 models: User and Phone. first question: I want to be able to update user data (email) as well as phone data (phone number) in 1 api update response. Phone numbers can be 0 or many. Well, it's actua

Django insert performance with multiple nested models

Thanh Nguyen I have the following model definition: class Workflow(models.Model): name = models.CharField(max_length=255) class Step(models.Model): workflow = models.ForeignKey(Section, on_delete=models.CASCADE, related_name='steps') title = model

Web API 2 Multiple Nested Models

kwv84 I am creating a web service where I can post new orders with multiple lines. role model public class Order { public int OrderID { get; set; } public string Description { get; set; } public string Account { get; set; } public ICollection<

Nested Json for Django multiple foreign key models

Mehmet Ince I have 4 models with relationships via FK. class Journal(models.Model): name = models.CharField(max_length=255) class Volume(models.Model): journal = models.ForeignKey(Journal, related_name='volumes') number = models.IntegerField() cl

Web API 2 Multiple Nested Models

kwv84 I am creating a web service where I can post new orders with multiple lines. role model public class Order { public int OrderID { get; set; } public string Description { get; set; } public string Account { get; set; } public ICollection<

Django - Handling multiple nested models at different levels

CH Andre Meloa I have some questions about Django nested models. As far as I know, to handle nested data structures, I have to deserialize each data and then create objects that will be concatenated with the ForeignKeyField. I can handle this by overriding the

Validating multiple models into one with nested properties

Radek So I'm trying to build a form consisting of fields from two models. Unfortunately, although they are the same, the validation is only for one of them. If there is a white symbol in the first field, the console shows a red "rollback" and the view shows an

Django - Handling multiple nested models at different levels

CH Andre Meloa I have some questions about Django nested models. As far as I know, to handle nested data structures, I have to deserialize each data and then create objects that will be concatenated with the ForeignKeyField. I can handle this by overriding the

django rest framework nested fields with multiple models

Momokjaaaaa This is django and django rest framework. I have 2 models: User and Phone. first question: I want to be able to update user data (email) as well as phone data (phone number) in 1 api update response. Phone numbers can be 0 or many. Well, it's actua

Web API 2 Multiple Nested Models

kwv84 I am creating a web service where I can post new orders with multiple lines. role model public class Order { public int OrderID { get; set; } public string Description { get; set; } public string Account { get; set; } public ICollection<

Web API 2 Multiple Nested Models

kwv84 I am creating a web service where I can post new orders with multiple lines. role model public class Order { public int OrderID { get; set; } public string Description { get; set; } public string Account { get; set; } public ICollection<

Django insert performance with multiple nested models

Thanh Nguyen I have the following model definition: class Workflow(models.Model): name = models.CharField(max_length=255) class Step(models.Model): workflow = models.ForeignKey(Section, on_delete=models.CASCADE, related_name='steps') title = model

Nested Json for Django multiple foreign key models

Mehmet Ince I have 4 models with relationships via FK. class Journal(models.Model): name = models.CharField(max_length=255) class Volume(models.Model): journal = models.ForeignKey(Journal, related_name='volumes') number = models.IntegerField() cl