Reputation: 188
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
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.
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/.
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 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