Meet Spine.js – My framework of choice for client side MVC (Part 1)

Take a cup of coffee or light up your smoke, this is going to be a lengthy post, I know this is long due to some of my friends over twitter and reading after Rob Conery’s “The BackboneJS and Knockout DanceOff” I decided to start writing it finally. This post will cover a basic introduction of Spine.js, the things you have to do to integrate it with ASP.NET MVC and finally why Spine.js is my framework of choice when there are other popular alternatives for the client side development.

I will be writing all of my client side codes in CoffeeScript, if you are new to CoffeeScript then do visit CoffeeScript official site (the previous link) and there are also few more free resource like little book of CoffeeScript, Smooth CoffeeScript or blog post series of Jan Van Ryswyck which you can check. In order to compile your CoffeeScript to JavaScript inside Visual Studio you can use the new tool Web Workbench from the good guys of Mindscape or the popular open source add-in Chirpy. You can of course use the plain old JavaScript but after writing CoffeeScripts in last four months I can only say I am not going back to the plain JavaScript unless the languages features that exists in CoffeeScript are introduced in JavaScript. In short, CoffeeScript is a tiny little language which brings most of the good stuffs from Ruby, Python and probably from Perl to write highly readable and beautiful codes that compiles down to JavaScript.

Now, let me give you the summary of the sample that we will be building, we are going to build a ToDoList, but rather than creating the same old boring ToDoList, we will try to create one which would help us to apply Pomodoro technique in our life. In case if you heard the Pomodoro Technique for the first, it is a time management technique to improve you or your productivity by getting most out of your time. It is very simple technique, create a list of task which is referred as the activity sheet, then in the beginning of the day you will select the tasks (ToDoList) from the activity sheet according to your priority. There is a timer referred as Pomodoro which is 25 minutes long, you will keep working on the task from the ToDoList until it is complete or the timer rings, after the Pomodoro session you will take 3 to 5 minutes break, after the break, if the task is not complete you will start another Pomodoro of the same task and work till it is complete or the timer rings again. The loop continues until the task is complete, the only exception is after completing 4 Pomodoro session you will take a 20-25 minutes break instead of 3-5. Once the Task is complete, you will mark it as done and start working on the next. Obviously there are other parts of Pomodoro Technique like interruption, new urgent and unplanned tasks, priority changes, recording and assessments which I am skipping as we already have the few basic features that we can start working on but you can see full details in its official site.

It is time to build the initial structure of the application. I will start with an empty ASP.NET MVC application and create a new directory named assets, I prefer to keep my resource in a single directory which usually ends up like the following:

Client side folder structure
Client side folder structure

As you can see other the Spine we are also going to use

We have everything in place, so lets start building the app, but before that let me give you a brief intro on Spine.js. It is probably the only client side mvc framework where people who come from conventional server side mvc framework like Rails or ASP.NET MVC does not get confused. It has most of the common building blocks like Controller, Model (with persistence), Views, Routing which most of the server side mvc developers are already familiar with, so unlike the other frameworks working on Spine does not take too much of effort. Now, like any other mvc framework, the best place to start is the Model and with no exception Task is heart of any ToDoList app, so lets start with building the Task Model first. Other than the required Name the Task should have optional properties like Notes, Estimated Pomodoro, Planned/Unplanned, Complete Date, Created Date, Deadline etc. I will show you the Model, then I will explain you the code bit by bit.

Initial Task Model

              class Task extends Spine.Model
              
                  @configure 'Task', 'name', 'planned', 'notes', 'estimation', 'createdAt', 'deadlineAt', 'completedAt'
              

