Recently, I have been involved in a couple of projects where we use a lot of Backbone.js, and even though I'm still learning, I can definitively help you out with the basics on how to code a basic CRUD with Backbone and Rails.
So, if you are not familiar with the framework and want to learn how to use backbone in your projects go ahead and check out this little tutorial where we'll be creating a basic blog.
We'll be using coffeescript. If you are not familiar with coffescript, take a look at this post from one of my fellas.
Before we start let me explain some basic concepts about backbone and it's components:
- Model
This is like regular rails models, where all our data is stored and a large part of the logic.
- Collection
Backbone collections are simply an ordered set of models.
- View
If you think this is the place where you put all the layouts like in rails, well its not. This is where you bind all the Javascript events to the layout objects.
- Template
This one is simple, templates are like a rails views where you put all your layouts.
- Router
This one handles the browser url box so we can trigger events when a route matches right after a hash tag (#).
First let's create our rails project:
rails new blog
Now we have to include Backbone within our rails application, but, instead of downloading the file and put it into the assets folder manually, we are going to install the backbone-rails gem, so, now let's open our Gemfile and type the following and don't forget to bundle afterwards.
gem "rails-backbone"
The gem itself comes packed with some a fancy scaffold generator that almost does all the work for you, but, we'll put this aside and start everything from scratch, so, lets start with this generator that creates the basic structure for our backbone files:
rails g backbone:install
Now, let's create a rails scaffold and a backbone model for our post.
rails g scaffold Post title:string content:string
rails g backbone:model Post
rake db:migrate
Now go and open the post.js.coffee you will see that both a model and a collection were created now lets focus on the properties of those objects.
-Blog.Models.Post
paramRoot: 'post'
- This is a property used by the gem, its not a backbone property. It works as a namespace, when the request is sent, your parameters are going to look like this
{"post" => {"title"=>"name", "content"=>"title"}}
defaults:
- This property just sets default values for the model in case we need to, since we don't need any default values lets just get rid of it.
-Blog.Collections.PostsCollection
model: Blog.Models.Post
- This property tells our collection what kind of objects it can have.
url: '/posts'
- This property tells the collection where to go when when we execute the fetch method of the collection.
Now lets create a router to handle or url.
touch app/assets/javascripts/backbone/routers/posts_router.js.coffee
and edit it with the following:
class Blog.Routers.PostsRouter extends Backbone.Router
initialize: (options) ->
@posts = new Blog.Collections.PostsCollection()
@posts.reset options.posts
routes:
"index" : "index"
"new" : "newPost"
":id" : "show"
":id/edit" : "edit"
".*" : "index"
index: ->
@view = new Blog.Views.PostsIndexView({collection: @posts})
newPost: ->
@view = new Blog.Views.PostsNewView({collection: @posts})
show: (id) ->
post = @posts.get(id)
@view = new Blog.Views.PostsShowView({model: post})
edit: (id) ->
post = @posts.get(id)
@view = new Blog.Views.PostsEditView({model: post})
This router would match what's in the url box and then would call the method linked to that route e.g. when the url is /:id/edit it will call the edit method inside the router.
Now that we've set up our router, its time to create templates and views for each action, so, just go ahead and copy this in your terminal:
mkdir app/assets/javascripts/backbone/views/posts
mkdir app/assets/javascripts/backbone/templates/posts
touch app/assets/javascripts/backbone/views/posts/index_view.js.coffee
touch app/assets/javascripts/backbone/views/posts/post_view.js.coffee
touch app/assets/javascripts/backbone/views/posts/new_view.js.coffee
touch app/assets/javascripts/backbone/views/posts/edit_view.js.coffee
touch app/assets/javascripts/backbone/views/posts/show_view.js.coffee
touch app/assets/javascripts/backbone/templates/posts/index.jst.ejs
touch app/assets/javascripts/backbone/templates/posts/post.jst.ejs
touch app/assets/javascripts/backbone/templates/posts/new.jst.ejs
touch app/assets/javascripts/backbone/templates/posts/edit.jst.ejs
touch app/assets/javascripts/backbone/templates/posts/show.jst.ejs
Time to edit app/views/posts/index.html.erb
with the following:
<div id="posts"></div>
<script type="text/javascript">
$(function() {
// Blog is the app name
window.router = new Blog.Routers.PostsRouter({posts: <%= @posts.to_json.html_safe -%>});
Backbone.history.start();
});
</script>
This creates a new instance of our Router which will handle the whole CRUD.
Well, there's still some work to do. We have to bind events and create the layouts using both views and templates. The backbone views have attributes like the models and collections, let's see what these are for.
el: '#posts'
- The parent element of the whole view expressed as a css selector. This means that the events binding in this view would only be available for elements within '#posts'.
template: JST['path/to/template]
- This is the template (layout) our backbone view would use, you can set multiple templates to a single view.
events: 'event selector' : 'method'
- This is really helpful because it binds all of the declared events to functions, so we don't have to bind events manually.
tagName: 'someHtmlTag'
- This is the element that would wrap the template content.
class Blog.Views.PostsIndexView extends Backbone.View
el: '#posts'
template: JST["backbone/templates/posts/index"]
initialize: ->
@render()
@addAll()
addAll: ->
@collection.forEach(@addOne, @)
addOne: (model) ->
@view = new Blog.Views.PostView({model: model})
@$el.find('tbody').append @view.render().el
render: ->
@$el.html @template()
@
In this example index_view.js.coffee we set the '#posts' element as the container, and, whenever an instance of the PostIndexView is created the addAll()
function is called to render singular views of the posts for each object in the collection.
<table>
<tbody>
<tr>
<th>Title</th>
<th>Content</th>
<th></th>
<th></th>
<th></th>
</tr>
</tbody>
</table>
<a href="#new">New Post</a>
The code above, index.jst.ejs, is the index template, what the index_view renders, but, as you can see, its nothing but headers. Let's go back to our addOne()
method and you'll see that it creates an instance of another view. This view works like a rails partial and here it is post_view.js.coffee:
class Blog.Views.PostView extends Backbone.View
template: JST["backbone/templates/posts/post"]
events:
"click .destroy" : "destroy"
tagName: "tr"
destroy: () ->
@model.destroy()
this.remove()
return false
render: ->
@$el.html(@template(@model.toJSON()))
return this
As you can see we are binding a destroy action when the user clicks something with the class destroy within a tr, and something new here is that when we render the template we are sending the model as JSON.
That's how the template can read the values to use them in the layout. In post.jst.ejs template you can see how we tell it to change the text according to the value in the keys 'title' and 'content':
<td><%= title %></td>
<td><%= content %></td>
<td><a href="#/<%= id %>">Show</td>
<td><a href="#/<%= id %>/edit">Edit</td>
<td><a href="#/<%= id %>/destroy" class="destroy">Destroy</a></td>
It's time to create new_view.js.coffee. This view would be in charge of saving new posts to our blog.
class Blog.Views.PostsNewView extends Backbone.View
el: '#posts'
template: JST["backbone/templates/posts/new"]
events:
"submit #new-post": "save"
initialize: ->
@render()
render: ->
@$el.html @template()
save: (e) ->
e.preventDefault()
e.stopPropagation()
title = $('#title').val()
content = $('#content').val()
model = new Blog.Models.Post({title: title, content: content})
@collection.create model,
success: (post) =>
@model = post
window.location.hash = "/#{@model.id}"
Well, the only new thing here is the save method, but, it is pretty straight forward. If you read carefully, you can see we are getting the values using jQuery and then we create an instance of our Post model with the given values, then, we use the create method of the backbone collection that sends a POST request to the server and saves a record. We could have used @model.save()
instead of @collection.create()
but then, the new object would not be available on the collection unless we call fetch()
over the collection which sends a GET request to the given url in the collection.
The code below is the template in charge of taking user inputs for new posts, new.jst.ejs.
<h1>New post</h1>
<form id="new-post" name="post">
<div class="field">
<label for="title"> title:</label>
<input type="text" name="title" id="title" >
</div>
<div class="field">
<label for="content"> content:</label>
<input type="text" name="content" id="content">
</div>
<div class="actions">
<input type="submit" value="Create Post" />
</div>
</form>
<a href="#index">Back</a>
What if the user made a mistake in the creation of a post? Well, that's when this guy comes in handy, edit.js.coffee:
class Blog.Views.PostsEditView extends Backbone.View
template: JST["backbone/templates/posts/edit"]
el: '#posts'
events:
"submit #edit-post" : "update"
initialize: ->
@render()
render: ->
@$el.html @template(@model.toJSON())
@
update: (e) ->
e.preventDefault()
e.stopPropagation()
title = $('#title').val()
content = $('#content').val()
@model.save({title: title, content: content},
success: (post) =>
@model = post
window.location.hash = "/#{@model.id}")
This is practically the same as the new view, the only difference is that, this time we use @model.save({attributes}) instead of the collection.
Now edit.jst.ejs:
<h1>Edit post</h1>
<form id="edit-post" name="post">
<div class="field">
<label for="title"> title:</label>
<input type="text" name="title" id="title" value="<%= title %>" >
</div>
<div class="field">
<label for="content"> content:</label>
<input type="text" name="content" id="content" value="<%= content %>" >
</div>
<div class="actions">
<input type="submit" value="Update Post" />
</div>
</form>
<a href="#/index">Back</a>
Let's go with the last view show_view.js.coffee. This one is pretty simple, and, at this point it shouldn't need an explanation:
class Blog.Views.PostsShowView extends Backbone.View
template: JST["backbone/templates/posts/show"]
el: '#posts'
initialize: ->
@render()
render: ->
@$el.html(@template(@model.toJSON()))
@
Finally, the template show.jst.ejs:
<p>
<b>Title:</b>
<%= title %>
</p>
<p>
<b>Content:</b>
<%= content %>
</p>
<a href="#/index">Back</a>
One more thing before running our app, go to the posts controller and edit the update action to only update title and content attributes and not the whole set of attributes because as created at, and updated at are protected attributes rails just won't let you update the whole set, that update action should end up looking like this:
def update
@post = Post.find(params[:id])
respond_to do |format|
if @post.update_attributes(title: params[:post][:title], content: params[:post][:content])
format.html { redirect_to @post, notice: 'Post was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
Now, we are good to go. Just start your server then go to http://localhost:3000/posts.
Conclusion
Using backbone on the client side is really helpful to keep your JS code well structured, even though I don't really like having all those JS files in the app, but, as your app grows it's better to have small and well structured portions of code than having a gigantic JS file of thousands lines of code. It becomes easier to read and to work with, and, let me tell you, once you start with backbone its really hard to put it aside and all of a sudden you'll want to "backbonize" everything in your proyects.
More info
If you want to go deeper I'd recommend you go straight to Backbone's documentation and start reading it since this post only covers the basics.
Don't forget to check the backbone-rails
gem page at github
Contact Me
If there is something I can help you out or you have some questions for me don't hesitate in contacting me at gustavo.robles@magmalabs.io