A demo Todo explanation with the Backbone-on-Rails gem, that is the background of this discussion, is ready for download here: https://github.com/mulderp/Backbone-on-Rails-todoDemo
1. From server-side to client-side programming
The Rails framework is well known for its nice interaction of views, controllers and models. How these components work with HTTP and a database, is typically explained using a blog application. Client-side programming poses slightly different programming problems. Client-side programming is influenced by intricacies of web browsers and the DOM, which includes presentation details (HTML/CSS) and logic (Javascript). Similarly as JQuery provides a better API to manipulate simple structues in the DOM, the goal of Backbone is to provide a language that facilitates so called "data-driven programming", where a high amount of data changes and events in the DOM are becoming easier to deal with on the client-side.
2. Entering client-side programming
Typically, client-side programming is explained with the help of a Todo application. There is great demo of a Todo application from Jérôme Gravel-Niquet , here:
http://documentcloud.github.com/backbone/examples/todos/index.html
The HTML of a Todo-List is rather simple, and consist of a 'list' that contains a number of 'todos'. For those, who are new to client-side programming, a different toolset is helpful in solving programming problems. These tools and debugging tricks are:
- jsfiddle: An interactive sandbox to play with HTML, CSS and Javascript code. Libraries, such as backbone, can be included too
- jslint: This tools helps in finding errors in Javascript or JSON data
- the browser console like firebug in Firefox or in the web developer tools of Chrome: The console helps in evaluating small pieces of code and variables, and breakpoints can help to understand which context and scope is currently active.
- console-log: With the console.log() function in Javascript, it's possible to monitor the correct flow of data in the application
- http://js2coffee.org/ : When using coffeescript, as is advised from Rails 3.1 on, it's helpful to understand the conversion of coffeescript into Javscript
3. Fetching data from the server
The main mechanism in backbone to fetch data is by extending a Backbone.Collection For a todo list, where 'todos' should be fetched from the server, or written to the server, a Todos collection might look like this:
class BackboneOnRailsTodo.Collections.Todos extends Backbone.Collection
model: BackboneOnRailsTodo.Models.Todo
url: '/todos'
The important piece here is the 'url'. Coming from a Rails environment, where an 'url' is only defined in the router, this might be a bit confusing, however, 'routes' in Backbone have a different usage, namely to interact with a client-side URL that is marked by a hashtag (e.g. http://mydomain/todos#list ). As a first test, to see that your collection is working, you can use the browser console and fetch some simple todo json from the server.
This could look like this:
Note, the 'new' and '()' in the statement above are important, because otherwise, you get some wrongly initalized object. You can then fill your collection, with todos.fetch()
4. Rendering data with help of views and templates
Once, data is available, render it with help of views
a) Views are some kind of containers, where you put data and recipes (templates), how to render the data. In the Backbone-on-Rails gem, you can easily use the ECO type template, which is some kind of ERB in the coffeescript context. Note, you must address view variables with help of @ from the view, like so:
<%= @todo.get('content') %>
b) Views must be initialized with a model or collection hash, typically looking like this:
view = new BackboneOnRailsTodo.Views.TodoListIndex(collection: @todos)
c) Views can be rendered, and for this, the render() function is called together with .el(), that actually gives the HTML of the rendered element
d) In views, unlike as in Ruby, there is not much syntactic sugar by default. A function like 'each' is given by the underscore library, but it's even easier to use the construct for .. in from backbone
5. When to render views?
a) The rendering of view can easy be tested for development purposes, by using the browser console.
preload / 'reset' function
As the rendering of a view, needs to have a model or collection as input, a collection must be initialized first:
todos = new BackboneOnRailsTodo.Collections.Todos()
todos.fetch()
Then,
view = new BackboneOnRailsTodo.Views.TodoListIndex({collection: todos})
The rendering of a view can be tested with
view.render()
b) In our Todo application we work with 2 views. Similar to the demo Todo app by Jérôme NG as above:
// Todo Item View --> The DOM element for a todo item...
var TodoView = Backbone.View.extend({
and
// The Application --> Our overall **AppView** is the top-level piece of UI.
var AppView = Backbone.View.extend({ .. })
c) For the doing the first, startup rendering of a view, a Backbone router can be instructed to initialize the view:
class BackboneOnRailsTodo.Routers.TodoLists extends Backbone.Router
routes:
'': 'index'
initialize: ->
@todos = new BackboneOnRailsTodo.Collections.Todos()
@todos.fetch()
index: ->
view = new BackboneOnRailsTodo.Views.TodoListIndex(collection: @todos)
$('#todo-list').html(view.render().el)
There are other ways to initialize views, such as synchronous or asynchronous loading of data and/or view templates. In the example above, the data is provided asynchronous from server side.
6. Handling user interaction
So far, the explanation above can be used to fetch data from the server, and to render it. However, in a rich-client application, events in the DOM, that are issued by user interactions (mouse click, key pressed, etc. ) are importat too.
For having user interaction in the application event binding to DOM elements is used. Event binding events uses either Backbone or JQuery event delegation ('bind' or 'on' functions). To lookup the right DOM elements, the delegation must be bound in the correct context.
This can be a cause for confusion as discussed here:
- http://stackoverflow.com/questions/5125958/backbone-js-views-delegateevents-do-not-get-bound-sometimes
- http://stackoverflow.com/questions/4909564/backbone-js-why-isnt-this-event-bound
- http://stackoverflow.com/questions/9304625/in-backbone-js-how-do-i-bind-a-keyup-to-the-document
In the demo Todo application I use the following strategy to binding to events in the TodoListIndex:
initialize: ->
@collection.on('reset', @addAll, this)
@collection.on('add', this.addOne, this)
$('#new-todo').on "keypress", {collection: @collection}, @keyTodoInput
Note, the 'add' event works by using a backbone event binding. The 'keypress' event must use a JQuery binding, because the input form is outside the scope of the TodoIndex view.
The event that a new todo is added, is processed with:
addOne: (todo) ->
console.log(todo)
view = new BackboneOnRailsTodo.Views.Todo({model: todo})
$("#todo-list").append(view.render().el)
The event that an input is made, is processed with:
keyTodoInput: (e) ->
# console.log(event.type, event.keyCode)
return if (e.keyCode != 13)
return if (!this.value)
console.log(e.data.collection)