KnockoutJS and multiple nested models
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 line with edit and delete order line functionality. I've somehow got this working by following some tutorials, but it messes up and I want to start over (finally, I started using jQuery to get what I wanted, but it felt like cheating and halfway through things). Are there any tutorials for this, or any pointers on where I should start (KnockoutJS or other frameworks?) Yes, I've followed the tutorials on kickoutjs.com, but I'm stuck on the functionality of the third level .
Thanks in advance.
EDIT: Follow this 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>
A data-bound nested view model with multiple levels is the same as a data-bound nested view model with a single level.
In the examples below, I'll use yours Store -> Order -> OrderRow
, assuming Store
there's one storeName
property for each Order
, one for each, and one orderNumber
for each . I also render an item at each level .OrderRow
runningNumber
ul
don't use templates
To data-bind a single view's nested view models , the list stores
in this example can be similar to the following:
<ul data-bind="foreach: stores">
<li>
Store Name: <span data-bind="text: storeName"></span>
</li>
</ul>
To data-bind a single view's nested view models, Store -> Order
it can be done with steps similar to the following:
Store Name: <span data-bind="text: storeName"></span>
<ul data-bind="foreach: orders">
<li data-bind="text: orderNumber"></li>
</ul>
To databind a nested viewmodel of a single view, Order -> OrderRow
it can be done in the following way:
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, nest it in multiple levels, as simple as combining the above, move the third code in to replace what's in the li
second , and use the new second code to replace li
the first content in one .
<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 basically have the above code running (although buttons are added for adding new ones Store
, Order
and OrderRow
the object is in) http://jsfiddle.net/8yF6c/ .
with template
To make your code more maintainable, you can use templates instead. Of course, as usual, with such a small example, the benefits may not be so obvious.
With templates, the code basically looks very similar to the first three cases in the above example. 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>
And then the template for the order:
<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 PO line.
<script type="text/html" id="orderRowTemplate">
A row with running number: <span data-bind="text: runningNumber"></span>
</script>
Note that the three parts of code above are the same as the first example single-level binding, just wrapped in an element script
with a type text/html
(to make sure the browser doesn't try to execute it as a script). Then we just need to start using it at the root level storeTemplate
.
<ul data-bind="foreach: stores">
<li data-bind="template: 'storeTemplate'"></li>
</ul>
That's it. As before, my code above works (although the button adds new Store
, Order
and OrderRow
the object is added in) http://jsfiddle.net/Ag8U3/ .
Add edit and delete functionality
Adding editing functionality to the above template (or a binding without a template) is as simple as changing the span
element to a input
box (if you want other bindings to be aware of the change, then of course you need to change some properties to be able to observe). If you need different "modes", edit mode and view mode, you can choose a template dynamically, examples of templates can be found in the template documentation.
To add delete functionality, just add a function that removes an item from the list when the delete button is clicked (e.g. deleteOrder
you Store
could add a function on the object and self.removeOrder = function(order){ self.orders.remove(order); };
then add a button to the order, eg <button data-bind="click: $parent.removeOrder">Remove Order</button>
.) Template example, url as http://jsfiddle.net/Ag8U3/1/ .