The Ferrari meets JayData

Good morning, good afternoon, good evening to you.

Welcome back to a new episode in the Ferrari series. For those of you who remember what I call the Ferrari read on; all other please make yourself familiar with Part 1 and Part 2 in this series before moving on.

In Part 2 we ended up with a HTML5 app that shows a nice metro style tile view of SharePoint lists and libraries. This time we want to enhance the solutions so that it retrieves items through SharePoint’s OData service. Sorry SP2007 we’ve to leave you behind as this service was first introduced in SP2010. Don’t worry too much though; using REST is forward thinking as SP2013 will expose a whole new REST API that will greatly expanding the existing capabilities.

In the simplest form you could make XHR calls to the service endpoint at _vti_bin/listdata.svc, but we’re not going down that route. Instead we’re using JayData for doing the heavy lifting. While being relatively new to the market JayData brings an astonishing range of capabilities to the table, one of them is the abstracting of underlying storage provider. Yes, you heard me right; OData is just one of the supported storage provider, see JayData provider for a full list. Once you learned a little bit about JSLQ the JavaScript Query Language you will (probably) never go back talking to any storage provider on your own.

After settling on JayData the next choice is about a client framework that helps us building our dynamic UI. JayData comes with a bunch of modules that offers integration into various third party frameworks and knockout is one of them. A word of warning take everything that you see me doing with knockout with some grain of salt, as I haven’t worked with it before. The last piece of software that I want to bring to the table is of course RequireJS. We are going to build a JavaScript app and based on my experience there’s simply no way of writing one without it.

So without further hassle we are now going to build a pretty straight forward three tier navigation, which BTW will align us nicely with the Metro-style design guides; and nah, we won’t do Metro style left <-> right scrolling, this is still a web app not Window 8 Metro ;-). For the eager of you here are the links to the live demos:

Live on SP 2010

Live on SP 2013

First tier: Entry screen consists of a Tile View (green) and a Login View (yellow). The whole page can be served anonymously, but in order to see the next tier you’ve to log on. You probably heard me saying that before “SharePoint OData service won’t work anonymously”.

Tile View

The following HTML is used as by Knockout to produce the Tile View.

 1 <div class="row" id="tileVM">
 2     <div class="twelve columns tiles"
 3          data-bind="foreach: TileData, updateTilesOnce: true">
 4         <div class="live-tile" data-stops="100%" data-speed="750" data-delay="-1"
 5              data-bind="class: color + ' ' + size,
 6                         attr: {'data-mode': mode ? mode : '', 'data-direction': direction} ,
 7                         click: $root.goToDetail">
 8             <span class="tile-title" data-bind="text: title"></span>
 9             <span class="badge" data-bind="text: count"></span>
11             <div>
12                 <img class="micon" data-bind="attr: {src: icon}"/>
13             </div>
14             <div>
15                 <h3 data-bind="text: backTitle"></h3>
16                 <span class="prettyDate" data-bind="attr: {title: prettyDate}"></span>
17             </div>
18         </div>
19     </div>
20 </div>

And here’s the TileViewModel as RequireJS module leveraging the revealing module pattern. I’ve seen John Papa mentioning this pattern more than once. BTW feel free to provide feedback about your favorite way to structure knockout apps.

 1 define (['knockout', 'helper'], function ( ko, fn ) {
 2     "use strict";
 4     return function () {
 5         // ko observables
 6         var userId = ko.observable ().subscribeTo ('userId'),
 7             selectedList = ko.observable ().subscribeTo ('selectedList'),
 8             TileData = ko.observableArray ([]),
 9         // functions
10             goToDetail;
12         // end of var declaration
14         goToDetail = function ( tile, event ) {
15             if ( userId () !== 'anonymous' ) {
17                 selectedList (fn.cleanup (tile.title));
18                 location.hash = '/view/' + selectedList ();
19             }
20             else {
21                 alert (' Make sure to log on.');
22             }
23         };
25         // Bootstrap
26         TileData (app.tilesData.tiles.tile);
29         // Return public methods
30         return{
31             userId : userId,
32             TileData : TileData,
33             goToDetail : goToDetail
34         };
35     };
36 });

Our trusted DVWP produces the required app.tilesData.tiles.tile array that is used to bootstrap the TileData observableArray. I’m not going to cover the required XSLT here as this is just a variation what we did in Part 2, but if you want to deep dive again take a look at this gist DVWP’s settings and XSLT.