First, we are creating a Class which inherits from the Spine.Model, the extends is a CoffeeScript language feature which writes the necessary codes to maintain prototype hierarchy of JavaScript inheritance chain. Next, the sign @ means this (or self in ruby) in C#/JavaScript,so we are calling a base class method configure which declares the name and properties of the Model, the first parameter is the name of the model and the consequent values are the properties. When you write code in CoffeeScript, by default (which can be turned off but not recommended) it puts everything in an anonymous function which is not visible to outside. In order to use it outside of that scope we have export it, we are going to export all our codes under a root namespace which is always recommend instead of polluting the global, over here our root namespace is MeetSpine. So the complete code of the Task Model:


              # # Here @ is the window object, so the complete meaning of 
              # the following line is create a property in the widow object 
              # named MeetSpine if it does not exists, if exists use that
              @MeetSpine ?= {}
              
              class Task extends Spine.Model
              
                  @configure 'Task', 'name', 'planned', 'notes', 'estimation', 'createdAt', 'deadlineAt', 'completedAt'
              
              # Expose it in the root namespace
              MeetSpine.Task = Task
              

Now lets add some default values for the planned and created date property, whenever a new instance of task is create we want the created date to be the date of today and planned should be true. To add default values we are going use the following:


              @configure 'Task', 'name', 'planned', 'notes', 'estimation', 'createdAt', 'deadlineAt', 'completedAt'
              
              # Default values
              createdAt: Date.today().toString DATE_FORMAT
              planned: true
              

The Date.today is an method of Date.js and the DATE_FORMAT is just an constant for the date formatting. Now you can create a temporary html page and include the spine and task script to start playing with it. For example if I type the following in firebug it would give nice hint of the model signature:


              var task = new MeetSpine.Task;
              console.log(task);
              
Firebug
Firebug

You can even save this model, just call task.save(), now when you save it, you will find that it has new id which spine generates and the newRecord property becomes false. Obviously saving an empty model does not make any sense, so lets add some validation, as mentioned in the above all of the properties are optional except name, but when the optional properties values are set we have to ensure that those values are valid, for the estimation should not allow any negative integer, deadline date cannot be a past date. To add validation all you have to do is override the default validate method of the model which spine calls before saving a model. So lets add the validation but before that lets add the spec of the Task and as mentioned that we will be using the Jasmine for testing specing our code, I am dumping the complete code, but in real life you should never write your test/production at once, I know you know the drill.

Task Spec

              MeetSpine = @MeetSpine
              
              describe 'Task', ->
              
                  beforeEach -> @task = new MeetSpine.Task
              
                  describe 'created', ->
              
                      it 'created date is today', ->
                          (expect @task.createdAt).toBe Date.today().toString MeetSpine.DATE_FORMAT
              
                      it 'is planned', ->
                          (expect @task.planned).toBeTruthy()
              
                  describe 'validation', ->
              
                      it 'does not accept blank name', ->
                          errors = @task.validate()
                          (expect errors[0].name).toBe 'name'
              
                      it 'only accepts integer as estimation', ->
                          @task.name = 'dummy task'
                          @task.estimation = 'foobar'
                          errors = @task.validate()
                          (expect errors[0].name).toBe 'estimation'
              
                      it 'does not accept negative estimation', ->
                          @task.name = 'dummy task'
                          @task.estimation = -1
                          errors = @task.validate()
                          (expect errors[0].name).toBe 'estimation'
              
                      it 'does not accept past date as deadline', ->
                          @task.name = 'dummy task'
                          @task.estimation = 10
                          @task.deadlineAt = (Date.parse 'yesterday').toString MeetSpine.DATE_FORMAT
                          errors = @task.validate()
                          (expect errors[0].name).toBe 'deadlineAt'
              
                      it 'no errors if valid', ->
                          @task.name = 'dummy task'
                          @task.estimation = 10
                          @task.deadlineAt = (Date.parse 'tomorrow').toString MeetSpine.DATE_FORMAT
                          errors = @task.validate()
                          (expect errors).toBeUndefined()
              

Now the actual code.

