Thursday, February 21, 2013

Coffeescript And Backbone.js - Convert Reminders App

Coffeescript And Backbone.js - Convert Reminders App

Hey all,

Gonna be a bit late on the post regarding callbacks in javascript.  Ran into a little time consuming issue while updating my gift list app and haven't had time to work promises into it yet.  That should be on it's way soon but as Space Hog would say "In the mean timeeeeee"... More Coffeescript, this time with some Backbone.js.

I've been meaing to change my Messages and Reminders app from vanilla javascript and jQuery to something in Coffeescript, Backbone.js and jQuery(w/UI).  Why you ask?  Well the app is essentially a single page web app already and though the initial version is working fine from the front-end perspective right now, some recent changes I wanted to add showed an increasing need for a bit more organization and robustness in the code so...

If you haven't seen Messages and Reminders, check it out, sign up, play around with it, a new version should be out before the end of the month.  So basically we have a web app that allows you to send messages back and forth between yourself and other members as well as add reminders to yourself or convert a message to a reminder.  Pretty simple stuff.  Lets take a look at some of the code:


class Message extends Backbone.Model 
  defaults: 
    to : "Me" 
    from : "You" 
    title : "Hey" 
    body : "What's up" 
    date : "02/18/2013"

Models are the basic building blocks of Backbone.js.  Here we are creating a Message class which will extend the base model class and then we add some defaults to that new class.  By extending the model class we get all kinds of great utility methods including a getter and setter for our properties and other great methods.  The defaults shown here are the default values of our model properties if we instantiate an instance and don't set the properties at that time.

If we do this


newMessage = new Message() 
newMessage.to ## 'To'

or if we do this


newMesage = new Message({to: 'You'})
newMessage.to ## 'You'


Ok, hope that is clear as mud.
Next we are going to setup a collection which will hold our models


class MailBox extends Backbone.Collection

Wow is that it for a collection? Yup. Actually for the purposes of this article we could even write

reminders = new Backbone.Collection()

So why extend if we could just instantiate a new instace of a collection. Mostly for future issues including adding some error checking on the collection.
Something I should mention just occured to me while discussing collections. Backbone has one hard dependency, either Underscore.js or lo-dash. Both do essentially the same thing, lo-dash is smaller and faster whereas Underscore is larger but maintains some support for older browsers. Either will work. The reason for my sudden memory flash of Undercore is that Backbone proxies 28 iteration functions to collections such as forEach, map, reduce, min, max and more. This makes collections super useful in any data heavy web app. I should note that collections are not the only reason Underscore is a requirement for Backbone so don't assume just because you are not planning on using the iteration functions you won't have to include Underscore.


Next up we get into some views


class MessageView extends Backbone.View 
  tagName: "li" 
  className: "message" 
   model: Message 
   temp: "
      <%=to%>
      <%=from%>
      <%=title%>
      <%=body%>
      <%=date%>
   " 
 initialize:() => 
   @render() 
 render: () => 
 if @temp? && @model? 
   messageTmp = _.template(@temp) 
   @$el.append(messageTmp(@model.attributes)) 
 return @


Views are what we use to display data in a Backbone app.  They can reflect a model, collection or just a be a static representation of whatever you want.  Here we have the view that is created when a new Message model is added to a MailBox collection.  A few things to note about the view.  I am using the tagName and className properties to describe what type of element I want created, so a list element with a class of message.  The temp property is actually a template string that we will compile down and use to display the model data.  Here we are using Underscore templates but we could use whatever templating we wanted.  Then we have the initialize function which is our constructor, this function is called when you create a new instance of your model so whatever setup needs to be done do it here.  In this case I am simply calling into the render function.  The render function's default implementation is a no-op, it's provided to be overridden to contain whatever needs to be done to get your view properly rendered.  So in this render we compile our template down and send in @model.attributes which is a property provided that cotains the model data as a hash or in this case {to:"To", from: "From", title: "Title"..etc}.  We then append the string to the views @$el which is a wrapper function that uses jQuery or Zepto if they are available on @el which is the current element.  We then return @ (javascripts this) which is a common practice to allow method chaining.  Why did we call render from initalize, just cause.  It basically lets me simplify the calling code from new MessageView({model:model}).render() to new MessageView({model:model}).


class MailBoxView extends Backbone.View 
  tagName : "ul" 
  className : "mail-box" 
  initialize:() => 
    @listenTo(@collection, "add", @render) 
  render: () => 
    if @collection? && @collection.length 
      newMessage = @collection.at(@collection.length - 1) 
      @$el.append(new MessageView(model:newMessage).el) 
    return @

This is the code for the MailBox collection view.  Notice in the initialize function(or constructor) we call @listenTo(@collection, "add", @render).  listenTo is from Backbone events.  It takes a collection or model, an event type and a function.  When the listening event type is fired by the collection or model the callback function executes.  So here, when a model is added to the views collection, we execute the render function of the view.  So where does this collection come from? Look further down the code to the MailBoxView instansiation reminderView = new MailBoxView(collection: reminders).  We add the collection reminders here to the new MailBoxView.  Why not add the collection as a property?  De-coupling.  We don't want this particular collection tied to the view in that way, we want the backbone events to do what they are there for which at least in part is helping our different application pieces communicate.  There is way more on events in the Backbone documentation, you can even create and use custom events which can be extremely useful.


class CreateMessageView extends Backbone.View 
  events: "click .submit": "addMessage" 
  addMessage: () => 
    to = @$el.find(".to").val() 
    from = @$el.find(".from").val() 
    title = @$el.find(".title").val() 
    body = @$el.find(".body").val() 
    date = @$el.find(".date").val() 
  newMessage = new Message( 
    to: to from: 
    from title: 
    title body: 
    body 
    date: date 
  ) 
  @collection.add(newMessage) 
  @render() 
  return 
 render: () => 
   @$el.find("input:not(button)").val("") 
   return @ 
 initialize: () =>

Alright! More events!  Here we are adding the events using the events property on the view.  This is a form of event delegation which you may also be familar with from jQuery, which would look like $("#createMessage").on("click", ".submit", @addMessage).  Event delegation speeds up events by having the event bubble only as far as the containing element.  A problem with delegation in Backbone is that if the object is destroyed without properly removing the event binding first, you can create a memory leak[citation needed].  So keep that in mind.  Now back to what's going on here.  On clicking the submit button we call addMessage which get's the values of the input fields and creates a new message (newMessage) of the type Message with the field values.  We then add the new model to the collection, which remeber will cause the MailBox view to render the new Message in it's ul element because it is listening for changes in this collection.  So where is the "#createMessage" element from?  Well we bind that directly to an existing DOM element that was rendered when the page was created

createView = new CreateMessageView( 
 collection: reminders 
 el: document.getElementById("createMessage") 
 )

Notice again the collection being set to reminders and here we see that el is explicitly bound to the DOM element instead of creating an element to insert data into.


reminderView = new MailBoxView( 
 collection: reminders 
 ) 
$("#reminder").append(reminderView.el)

The final line ties it together by creating the reminder view and attaching it to the DOM at the "#reminder" element.

You can see the code here

That was a bit long for the type of overview I wanted to do but I think I covered the basics.  Next time I talk at length about Coffeescript I'm hoping to do some routing and show some use of Backbone.sync.

No comments:

Post a Comment