Here’s the HTML for the login area and the related LogonViewModel.

 1  <div id="logonVM">
 2     <div data-bind="visible: userId() !== 'anonymous' " style="display: none;">
 3         You're logged on as: <span class="success label" data-bind="text: userId"></span>
 4     </div>
 5     <div data-bind="visible: userId() === 'anonymous' " style="display: none;">
 6         <a href="#" data-bind="attr: {href: loginURL}" class="button"> Sign
 7             in</a>
 8         with username: <span class="secondary label">ODataDemo</span> password: <span class="secondary label">OData!Demo</span>
 9     </div>
10 </div>
 1 define (['knockout'], function ( ko ) {
 2     "use strict";
 4     return function () {
 5         var userId = ko.observable (app.configMap.userId).publishOn ('userId'),
 6             loginURL;
 8         loginURL = ko.computed (function () {
 9             return '../_layouts/Authenticate.aspx?Source=' + encodeURIComponent (location.pathname) + location.hash;
10         });
12         // Return public methods
13         return {
14             userId : userId,
15             loginURL : loginURL
16         }
17     }
18 });

Second tier: The Login View (yellow) will show your username once you’re logged in and by clicking on one of the tiles you’ll see 10 items of the chosen list or library. Bear in mind that this is a demo, but adding paging, sorting and filtering per data source in the UI would be a pretty straight forward as they are already implemented in the ListingViewModel.

Listing View

Third tier: By clicking on one of the level 2 tiles you’ll see the full detail information on the right. One of the benefits of working with OData is that is allows you to get multiple related information in single call, which greatly simplifies your life as client side app developer.

From the OData website

A URI with a $expand System Query Option indicates that Entries associated with the Entry or Collection of Entries identified by the Resource Path section of the URI must be represented inline (i.e. eagerly loaded). For example, if you want to identify a category and its products, you could use two URIs (and execute two requests), one for /Categories(1) and one for /Categories(1)/Products. The ‘$expand’ option allows you to identify related Entries with a single URI such that a graph of Entries could be retrieved with a single HTTP request.

Detail View

For this demo I haven’t separated the Listing and the Detail view and the Detail view is just an un-styled list of properties for the current item. For a larger application it would probably make sense to separate the views and work the detail view a little bit out ;-).

To see the full html and JS that is involved take a look at this gist, I’m going to concentrate on the parts that deal with the JayData configuration here.