Task Validation

              MeetSpine.DATE_FORMAT = DATE_FORMAT = 'yyyy-MM-dd'
              
              DATE_EXPRESSION = ///^\d{4}[-]\d{2}[-]\d{2}$///
              
              class Task extends Spine.Model
              
                  @configure 'Task', 'name', 'planned', 'notes', 'estimation', 'createdAt', 'deadlineAt', 'completedAt'
              
                  # Default values
                  createdAt: Date.today().toString DATE_FORMAT
                  planned: true
              
                  validate: ->
              
                      errors = []
              
                      addError = (name, message) ->
                          error = { name: name, messages: [] }
                          error.messages.push message
                          errors.push error
              
                      addError 'name', 'Name cannot be blank.' unless @name
              
                      if @estimation
                          estimation = parseInt @estimation, 10
                          addError 'estimation', 'Estimation must be a positive integer.' if (isNaN estimation) or estimation < 1
              
                      if @deadlineAt
                          if DATE_EXPRESSION.test @deadlineAt
                              addError 'deadlineAt', 'Deadline cannot be in past date.' if ((Date.parse @deadlineAt, DATE_FORMAT).compareTo Date.today()) < 0
                          else
                              addError 'deadlineAt', "Unable to recognize the date format."
              
                      errors if errors.length
              
              MeetSpine.Task = Task
              

In the above we are creating a list of key/value pair, the key is the name of the property and its associated values are the messages of the property when it fails the validation test. One thing you may have noticed, with CoffeeScript there are no usage of curly braces, curly braces are only used in rare occasions like inline object notation, parentheses are also used in special cases like calling multiple methods at the same time, you can also invert the if statement, unless is supported too and like ruby the last statement is always get returned to the calling method, so you never have to use the explicit return unless you are breaking in the middle of the method. Another important symbol in CoffeeScript is the –> (dash rocket) which generates as function, for example, for the following CoffeeScript:


              sayHello = (toWhom) -> console.log "Hello #{toWhom}"
              

it will generate the following JavaScript:


              var sayHello;
              sayHello = function(toWhom) {
                return console.log("Hello " + toWhom);
              };
              

as you can see the CoffeeScript also has ruby like string interpolation support and like the dash rocket there is another symbol that we as a .NET developer are already familiar with the => (Fat Rocket), Fat rocket is used to function binding, it serves the same purpose as jQuery’s proxy, which you may have already familiar with. We will see the fat rocket in action later in this post. Now if you run the spec runner of Jasmine, it will show you the nice little output like following:

Jasmine Spec Runner
Jasmine Spec Runner

Now we have our initial model, lets start building the UI, since this will be a single page application, we will have only one action method which would deliver the necessary markup and resources to the browser. Lets add a ASP.NET MVC Controller Dashboard and it will only contain the Index method which should return the default view, also change default route so that it points to this action.

Dashboard Controller

              namespace MeetSpine
              {
                  using System.Web.Mvc;
              
                  public class DashboardController : Controller
                  {
                      public ActionResult Index()
                      {
                          return View();
                      }
                  }
              }
              

Now its time to integrate the jQuery Mobile, first the layout view:

Layout View

              <!DOCTYPE html>
              <html lang="en">
              <head>
                  <meta charset="utf-8" />
                  <meta name="viewport" content="width=device-width, initial-scale=1" />
                  <link href="@Url.StyleSheet("jquery.mobile")" rel="stylesheet" />
                  <script src="@Url.JavaScript("lib/jquery")"></script>
                  <script src="@Url.JavaScript("lib/jquery.mobile")"></script>
                  <title>Meet Spine</title>
              </head>
              <body>
                  @RenderBody()
                  <script src="@Url.JavaScript("lib/spine")"></script>
                  @RenderSection("bottomScripts", false)
              </body>
              </html>
              
Dasboard Index

              <div id="main">
                  @Html.Partial("_ActivitySheet")
                  @Html.Partial("_TaskEditor")
              </div>
              @section bottomScripts {
                  <script src="@Url.JavaScript("lib/jquery.tmpl")"></script>
                  <script src="@Url.JavaScript("lib/date")"></script>
                  <script src="@Url.JavaScript("models/task")"></script>
                  <script src="@Url.JavaScript("controllers/activitysheet")"></script>
                  <script src="@Url.JavaScript("controllers/newtask")"></script>
                  <script src="@Url.JavaScript("dashboard")"></script>
              }
              

