John Doe
John Doe

Reputation: 188

KnockoutJS and multiple nested models

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

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>

Upvotes: 2

Views: 692

Answers (1)

Robert Westerlund
Robert Westerlund

Reputation: 4838

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

Upvotes: 1

Related Questions