So far I've got an idea for Backbone through Codeschool's Anatomy of Backbone 1 & 2. Working alongside these 2 Railscasts has been a great next step.
-
Main Backbone app
window.Raffler
app/assets/javascripts/raffler.js.coffee
The main app file. Namespaces our backbone app. Initializes it. Initialization means that it starts the history and instantiates router. -
Entries router
Raffler.Routers.Entries
app/assets/javascripts/routers/entries_router.js.coffee
Sets routes. Fetches collections or models. When the route is called, the router instantiates a view, passing the collection into it, and rendering it into the DOM via the#container
div. -
Entries view
Raffler.Views.EntriesIndex
app/assets/javascripts/views/entries/entries_index.js.coffee
Specifies a template. Renders that template with the data that was passed from the router, placing the rendered contents in view'sel
property.
In@collection.on('reset', @render, this)
,reset
would not get fired by@collection.fetch()
anymore as of Backbone 1.1 (see change log). The quickest fix is to useCollection.fetch({ reset: true })
. -
template
JST['entries/index']
app/assets/templates/entries/index.jst.eco
<% for entry in @entries.models: %> <li><%= entry.get('name') %></li> <% end %>
-
Creating resources
Creating resources starts with an html form. We listen for the event from the view. When a users clicks 'Add' or pressesEnter
on the form with form id#new_entry
, the event triggered is'submit #new_entry'
. The view'sevents
hash routes this event to a function we callCreateEntry
, which takes the argumentevent
. -
CreateEntry: (event) ->
Create the new resource via its collection with@collection.create
, giving it the params grabbed from the elementname: $('#new_entry_name').val()
. -
Updating the view
Update the view by listening to the collection for its 'add' event.@listenTo(@collection, 'add', @appendEntry)
. TheappendEntry
function instantiates a newRaffler.Views.Entry
, passing it the newly created resource before appending the new view into the#entries
element in its ownel
.I'm not sure exactly why, but there is an issue with context here, and
appendEntry
needs to have an explicitthis
context provided. In Coffee this is done by defining the function with the fat arrow=>
. In plain js,this
is passed as the last argument. -
Updating the view - a refinement
Here we want to be able to append a new item rather than re-render the entire view when the item is added. First we make a new view and a new template for an individual entry.
/app/assets/views/javascripts/entries/entry.js.coffee
/app/assets/templates/entries/entry.jst.eco
In the index view'srender
function, we loop through the collection and callappendEntry
for each one. -
Passing server side validations to the front-end
Collection.create
has callback hooks forsuccess
anderror
. On success, we reset the form to clear the old input value. On error, we callhandleError
to display the server's error message on the DOM. To do this, first we checkresponse.status == 422
, then we use jQuery's$.parseJSON
to get the message values and add them to an#error
div.
There remains an issue that the collection'sadd
event has already been fired before server validations have returned, andappendEntry
has been called with an empty model. To prevent this, we add the paramwait: true
onCollection.create
. -
Drawing a winner
Put the click listener in the EntriesIndex view's events hash.
'click #draw': 'drawWinner'
@drawWinner
calls drawWinner function on the collection@collection.drawWinner()
In the collection weshuffle()
the contents and grab the first one to be our winner, updating its attributes towinner: true
.
We updateentry.jst.eco
so that ifwinner: true
, we'll add 'WINNER' next to the entry's name, and we'll put a class on it so we can style it or highlight it later.<% if @entry.get('winner'): %> <span class="winner">WINNER</span> <% end %>
The
:
added to the end of the if statement is for dealing with Coffee's whitespace sensitivity. -
Updating the view with 'winner'
In theentry
view, we listen to the model for its change event to trigger its render function with the updated model. -
Highlighting the latest winner
After updating the winner model, we'll trigger a custom event.
winner.trigger('highlight')
Returning to theEntry
view, we listen to the model for that 'highlight' event to call ourhighlightWinner
function.
@highlightWinner
then callsaddClass(.highlight)
on the latest winning entry (andremoveClass(.highlight)
on any other winners in the DOM.) -
Refining and refactoring
ThedrawWinner
function, defined on the collection class, makes several calls on the winner model, so we refactor that code into theEntry
model instead. For this to work, the collection class must have the model hash defined and pointing toRaffler.Models.Entry
. -
Bootstrapping
We currently callfetch()
on the collection after instantiating it in the router, causing an additional call to the server. To reduce that to a single call, we can put the JSON data in the DOM instead using a html5data
attribute. To get the data into the DOM, we use this code in the Rails view:
<%= content_tag 'div', 'Loading...', id: 'container', data: { entries: Entry.all } %>
Then in the router, after constructing our collection we can call@collection.reset($('#container').data 'entries')
instead of.fetch()
.
For this refinement to work, we need to tweak ourappendEntry
function in the collection. Written this way:$('#entries').append(view.render().el)
, the code does not work becauseappendEntry
is getting called before the #entries container has been inserted into the DOM. We have to add an@
to the start of that line so that it looks inside itsel
for the #entries container instead of trying to find it in the DOM. Lesson in scoping: Go through the view, not the DOM. There is still another scope issue becauseappendEntry
is called from@collection
and not@
, therefore the scope ofthis
is changed inappendEntry
. Hence,appendEntry
needs to be defined with a fat arrow.
-
Inspecting server response errors in Chrome.
Developer Tools -> Network
Click on 4xx response, preview tab.
Why did that take so long to figure out?! -
Resetting forms with jQuery.
Ryan Bates used the jquery syntax:
$('#new_entry')[0].reset()
Standard Javascript syntax:
getElementById('#new_entry').reset();
jQuery blog suggests
$('#new_entry').trigger('reset')