As you can see that we are using two partials, the activity sheet will be used to list tasks and the task editor will be used to add/edit tasks. Here is the markup of the activity sheet:

ActivitySheet partial

              <section id="activity-sheet" data-role="page">
                  @Html.Partial("_Header", "Activity")
                  <div data-role="content">
                      <a href="#new-task" data-role="button" data-icon="plus" data-iconpos="right">New task</a>
                      <ol id="list"></ol>
                  </div>
                  @Html.Partial("_Footer")
                  <script id="template" type="text/x-jquery-tmpl">
                      <li data-id="${id}"><a href="#">${name}</a></li>
                  </script>
              </section>
              

Nothing complex, we are going to use the jQuery Mobile listview to show the tasks, currently it is going to show only the task name, later on we will add more fields. In order to render each listview item, we are going to use jQuery template. In the top of the listview there is a link which should show the task editor for creating the new task, we are also going to handle the edit task later on. Next, is the task editor:

Task editor partial

              <section id="new-task" data-role="page">
                  @Html.Partial("_Header", "New task")
                  <div data-role="content">
                      <form id="task-editor-form" action="" method="post" data-ajax="false">
                          <div data-role="fieldcontain">
                              <label for="task-name"><em>*</em> Name</label>
                              <input id="task-name" name="name" type="text" />
                              <span class="field-validation-error"></span>
                          </div>
                          <div data-role="collapsible" data-collapsed="true">
                              <h3>Optional</h3>
                              <div>
                                  <div data-role="fieldcontain">
                                      <label for="task-notes">Notes</label>
                                      <textarea id="task-notes" name="notes" rows="2" cols="4"></textarea>
                                      <span class="field-validation-error"></span>
                                  </div>
                                  <div data-role="fieldcontain">
                                      <label for="task-estimation">Pomodoros (estimated)</label>
                                      <input id="task-estimation" name="estimation" type="range" min="0" max="25" />
                                      <span class="field-validation-error"></span>
                                  </div>
                                  <div>
                                      <label for="task-deadline-at">Deadline</label>
                                      <input id="task-deadline-at" name="deadlineAt" type="date" />
                                      <span class="field-validation-error"></span>
                                  </div>
                              </div>
                          </div>
                          <div data-role="fieldcontain">
                              <button data-icon="check" data-iconpos="right">Save</button>
                              <a data-role="button" data-icon="refresh" data-iconpos="right" data-rel="back">Cancel</a>
                          </div>
                      </form>
                  </div>
                  @Html.Partial("_Footer")
              </section>
              

In the above first we are adding data-ajax=false which tells the jQuery mobile not to handle the form post, instead our controller will be responsible for handling it. Although, the form tag is not required, but there are extra benefits of using the form tag, which we will see shortly. Next, we are putting all of the optional fields in a collapsible content, hiding it by default. There is also an additional span to show the validation error message associated with that input. Now if you run it, you will see the following screens:

Initial UI
Initial UI

Now, lets start writing the controllers that will glue our model with the above views. Here is the initial version of the NewTaskController which will handle the task editor.

NewTask controller

              MeetSpine = @MeetSpine
              Task = MeetSpine.Task
              
              class NewTaskController extends Spine.Controller
              
                  @elements: { 'form': 'form' }
                  @events: { 'submit form': 'create' }
              
                  constructor: ->
                      super
              
                      Task.bind 'error', @showErrors
                      Task.bind 'create', -> $.mobile.changePage $ '#activity-sheet'
              
                  create: (event) ->
              
                      event.preventDefault()
              
                      fields = @form.serializeForm()
              
                      if fields.deadlineAt
                          # only parse the date if it is valid otherwise let the validation handles it
                          deadlineAt = (Date.parse fields.deadlineAt)?.toString MeetSpine.DATE_FORMAT
                          fields.deadlineAt = deadlineAt if deadlineAt?
              
                      task = new Task fields
                      task.save()
              
                  activate: -> # ommitted
              
                  reset: ->  # ommitted
              
                  hideErrors: -> # ommitted
              
                  showErrors: (task, errors) => # ommitted
              
              MeetSpine.NewTaskController = NewTaskController
              

