ict.ken.be

 

Building a SPA with html5, web api, knockout and jquery - Notes

Related Posts

Categories: CSS Javascript Notes WebApi

Building single page apps with html5, asp.net web api, knockout and jquery by JohnPapa.net and Pluralsight

 
Wonderful, just wonderful. This is one of the best and most complete tutorials I have ever seen. A must for every web developer. Non-developers should at least watch the application, to see how user friendly and responsive it is. The senior developer will get plenty of pointers to useful frameworks and libraries, while the beginner will get clear explanations and the why and how things are done. The included source code is very reusable and the only things that might need to be added to be able to use this in an enterprise environment are some secure token service and caching. (the use of breeze or upshot is recommended but not shown in the tutorial, probably because these frameworks are very new?) Enjoy. 
Why SPA?
  • As rich and responsive as a desktop app
  • Persisting important state on the client
  • Mostly loaded in the intitial page load
  • Progressively downloads features as required
  • 2-way data binding (MVVM)
  • Tombstoning / dropped connections 
1.CODE CAMPER
Technologies used:
  • CSS
  • LESS
  • Media Queries and responsive design
  • HTML5
  • Modernizr
  • HTML5 Boilerplate
  • AMD - Async Module Definition
  • Revealing Module Pattern
  • Prototypes
  • MVVM
  • jQuery - DOM/AJAX (+jquery.mockjson +jquery.activity)
  • knockout.js - Data Binding/MVVM (+Commanding +ChangeTracking +Templating +Validation)
  • amplify.js - Data Push/Pull, Client Storage, Messaging
  • Sammy.js - Nav/History
  • require.js - Dependency Resolution
  • underscore.js - Javascript Helpers
  • moment.js - Date parsing and formatting
  • toastr.js - UI Alerts
  • qunit - Testing
  • POCO Models
  • JSON and AJAX
  • Web API
  • Unit of Work Pattern
  • Repository Pattern
  • Single Responsibility Principle (SRP)
  • Entity Framework (EF Code First)
  • SQL Database
  • NuGet
  • JSON.NET
  • Ninject IoC
  • Web Optimization
2. TOOLS
  • JsFiddle.com / JsBin.com
  • Sublime / Notepad++
  • Mindscape - less in visual studio
  • Resharper
  • Responsinator.com - check in different sizes
  • ElectricPlum.com - iphone emulator
  • http://jpapa.me/responsivedesignbookmarklet
  • Web Standards Visual Studio Plugin
  • Web Essentials Visual Studio Plugin
3.DATA LAYER (EntityFramework 5.0)
SessionBrief (uses IDs)
Session: SessionBrief (gets full properties)
[Key] (uses Id as default)
DbSet<T>
EntityTypeConfiguration<T>
HasRequired( ).WithMany( ).HasForeignKey( ) 
override OnModelCreating : modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
CodeCamperDbContext : DbContext
{
   public CodeCamperDbContext() : base(nameOrConnectionString: "CodeCamper") { }
   public DbSet<Session> Sessions { get; set; }
}
Database.SetInitializer( );
CreateDatabaseIfNotExists<T>
DropCreateDatabaseIfModelChanges<T>
HasKey
EnityTypeConfiguration
HasRequired (s => s.Speaker).WithMany(p => p.SpeakerSessions).HasForeignKey(s => s.SpeakerId);
SRP - Single Responsibility Principle
public interface IRepository<T> where T : class
{
   IQueryable<T> GetAll();
   T GetById(int id);
   void Add(T entity);
   void Update(T entity);
   void Delete(T entity);
   void Delete(int id);
}
public class EFRepository<T> : IRepository<T> where T : class
{
   protected DbContext DbContext { get; set; }
   protected DbSet<T> DbSet { get; set; }
   public virtual T GetById(int id)
   {
      return DbSet.Find(id);
   }
  ...
}
Unit of Work Pattern decouples webapi controllers from the repositories and dbcontext
4. JSON with WebApi
Ninject IoC Container
kernel = new StandardKernel();
Kernel.Bind<ICodeCamperUow>().To<CodeCamperUow>();
..
config.DependencyResolver = new NinjectDependencyResolver(kernel);
 
[ActionName("rooms")]
GetRooms()
[ActionName("timeslots")]
GetTimeslots()
 
get throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
put return new HttpResponseMessage(HttpStatusCode.NoContent);
post 
response = request.createresponse(HttpStatusCode.Created);
response.Headers.Location = new Uri(Url.Link(RouteConfig.ControllerAndId, new { id = ...
testing : endpoints respond with success? return expected data? crud works?
config.Formatters.Remove(config.Formatters.XmlFormatter);
 
var json = config.Formatters.JsonFormatter;
json.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
 
[Required(ErrorMessage = "Title is required")]
config.Filters.Add(new ValidationActionFilter());
//Puts the model errors in a JObject and returns them with
content.Response.CreateResponse<JObject>(HttpStatusCode.BadRequest, errors);
5. SPA Frame
 
Fallback for jQuery if CDN fails
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="script/lib/jquery-1.7.2.min.js"><\/script>')</script>
 
To activate bundles, the compilation debug has to be false.
bundles.UseCdn = true 
Modernizr goes separate since it loads first
bundles.Add(new ScriptBundle("~/bundles/modernizr").Include("~/Scripts/lib/modernizr-{version}.js"));
@Scripts.Render("~/bundles/modernizr") 
bundles.Add(new ScriptBundle("~/bundles/jsmocks").IncludeDirectory("~/Scripts/app/", "*.js", searchSubdirectories:false);
 
bundles.Add(new StyleBundle("~/Content/css").Include(...);
bundles.Add(new StyleBundle("~/Content/less", new LessTransform(), new CssMinify()).Include("~/Content/styles.less"));
@Styles.Render("~/Content/css","~/Content/less")
6. Javascript Structure
  • Who is responsible for what?
  • Global scope pollution
  • High code re-use
  • Separation of concerns
  • Revealing Module Pattern
  • Bootstrapper.js will prime the data and setup the presentation
 
jQuery's deferreds, promises and $.when() at http://api.jquery.com/jquery.when/
 
ModularDemo to show js module dependencies: http://requirejs.org
define('alerter', ['dataservice'], function (dataservice) { ... });
requirejs.config({ baseUrl:'scripts', paths: { 'jquery' : 'jquery-1.7.2' } });
7. MVVM & KNOCKOUT
<input class="filter-box" type="text" 
data-bind="value: sessionFilter.searchText, 
valueUpdate: 'afterkeydown', 
escape: clearFilter"
placeholder="filter by model and brand"/>
 
_.reduce(timeslots, function (memo, slot) {
...
day = moment(slotStart).format('ddd MM DD');
...
}
 
configureExternalTemplates = function () {
infuser.defaults.templatePrefix = "_";
infuser.defaults.templateSuffix = ".tmpl.html";
infuser.defaults.templateUrl = "/Tmpl";
}
 
ko.bindingHandlers.escape example
 
self.searchText = ko.observable().extend({ throttle: config.throttle });
 
WebEssentials Plugin
ko.bindingHandlers example for favorites icon
 
sessions = ko.observableArray().trackReevaluations()
 
Debugging Tip
<div style="border: white dotted thin; clear: both">
<h4>Sessions JSON</h4>
<pre data-bind="text:ko.utils.debugInfo(speaker())"></pre>
</div>
ko.utils.debugInfo = function (items) {
return ko.computed(function() {
return ko.toJSON(items, null, 2); // JSON.stringify(ko.toJS(timeslots), null, 2);
}
}
8. Data Services on the Client
  • DataContext, DataService and Model Mappers
  • SRP - Single Responsibility Principle
  • datacontext.sessions.getData({data: sessions});
 
It's faster to create the raw array and then stick it into an ko.observableArray, so you get only one event notification !
 
  • $.ajax() vs $.getJSON()
  • http://amplifyjs.com : Wraps Ajax calls, Always Async, Mock Data, Caching
var init = function () {
amplify.request.define('session-briefs', 'ajax', {
url: 'api/sessionbriefs',
dataType: 'json',
type: 'GET'
//cache: true
//cache: 60000 // 1 minute
//cache: 'persist'
});
}
dataservice.session.js vs mock.dataservice.session.js
$.mockJSON.data.TITLE = [
'Pick and choose from these lines',
'Bla bla bla',
  'more bla bla' 
];
 
var data = $.mockJSON.generateFrom Template({
 'sessions|100-120' : [{
'id|+1': 1,
title: '@TITLE',
code: '@LOREM', //just one word
'speakerId|1-50':1,
description: '@LOREM_IPSUM' //just some text  
  }]
});
 
//some hardcoded to do some testing against
data.sessions[0].id = 1;
data.sessions[0].title = 'Single Page Apps';
 
if (_useMocks) {
dataserviceInit = mock.dataserviceInit;
}
dataserviceInit();
 
config.useMocks(true); //can be used in your tests
Role of the Data Context
  • Caches client data (Stores data in memory)
  • Facade for Data Services (Simplified API)
  • HandlesDataRequestLogic (Cached then return else get and cache) 
datacontext.sessions.getData(dataOptions(false))
sessions = new EntitySet(dataservice.session.getSessionBriefs, modelmapper.session, model.session.Nullo); 
EnititySet has mapDtoToContext, add, removeById, getLocalById, getAllLocal, getData, updateData
 
Prototypes in Javascript
 
Upshot.js 
  • Uses DataController that inherits from ApiController
  • Can get meta data from your WCF services
  • From the asp.net team
var dataSource = upshot.RemoteDataSource({
    providerParameters: { 
        url: constants.serviceUrl, 
        operationName: "GetProducts" 
    },
    provider: constants.provider,
    entityType: "Product:#Sample.Models"
}).refresh();
Breezejs.com
manager = new entityModel.EntityManager('api/todos');
var query = new entityModel.EntityQuery()
.from("Todos")
.orderBy("CreatedAt");
query = query.where("IsArchived", op.Equals, false);
  • Uses a Metadata action method on your controller (contextProvider.Metadata());
9. Navigation between views
sammy.get("#/sessions/:id', function (context) {
vm.callback(context.params);
presenter.transitionTo( $(viewName), path );
}
  •  jQuery event delegation to the container (performance)
  • bindEventToList(config.viewIds.sessions, sessionBriefSelector, callback, eventName)
  • resharper ctrl-shift-f7 and ctrl-alt-down will find the selected word in file.
$(rootSelector).on(eName, selector, function() {
var session = ko.dataFor(this);
callback(session);
return false;
});
Adding page transitions
  • css3-transitions: http://caniuse.com or http://realworldvalidator.com
  • jQuery transitions if you want to go cross-browser
  • presenter.transitionTo();
 
Browser history
<button class="button-back" data-bind="command: goBackCmd"></button>
 
goBackCmd = ko.asyncCommand({
execute: function (complete) { router.navigateBack(); complete() },
canExecute: function (isExecuting) { return !isExecuting && !isDirty(); }
});
ko.bindingHandlers.command = { ... };
 
Cancel & Recall using amplifyjs
  • module amplify.store for client storage on old and new browsers
  • messenger module amplify.publish and amplify.subscribe
 
If changes and user clicks back button, we use toastr to display a warning message (http://nuget.org/packages/toastr).
sammy.before(/*/, function () { ... response = routeMediator.canLeave(); ... }
 
store.save(config.stateKeys.lastView, context.path);
This module wraps amplify, so it can easily be replaced with localstorage on HTML5
expires: config.storeExpirationMs in milliseconds
 
Alternatives for sammy.js
10. Saving, Change Tracking and Validation
model.person.js
self.dirtyFlag = new ko.DirtyFlag([
self.firstName,
self.lastName,
self.email,
]);
isDirty = ko.computed(function() {
return canEdit() ? speaker().dirtyFlag().isDirty() : false;
});
 
Asynchronous Commands 
(eg. disable save button once it is clicked and put status indicator)
saveCmd = ko.asyncCommand({
execute: function(complete) {
$.when(datacontext.person.updateData(speaker()))
.always(complete);
},
canExecute: function(isExecuting){
return !isExecuting && isDirty() && isValid();
}
});
<button data-bind="command: saveCmd, activity: saveCmd.isExecuting'>Save</button>
Explicit vs Implicit Save?
saveFavorite = function (selectedSession) {
if (isBusy) { return; }
isBusy = true;
var cudMethod = selectedSession.isFavorite()
? datacontext.attendance.deleteData
: datacontect.attendance.addData;
cudMethod(selectedSession,
{
success: function() { isBusy = false; },
error: function() { isBusy = false; }
}
);
}
 
Use valueHasMutated() to tell and observable its value has updated.
 
Validate Data?
 
  • Using Knockout.Validation plugin.
self.firstName = ko.observable().extend({ required: true });
self.email = ko.observable().extend({ email: true });
self.blog = ko.observable().extend({
pattern: {
message: 'Not a valid url',
params: /[@]([A-Za-z0-9_]{1,15})/i
}
});
 
validationErrors = ko.validation.group(speaker());
isValid = ko.computed(function() {
return canEdit() ? validationErrors().length == 0 : true;
});
canLeave = function() {
return canEdit() ? !isDirty() && isValid() : true;
}
11. Mobility and Responsive Design with CSS and Less
  • Balsamiq to do mocks of gui
  • Web Essentials in Extensions and Updates of Visual Studio 2012
  • http://lesscss.org
  • Less: Variables, Mixins, Nested Rules, Functions and Operations
  • Media Queries
  • Testing for multiple devices: http://jpapa.me/electricplum 
@media only screen and (max-width: 1000px)
@media only screen and (max-width: 900px)
@media only screen and (max-width: 800px)
@media only screen and (max-width: 480px)
@media only screen and (max-width: 320px)
@media only screen and (max-width: 240px)
 
@media only screen and (max-height: 768px) {
.view-list { height: 330px; }
}