But before we can do that, we need to take quick look how all the various VMs are integrated by RequireJS. As in almost every standard RequireJS app there’s a main.js file that allows you to configure various aspects and then kick off your app.

 1 require.config({
 2    //By default load any module IDs from js/lib
 3     baseUrl : 'libs',
 4     paths : {
 5        // Try loading from CDN first
 6        ...
 8 require(['app'], function (app) {
 9     // we can safely kick of the app even before document.ready
10     app.init();
11 });

Within app.js we define dependencies on the various VMs that you’ve seen earlier and within the init method we use ko.applyBindings() to bind them to the corresponding element in the DOM. As said I’m new to knockout, so I leave it to the experts if that’s a recommend pattern or not. At least it’s working, so it can’t be totally wrong ;-). Path.js is used as a light alternative to sammy.js in order to setup some URL rules. As you can see Ryan Niemeyer’s excellent postbox -a native ko pubsub sytem- is used to notify subscribers whenever changes occur.

 1 define (['jquery', 'knockout', 'LogonVM', 'TileVM', 'ListingVM', 'postbox', 'path', 'appData', 'kocBH' ],
 2     function ( $, ko, LogonVM, TileVM, ListingVM, postbox ) {
 3         "use strict";
 5         var init = function () {
 7             // Exposing ko as global
 8             window.ko = window.ko || ko;
 9             var $tileContainer = $ ('#tileVM');
11             // Configuring JayData to use current site's Odata service
12             app.context = new app.MetroStyleDataContext ({ name : 'oData', oDataServiceHost : '../_vti_bin/listdata.svc' });
14             // binding ko to the appropriate container
15             ko.applyBindings (new TileVM (), document.getElementById ('tileVM'));
16             ko.applyBindings (new LogonVM (), document.getElementById ('logonVM'));
17             ko.applyBindings (new ListingVM (), document.getElementById ('listingVM'));
19             // Client-side routes. Path exposed as global via shim configuration
20             function toggleTiles() {
21                 var isVisible = $tileContainer.css ('display') === 'block';
22                 isVisible ? $tileContainer.slideUp (300) : $tileContainer.slideDown (300);
23             }
25    ("#/view/:list").to (function () {
26                 postbox.publish ('selectedList', this.params.list);
27             }).enter (toggleTiles);
29    ("(#)").to (function () {
30                 postbox.publish ('selectedList', '');
31             }).enter (toggleTiles);
33             Path.listen ();
34         };
36         return {
37             init : init
38         }
40     });

Boy, Rainer! All those talk about RequireJS and knockout basics. I’m already familiar with it, so where’s the beef? You promised to tell us about JayData, didn’t you?

Yes I did and here you go.

Part 1: Retrieving ListingView date via OData service.

 1  postbox.subscribe ("selectedList", function ( newValue ) {
 2     if ( app.configMap.userId === 'anonymous' ) {
 3         return alert ('Make sure to log on.');
 4     }
 5     if ( newValue !== '' ) {
 6         // Clean out existing itemDetail
 7         itemDetail ([]);
 8         var base = app.context[newValue],
 9             myBase = _.extend ({}, base),
10             sortExp = 'it.' + orderBy ();
12         if ( orderAsc () ) {
13             _.extend (myBase, myBase.orderBy (sortExp));
14         }
15         else {
16             _.extend (myBase, myBase.orderByDescending (sortExp));
17         }
18         _.each (includeArray (), function ( inc ) {
19             _.extend (myBase, myBase.include (inc));
20         });
21 (chooseMap (newValue))
22             .take (take ())
23             .toArray (allItems);
24     }
25     else {
26         // Clean out existing allItems
27         allItems ([]);
28         // Clean out existing itemDetail
29         itemDetail ([]);
30     }
31 });

Within ListingViewModel.js you find the above postbox.subscriber that listen to any change in the selectedList channel. After checking if there’s a non empty value and that the user is already logged in JayData takes over.

  1. app.context knows about all lists and libraries, so here we use base to select a specific one var base = app.context[newValue]

  2. orderBy() or orderByDescending() are using the default sorting criteria . This can be modified during runtime by updating the OrderAsc and OrderBy observables.

  3. include() in OData terms maps to the OData $expand verb. Default settings here are CreatedBy and ModifiedBy. There’s a includeArray() observabelArray that is used to store the ones you’re interested in.

  4. map() mapping or projection is another important feature that allows you to select specific fields instead of retrieving all information. The chooseMap() method is used to retrieve a small set of fields based on their availability e.g. Title vs. Name vs URL.

  5. take() allows you to restrict the number of items returned by the system. Again a observable is used to store the default.

  6. toArray() and finally this method is used to update the allItems observableArray. For those of you that are curious how that works under the cover, take a look at the following blog post.

Part 2: Retrieving the Detail View data for a selected item.

 1  showDetails = function (currItem) {
 2     var currentList = selectedList();
 4     app.context[currentList]
 5         .single(function (item) {
 6             return item.Id == this.Id
 7         },
 8         {Id : currItem.Id},
 9         function (item) {
10             var keyValue = [];
11             _.each(item.toJSON(), function (val, key) {
12                 keyValue.push({"key" : key, "val" : val});
13             });
14             itemDetail([]);
15             ko.utils.arrayPushAll(itemDetail(), keyValue);
16             itemDetail.valueHasMutated();
17         });
18 };

Come on, that’s almost too easy isn’t it? This time we’re using JSLQ`s .single method to retrieve ONE item based on a unique criteria, here the currItem.Id. Whenever the system returns the information it’s converted to JSON. Underscore’s each method allows us to push the returned key/value pairs into the keyValue array. Whenever done we use ko.utils.arrayPushAll to update the itemDetails() observable and valueHasMutated() to notify it about the changes.

As you can see, we haven’t dealt a bit with the native OData protocol and when you take a look at what’s going over the wire you’ll be surprised how complex it can get. But luckily that is of no concern for us any longer as we only have to deal with JSLQ.

That’s it for today, but before you ask I wanna cover two questions that I feel coming up:

Would you choose JayData for another project?

That’s a clear YES. JayData is something that’s better than sliced bread. Luckily it’s already invented.

Would you choose knockout for another project?

Yes… most likely. After reading some of the stuff over at the knockout site itself, but more important over at Ryan Niemeyer’s blog, I found it pretty straight forward to work with. By being the only overall framework that JayData supports as module at the moment, it’s the natural choice for the time being. With that said, there are already Sencha and Titanium modules out there, so maybe there’s a JayData backbone module one day… at that point feel free to ask me that question again.

One last thing, you can find the code for the 2010 demo site on github. You have to update the appdata.js in order to work in your environment. Checkout this post that guides you through the required steps with JaySvcUtil.

Published: August 01 2012

blog comments powered by Disqus