In the above, we are first creating the Controller inheriting from Spine.Controller. Next, we are declaring the dom elements that we need to access in the controller, in this case we only need the form element, the elements are declared as a hash, the key is the selector that Spine will use to get a reference of that element and value is the name of the variable that we will use in the controller. As you can see, as a selector we are only using the element tag, does it mean that Spine will return all of the forms of the document? Of course not, The Spine controller is always tied with a specific dom element. You can declare the dom element just like the elements, for example, el: $ '#someSection' or you can pass the element when creating the controller. If Spine could not find the specified element it will by default create a div as the root element, you can change this behavior by specifying the tag name like tag: 'section' just like the elements declaration. When Spine resolves the elements it uses the root element as context, so the elements that you will declare should be the child element of the root. You can also completely skip the elements collection declaration and write it yourself, for example, we can refer the same from in our code like @form = $ 'form', @el. The events is just like the elements but it is used to automatically bind the element events. The format of declaring event is, first you need to specify the name of the event, next a space separated selector, behind the scene it uses jQuery (you can also use Zepto) delegate to bind the method which is specified as the value of the key value pair. In this case we are binding form submit event with create the method. Next, in the controller constructor we subscribing two Task Model event error and create, Spine Model has quite a number of events like refresh, change, create, update, destroy also few beforeXXX, you can subscribe to individual instance or the class, in our case we are subscribing to error and create, the error is fired when the validation fails, in the handler Spine provides the instance and the errors which we created previously in the validate method of Task. Next, we are also subscribing the create event so that we can load the task list when a task is successfully created. The create method is the form submit handler, remember we mention previously that using the form has an advantage, the advantage is that we can create a tiny jQuery plug-in which serialize the form fields in key value which we can easily pass to the model constructor. Here is the code of the jQuery plug-in which does the serialization:

Form serializer

              $ = jQuery
              
              $.fn.serializeForm = ->
              
                  results = {}
                  $.each ($ @).serializeArray(), (index, item) -> results[item.name] = item.value
                  results
              

