Creating the rails app
In the Ember JS MVC tutorials we created a purely browser based application with no persistence between sessions and hard coded fixtures for data. In this article we will create a server side app to handle data persistence, we will also use it to serve our Ember application. We will use Ruby on Rails. The code is available here in zip form or if you want to fork etc on github try here. Start by creating a new rails application
rails new blog
Wait for the app to be created, go in to the blog directory and remove public/index.html
. You can use ember-rails to create the ember client side skeleton code but we will do it by hand using the classes and templates we created during the MVC tutorials. I find it is helps you to understand how the application works if you use as little auto-generated code as possible. However, we will use some rails scaffold to get us started.
rails generate scaffold Post title:string content:text
rails generate scaffold Comment text:string post_id:integer
This creates the controllers, views, model, helper, db migrations etc. Then we can create the database.
rake db:migrate
rake db:create
As we defined in the MVC tutorial a Post can have many Comments so add the relationships to the rails models, in blog/app/models.
add belongs_to :post to comment model
add has_many :comments to post model
In routes we have resource for posts and comments, add a root
root :to => 'welcome#index'
We therefore need something for this route to render. Create a welcome_controller.rb
in app/controllers
with an index
method and a index view for it in app/views/welcome/index.html.erb
. The view will be empty, it is just a placeholder for our rails app to boot the root ie /. Ember will be doing all the rendering.
class WelcomeController < ApplicationController
# GET /
def index
respond_to do |format|
format.html # index.html.erb
end
end
end
Add the Ember application
Just like in a rails app we can make our ember application easier to manage by creating an ember app structure in app/assets/javascripts
. ember-rails would do this for you.
/controllers
/templates
/models
/views
/helpers
Using the code from app.js
and index.html
from part 3 of the tutorial create the following files
controllers/commentsNewController.js
App.CommentsNewController=Ember.ObjectController.extend({
needs: 'post',
text: null,
save: function() {
var post = this.get('controllers.post.content');
App.Comment.createRecord({ post: post, text: this.get('text') });
this.get('target').transitionTo('post.index');
}
});
models/post.js
App.Post=DS.Model.extend({
comments: DS.hasMany('App.Comment'),
title: DS.attr('string')
});
models/comment.js
App.Comment = DS.Model.extend({
post: DS.belongsTo('App.Post'),
text: DS.attr('string')
});
The templates directory structure mimics the data-template-name
from index.html
/templates
/templates/comments
/templates/comments/new.hbs
/templates/post
/templates/post/comment.hbs
/templates/post/index.hbs
/templates/application.hbs
/templates/comments.hbs
/templates/index.hbs
/templates/post.hbs
/templates/posts.hbs
Copy the contents of the script tags in index.html
from part 3 of the MVC tutorial to the appropriate template, you do not need the enclosing script tag itself. We also need to add a router.js
to the app/assets/javascript
directory, use the code from app.js
we created in part 3.
App.Router.map(function() {
this.resource('posts');
this.resource('post', { path: '/posts/:post_id' }, function() {
this.resource('comments', function() {
this.route('new');
});
this.route('comment', { path: 'comments/:comment_id'});
});
});
App.PostsRoute=Ember.Route.extend({
model: function(){
return App.Post.find();
}
});
App.PostIndexRoute=Ember.Route.extend({
model: function(params) {
return this.modelFor('post');
}
});
App.CommentsNewRoute=Ember.Route.extend({
setupController: function(controller, model) {
controller.set('text', null);
}
});
Our store.js
no longer uses the fixture adapter but uses the rest adapter. Add store.js
to app/assets/javascripts
and put the following code in it.
App.Store = DS.Store.extend({
revision: 12,
adapter: DS.RESTAdapter.create({
bulkCommit: false
})
});
Serve the javascript from rails
It is advisable to use the latest stable version of ember and ember-data. Download ember-latest.js & ember-data-latest.js from http://builds.emberjs.com/ and put in vendor/assets/javascripts Add the handlebars_assets gem to the Gemfile so that we load handlebars.js
and get the assets pipeline to compile our handlebars templates. You may have noticed that we append .hbs to them since this is what the gem looks for rather than .handlebars. We also need to load our ember application javascript files, create a blog.js
file
//= require ./store
//= require_tree ./models
//= require_tree ./controllers
//= require_tree ./views
//= require_tree ./helpers
//= require_tree ./templates
//= require ./router
//= require_self
Then add blog
, handlebars
, ember
and ember-data
to application.js
. Also create our ember app instance inside there.
//= require jquery
//= require jquery_ujs
//= require handlebars
//= require ember-latest
//= require ember-data-latest
//= require_self
//= require blog
window.App = Ember.Application.create({
LOG_TRANSITIONS: true
});
Start a rails server with rails s
and go to localhost:3000
in a browser. You should see the Blog index page with a link to ‘Posts’. Open up the debugger to look at the network traffic. Click on the Posts link and you will see a request to localhost:3000/posts
. The response is empty but the application transitions to posts.
Add some data and respond with JSON
We will now add some data for the server to send to the browser. Add some db seeds to db/seeds.rb
post = Post.create( :title => 'First post', :content => 'Text for first post' )
Comment.create(:text => 'Post one comment one', :post_id => post.id)
Comment.create(:text => 'Post one comment two', :post_id => post.id)
post = Post.create( :title => 'Second post', :content => 'Text for second post' )
Comment.create(:text => 'Post two comment one', :post_id => post.id)
Comment.create(:text => 'Post two comment two', :post_id => post.id)
post = Post.create( :title => 'Third post', :content => 'Text for third post' )
Comment.create(:text => 'Post three comment one', :post_id => post.id)
Comment.create(:text => 'Post three comment two', :post_id => post.id)
Use rake db:seed
to add the posts and comments. We also need to make sure that our rails application serves JSON in the correct format. We need to add the correct JSON serialiser to the post model to include the comments as comment_ids array. Ember needs the JSON response in the format "posts" : [{...},{...}]
def as_json(options={})
{ :id => self.id, :title => self.title, :content => self.content, :comment_ids =>self.comments.collect{|comment| comment.id} }
end
We then need to change the posts controller index method to render json using this method
format.json { render :json => { :posts => @posts.as_json } }
as well as in the show method
format.json { render :json => { :post => @post.as_json } }
We also need to change the JSON response in the comments controller show method to
format.json { render :json => { :comment => @comment } }
so that the json is returned as "comment" : {....}
(see http://jonathanjulian.com/2010/04/rails-to_json-or-as_json/)
When fetching the comments ember will request it as /comments?ids[]=1&ids[]=2
ie as an array of ids, change the comments_controller index method to handle that
if params[:ids]
@comments = []
params[:ids].each do |id|
@comments.push(Comment.find(id))
end
else
@comments = Comment.all
end
respond_to do |format|
format.html # index.html.erb
format.json { render :json => { :comments => @comments } }
end
Save new comments to the server
We need to change the CommentsNewController
save method so that it sends a POST request to /comments
with the new comment in JSON. Change it to:
var post = this.get('controllers.post.content');
var comment = App.Comment.createRecord({ post: post, text: this.get('text') });
comment.get("store").commit(); //this is the new bit, after creating a comment we need to commit it
this.get('target').transitionTo('post.index');
comment.get("store").commit()
tells the comment to commit itself.
Go to /posts/1/comments/new
, enter a comment and hit save. You will see some network traffic heading to /comments on the server. Refresh /posts/1 and you will see that it has the new comment. You can use the rails console to convince yourself. Comment.all
will have the new one at the end. We can use exactly the same principles to create new posts, why not add it to your app?