Monday, October 8, 2012

A Single Page App with Knockout.js, ASP.NET Web API, and F#

A few weeks ago, I posted a simple example of a Single Page Application (SPA) built with Backbone.js, ASP.NET Web API, F#, C#, and more. Today, I'm posting a similar example, but built with Knockout.js. Let's look at some of the code.

View:

Knockout.js supports binding of model data directly to DOM elements, so the first thing that we will look at is one of the templates that are used for the views. Here's what this example uses for the contacts list view template:

<div class="nine.columns.centered">
<p class="row">
<a href="#/create" class="small button radius">Create Contact</a>
</p>
<div class="row">
<div class="five columns header">FirstName</div>
<div class="five columns header">LastName</div>
<div class="two columns header">Phone</div>
</div>
<div data-bind="foreach: contacts">
<div class="row">
<div class="five columns" data-bind="text: firstName"/>
<div class="five columns" data-bind="text: lastName"/>
<div class="two columns" data-bind="text: phone"/>
</div>
</div>
</div>
view raw gistfile1.html hosted with ❤ by GitHub
The main thing to notice in this markup is the use of the data-bind attributes to bind specific model data to the appropriate DOM elements.

ViewModel:

The example defines a single ViewModel that is used for both of the available views. The code, which is fairly similar to that described at http://knockoutjs.com/examples/contactsEditor.html, is shown below:

(function ( viewModel, $ ) {
viewModel.ContactsViewModel = function ( contacts ) {
var self = this;
self.contacts = ko.observableArray( ko.utils.arrayMap( contacts, function ( contact ) {
return { firstName: contact.firstName, lastName: contact.lastName, phone: contact.phone };
}));
self.addContact = function () {
var data = appFsMvc.utility.serializeObject( $("#contactForm") );
$.ajax({
url: "/api/Contacts",
data: JSON.stringify( data ),
type: "POST",
dataType: "json",
contentType: "application/json"
})
.done(function () {
toastr.success( "You have successfully created a new contact!", "Success!" );
self.contacts.push( data );
window.location.href = "#/";
})
.fail(function () {
toastr.error( "There was an error creating your new contact", "<sad face>" );
});
};
};
})( appFsMvc.ViewModels = appFsMvc.ViewModels || {}, jQuery );
view raw gistfile1.js hosted with ❤ by GitHub
Router:

Another thing to notice is the routing mechanism used in this example. Since Knockout.js doesn't provide built in URL routing, I've used Sammy.js to accommodate the need. Here's what the router looks like:

(function ($) {
appFsMvc.App = function( contactsViewModel ) {
return $.sammy( "#content", function () {
var self = this;
this.use( Sammy.Cache );
this.contactViewModel = contactsViewModel;
this.renderTemplate = function ( html ) {
self.$element().html( html );
ko.applyBindings( self.contactViewModel );
};
// display all contacts
this.get( "#/", function() {
this.render("/Templates/contactDetail.htm", {}, function ( html ) {
self.renderTemplate( html );
});
});
// display the create contacts view
this.get( "#/create", function() {
this.render("/Templates/contactCreate.htm", {}, function ( html ) {
self.renderTemplate( html );
});
});
});
};
$(function () {
$.getJSON( "/api/contacts", function ( data ) {
var viewModel = new appFsMvc.ViewModels.ContactsViewModel( data );
appFsMvc.App( viewModel ).run( "#/" );
});
});
})(jQuery);
view raw gistfile1.js hosted with ❤ by GitHub
ApiController:

Lastly, we'll get a quick look at the API Controller that is written in F#. Here's the code:

open System.Web.Http
open FsWeb.Models
type ContactsController() =
inherit ApiController()
let contacts = seq { yield Contact(FirstName = "John", LastName = "Doe", Phone = "123-123-1233")
yield Contact(FirstName = "Jane", LastName = "Doe", Phone = "123-111-9876") }
member x.Get() =
contacts
member x.Post ([<FromBody>] contact:Contact) =
contacts |> Seq.append [ contact ]
view raw gistfile1.fs hosted with ❤ by GitHub
That's pretty much it. You can find the full solution at https://github.com/dmohl/FsWebSpa-Knockout.