Next, in the create we are checking whether the deadline date is specified, if it is specified we are trying to convert to our supported format with the Date.js and creating the Task model and saving it. Now if the save is successful it fires the model create event (as we have subscribed this event in the constructor of the controller) and we are automatically redirected the task list view, but when it fails due to validation the showErrors method gets fired. The showErrors is just helper method which scans the form filed with matching error name and shows the associated span with the errors message. Here is the code of the showErrors.


              showErrors: (task, errors) =>
              
                  firstError = null
              
                  for error in errors when error.name? and error.name isnt ''
                      for message in error.messages when message? and message isnt ''
                          fieldsSelector = "input[name=#{error.name}], 
                              textarea[name=#{error.name}], select[name=#{error.name}]"
                          currentError = ($ fieldsSelector, @form).first()
                          firstError = currentError unless firstError?
                          showError currentError, message
              
                  $.mobile.silentScroll firstError.parent().children(validationErrorCssClass).offset().top if firstError?
              

If you check the above code carefully you will find few CoffeeScript language features. First, we can use condition with when while looping through an array which filters the array based upon those specified condition, next the ? operator, it calls existential operator, it returns true when the value is not null or undefined, you can also use it method chaining. For example: foo?.bar?.baz, the nice thing about you never have to check null, if any of the calls return null it does not execute the next call.

There are few more method in the new task controller, which we will come back letter. Now lets write the activity sheet controller. Here is the initial version:

ActivitySheet controller

              MeetSpine = @MeetSpine
              $ = jQuery
              
              Task = MeetSpine.Task
              
              class ActivitySheetController extends Spine.Controller
                  @elements:
                      '#list': 'list'
                      '#template': 'template'
              
                  constructor: ->
                      super
                      Task.bind 'refresh change', @render
              
                  activate: ->
                      $.mobile.showPageLoadingMsg()
                      Task.fetch $.mobile.hidePageLoadingMsg()
              
                  reset: ->
              
                  renderOne: (task) -> @list.append (@template.tmpl task)
              
                  render: =>
                      @list.empty()
                      @renderOne task for task in Task.all()
                      @list.listview 'destroy' if @list.jqmData 'listview'
                      @list.listview inset: true
              
              MeetSpine.ActivitySheetController = ActivitySheetController
              

We already know what elements is used for, so lets move to the constructor, similar to new task controller we are again subscribing the Task refresh and change events, just like jQuery you can bind multiple events to a single handler by separating it by a space. The refresh event get fired when we call the fetch of the model which we will see in a moment, the change event is fired when a model is created, updated and deleted, you can of course subscribe to these granular level events instead of change so that you do not have to re-render the whole list when an item is changed. Next, in the render we are first clearing the existing content and using the jQuery template to render each item. Next, we are making our list as jQuery Mobile listview, but before creating the listview we are checking whether it has been previously created, if created we are destroying it and then re-creating it. Now, lets move to the activate method, first we showing the jQuery Mobile built-in spinner and after that we are fetching the tasks, when the fetched we are hiding the previously shown spinner. One thing we may have noticed that activate is not at all called in this controller, which is our next topic, jQurey Mobile integration.

Up to this point we have created all the building blocks, now it time to bring those into action. Here is the bootstrapping code that does it:

Bootstrapping

              exports = @
              MeetSpine = @MeetSpine
              $ = jQuery
              
              createController = (selector, klass) ->
                  element = $ selector
                  controller = new klass el: element
                  element.data 'controller', controller
              
              getController = (element) -> element.data 'controller'
              
              $ ->
                  createController '#activity-sheet', MeetSpine.ActivitySheetController
                  createController '#new-task', MeetSpine.NewTaskController
              
                  viewsContainer = $ '#main'
              
                  viewsContainer.live 'pageshow', (event, ui) -> 
                      (getController ui.prevPage)?.reset()
              
                  viewsContainer.live 'pagecreate', (event) ->
                      (getController ($ event.target))?.activate()
              

Lets start with bottom-up, the $ –> is shortcut of the jQuery document.ready. In the document ready we are using the helper method createController to create our controllers. The createController accepts a selector which the controller would associated with the controller (remember the @el in the above section that we discussed) and the class name of the controller. The createController first resolves the selector to get the actual dom element reference then creates the controller with the resolved element, then it uses the jQuery data to store the controller in the dom element. Next, jQuery Mobile has quite a few built-in events that get fired when page is created or changed, we are hooking those events to get the reference of the controller and then calling the activate or reset method of those controllers.

Now, we have fully functional Activity and New-task UI, you can run it in the browser and start creating tasks. But once you close your browser and reopen, everything is lost. So everything was stored in memory, but we need the persistence support. I was blown away when I initially saw how easy to add the persistence support to spine,  just add the local.js from the Spine downloads in your script list, then open the Task Model and add the following line before you are exporting to root namespace:

Adding local storage persistence

              Task.extend Spine.Model.Local
              MeetSpine.Task = Task
              

Now, all your tasks will be stored in the html5 localStorage. Spine extend and include works just like ruby, if you are not familiar with ruby, let give you the short detail, ruby has nice support of mixin, when you use extend it will import the module's methods as class methods (in C# it is static) and include adds as instance methods, which you can further enhance you class without using the inheritance.

That’s it for today, next week I will show how to integrate it with ASP.NET MVC and the rest of the topic that I mentioned at the top. You will find the complete code in the following link.

Download: MeetSpine-Part1.zip

Shout it

Comments

blog comments powered by Disqus