- Single Page Applications - Part 1 - Basic Desktop application
- Single Page Applications - Part 2 - Advanced Desktop Application
- Single Page Applications - Part 3 - Basic Mobile Application
- Single Page Applications - Part 4 - Sorting, Filtering and Manipulation data with upshot.js
- Single Page Applications - Part 5 - With MVC4 RC
In part 1 I recreate the first part of a demo by Steven Sanderson that shows a list of deliveries on screen, allows a user to mark an item as delivered and have the results immediately sent back to the server. In this second part the application will also show the deliveries grouped by customer and uses "Save All" and "Revert All" buttons so that the user can decide when to update the back-end.
Group by Customer
In order to group deliveries by customer, you'll need to a bit of javascript code that can go over the observable 'deliveries' array and group them together based on the referenced customer. This grouped data needs to be observable too. Steven has released the following bit of javascript called arrayUtils.js in order to achieve this:
/// <reference path="knockout.debug.js" /> (function () { function firstWhere(array, condition) { for (var i = 0; i < array.length; i++) if (condition(array[i])) return array[i]; } ko.observableArray.fn.groupBy = function (keySelector) { if (typeof keySelector === "string") { var key = keySelector; keySelector = function (x) { return ko.utils.unwrapObservable(x[key]) }; } var observableArray = this; return ko.computed(function () { var groups = [], array = observableArray(); for (var i = 0; i < array.length; i++) { var item = array[i], existingGroup = firstWhere(groups, function (g) { return g.key === keySelector(item) }); if (existingGroup) existingGroup.values.push(item); else groups.push({ key: keySelector(item), values: [item] }); } return groups; }) } })();You can now add a new datasource to the DeliveriesViewModel.js like this:
self.deliveries = self.dataSource.getEntities(); self.deliveriesForCustomer = self.deliveries.groupBy("Customer");To show this new information on screen, add following code to index.cshtml. The groupBy operator has put every customer object on the "key" parameter and all his deliveries on the "values" parameter.
<h3>Customers</h3> <ul data-bind="foreach: deliveriesForCustomer"> <li> <div>Name: <strong data-bind="text: key.Name"></strong></div> <ul data-bind="foreach: values"> <li data-bind="text: Description, css: { highlight: IsDelivered }"> </li> </ul> </li> </ul>
If you would run the application now you will notice that knockout.js is keeping the values is the deliveries list and the customers list in sync.
Save, Revert, Exclude if delivered
Upshot can be configured to NOT synchronize the changes to the backend, meaning the user will have to click a "Save" button when he wants to submit. There is also the possibility to undo the accumulated changes or to put a filter on the datasource. In order to enable above functionality, make the following changes to the DeliveriesViewModel.js- Set bufferChanges to true so that the upshot datasource will keep the changes in memory and not sync them with the back-end
- Create a self.localDataSource that we can use for client-side operations like filtering the data
- Create a self.excludeDelivered variable to keep track of the value of the checkbox saying the user wants to exclude already delivered items from the list
- Create the self.saveAll and self.revertAll function to commit or revert the changes on the upshot datasource
- Subscribe a function to the self.excludeDelivered observable that gets called everytime the value changes. Inside this function a new filter rule is created which is then applied to the local data source
/// <reference path="_references.js" /> function DeliveriesViewModel() { // Private var self = this; var dataSourceOptions = { providerParameters: { url: "/api/DataService", operationName: "GetDeliveriesForToday" }, entityType: "Delivery:#DeliveryTracker.Models", bufferChanges: true, mapping : Delivery }; // Public Properties self.dataSource = new upshot.RemoteDataSource(dataSourceOptions).refresh(); self.localDataSource = upshot.LocalDataSource({ source: self.dataSource, autoRefresh: true }); self.deliveries = self.localDataSource.getEntities(); self.deliveriesForCustomer = self.deliveries.groupBy("Customer"); self.excludeDelivered = ko.observable(false); // Operations self.saveAll = function () { self.dataSource.commitChanges() } self.revertAll = function () { self.dataSource.revertChanges() } // Delegates self.excludeDelivered.subscribe(function (shouldExcludeDelivered) { var filterRule = shouldExcludeDelivered ? { property: "IsDelivered", operation: "==", value: false } : null; self.localDataSource.setFilter(filterRule); self.localDataSource.refresh(); }); }
Include the new arrayUtils.js in the Index.cshtml file
<script src="~/Scripts/arrayUtils.js" type="text/javascript"></script> <script src="~/Scripts/App/DeliveriesViewModel.js" type="text/javascript"></script>To take advantage of the functionalities you can bind the view to the functions that have been put in place in the DeliveriesViewModel
- Added two buttons of which knockout binds the click event to the appropriate functions in the DeliveriesViewModel
- The "Exclude Delivered items" checkbox bound to the 'excludeDelivered' property in the viewmodel
- The 'delivered' CSS class is bound to the existing 'IsDelivered' property which was foreseen on the view model. The 'updated' CSS class however is bound to the 'IsUpdated' property that comes built-in with the observable datasource
<div> <h3>Deliveries</h3> <button data-bind="click: saveAll">Save All</button> <button data-bind="click: revertAll">Revert All</button> <label> <input data-bind="checked: excludeDelivered" type="checkbox" /> Exclude delivered items</label> <ol data-bind="foreach: deliveries"> <li data-bind="css: { delivered: IsDelivered, updated: IsUpdated}"> <strong data-bind="text: Description"></strong> is for <em data-bind="text: Customer().Name"></em> <label><input data-bind="checked: IsDelivered" type="checkbox"/>Delivered</label> </li> </ol> <h3>Customers</h3> <ul data-bind="foreach: deliveriesForCustomer"> <li> <div>Name: <strong data-bind="text: key.Name"></strong> </div> <ul data-bind="foreach: values"> <li data-bind="text: Description, css: { delivered: IsDelivered, updated: IsUpdated}"> </li> </ul> </li> </ul> </div>I've defined these new styles in Site.css
.delivered { text-decoration: line-through; color: #008000; } .updated { background-color: #FFFF00; }
If you now mark items as delivered, they will get the 'delivered' AND 'updated' styles in both lists. These are nicely kept in sync by upshot. When the user clicks the "SaveAll" button, all changes will be submitted to the server and the 'updated' style will disappear. The "RevertAll" button undoes all the changes. Selecting "Exclude Delivered Items" will remove the delivered items from the shared datasource so they won't show up in either list.
Download the source code
You can download the code (Visual Studio 2010 project file) from my SkyDrive
4 comments:
Excellent work Bart.
Any chance you could post the code for this project as well?
You can download the code from my Skydrive now
https://skydrive.live.com/redir.aspx?cid=92fec8e2df222664&resid=92FEC8E2DF222664!453&parid=92FEC8E2DF222664!399&authkey=!ANN90ogRSODRCrw
While putting this together I discovered some missing information and a bug in above post. I've highlighted the additions and corrections
Hi Bart, have been following your articles with interest as there is very little documentation available.
Do you know of a way to detect the user clicking the back button with Nav.js?
I have followed the basic SPA tutorials and one thing I noticed is that if you add a TodoItem then click the back button, the TodoItem remains in the list of TodoItems, which I understand but how can you remove that newly added item to the array on back button?
Thanks for any help
If you look around line 50 in the file 'TodoItemsViewModel.js' inside the 'scripts' folder, you'll see that a 'onNavigate' function is defined here.
self.nav = new NavHistory({
params: { edit: null, page: 1, pageSize: 10 },
onNavigate: function (navEntry, navInfo) {
...
}
}).initialize({ linkToUrl: true });
The 'navInfo' object holds a 'isBack' property. So inside this function you test on 'isBack' and then call the 'deleteEntity' function on the datasource and you pass in the entity (todoItem) that you want to remove.
Post a Comment