Tag Archives: ember-data

Ember 1.0 – rise from the ashes

We have been moving our explorer2 app to use the released version of Ember (1.0) and the accompanying Ember Data (1.0.0 beta 3). There have been lots of changes to Ember Data which you can find here.

Ember changes
Ember itself has not changed too much between RC and release but here are some things we noticed

1) Refer to routes inside quotes in linkTo blocks

Previously we had

{{#linkTo post.comment comment}}{{comment.text}}{{/linkTo}} 

which is now

{{#linkTo 'post.comment' comment}}{{comment.text}}{{/linkTo}}

2) Define hasMany etc relationships as ‘async’ in models
If the child models are loaded as something like “comment_ids”:[1,2] in the server json response then

comments: DS.hasMany('comment')

should now be

comments: DS.hasMany('comment', { async: true })

3) hasMany etc relationships are now simple strings rather than referring to the model type

 DS.hasMany('App.Comment')

is now

 DS.hasMany('comment')

4) controller actions now inside ‘actions hash’.
Inside your controller put your actions inside a block like this:

actions: {
  someAction: function(){},
  anotherAction: function(){}
}

Ember Data Changes
There are some really important Ember Data changes, here are some highlights

1) Per model adapters.

Each model can have its own adapter to find data etc. This is really useful if you create your model from ‘non-standard’ json or assign your own ids.
Here is an example from explorer2 where we grab some json from a server and create the model on the fly.

App.CompoundAdapter = DS.Adapter.extend({
  find: function(store, type, id) {
    // return a promise inside of which is the callback which either resolves with the retrieved compound data or rejects with the status
    var promise = new Ember.RSVP.Promise(function(resolve, reject){
      var searcher = new Openphacts.CompoundSearch(ldaBaseUrl, appID, appKey);
      var pathwaysSearcher = new Openphacts.PathwaySearch(ldaBaseUrl, appID, appKey);
      // get the compound details  
	  var callback=function(success, status, response){  
        if (success) {
	    var compoundResult = searcher.parseCompoundResponse(response);
            compoundResult['pathways'] = [];
            resolve(compoundResult);
        } else {
            reject(status);
        }
      }
      searcher.fetchCompound('http://www.conceptwiki.org/concept/' + id, null, callback);
    });
    return promise;
  }
});

2) Promises and chaining.

Ember data makes heavy use of Promises (https://github.com/tildeio/rsvp.js). When finding models you resolve or reject from within the promise and can even chain them together if you need to do something else once you have found/created a model. The adapter example above demonstrates a simple promise. Here is an example of a promise chain where we find a model and then do something with it after it is found.

me.get('store').find('compound', url.split('/').pop()).then(function(compound) {
  thisCompound.get('structure').pushObject(compound);
});

3) Find uses the store, not the model
Before we would have

App.Post.find(1)

now we have

store.find('post', 1)

Similarly when creating we do

store.createRecord('post', {title: 'a great post'})

4) onCreate, transactions gone – use promises
previously we would listen to the ‘onCreate’ action of a model and then do something with it. Now do

post.save().then(function(post){
  //do something
});

5) Underscored keys needs different serializer

If your relationships are sent as underscored values in the json, for example:

{"comment":{"id":4,"text":"comment 3","post_id":1}}

Then you need to tell Ember Data by creating an ApplicationSerializer with

App.ApplicationSerializer = DS.ActiveModelSerializer.extend({});

Otherwise your json will look like

{"comment":{"id":4,"text":"comment 3",post:1}}

which in our case breaks the default rails behaviour.

6) Add child model to parent after save
Inside the promise when saving a child model you need to add it to the parent or it will not be shown when transitioning. In this example we save a comment for a post, add it to the posts array of comments and then transition to the index for that post. You could also reload the parent model instead, not sure what the preferred/correct behaviour should be.

comment.save().then(function(comment){
        //post.reload(); //could do this instead
        post.get('comments').pushObject(comment);
        me.get('target').transitionTo('post');
      });

Overall, quite a lot of changes but I think they make Ember easier to use and hopefully make it stable (for a little while at least). You can find an updated simple blog app with the latest changes here. The non rails version using fixture adapter is here with jsfiddle here.

Ember JS: Adding posts to our blog application

At the end of the last article I challenged you to add the ability to create new posts for our very simple blog application. We saw how to create new comments for a post and adding a new post uses a similar pattern. First we need a route for posts/new. Change the posts resource to include this

this.resource('posts', function() {
  this.route('new');
});

This means that ember will now create the routes posts, posts.index and posts.new. Previously we only had the posts route but when we go to /posts we will now transition to posts.index. We need to add a posts directory under app/javascripts/templates/ as well as handlebars files for the index and new routes.

/templates/posts/
/templates/posts/index.hbs
/templates/posts/new.hbs

These additional new routes means that any sub route of posts will be rendered within the posts template outlet. We need to add an {{outlet}} to it to allow this to happen. At the same time we need to move the handlebars code for showing the posts from the original posts template to the posts.index template.

The posts template now only contains

{{outlet}}

The posts.index template needs to contain the old content from the posts template

Posts
<ul>
  {{#each controller}}  
    <li>{{#linkTo post this}}{{title}}{{/linkTo}}</li>
  {{/each}}
</ul>

We will also add a content property to the Post model to reflect what the rails side expects.

App.Post=DS.Model.extend({
  comments: DS.hasMany('App.Comment'),
  title: DS.attr('string'),
  content: DS.attr('string')
});

In hindsight using content was a poor choice since each controller has a ‘content’ propery and this could get a little confusing. However we will stick with it for the moment.

Just like in the comments.new template we need a form to submit with the details for our new post. In the posts.new template add the following

<form {{action save content on='submit'}}>
  {{view Ember.TextArea valueBinding="title" placeholder="Enter title here"}}
  {{view Ember.TextArea valueBinding="content.content" placeholder="Enter content here"}}
  <button type="submit">Create Post</button>
</form>

You can see we have content.content in the second TextArea, this is because using content alone binds to the controllers ‘content’ rather than the models.

One problem here is that ember does not know what models to use in the posts.index route. We need to tell it.

Create a PostsIndexRoute using the following code or rename the PostsRoute to PostsIndexRoute within router.js

App.PostsIndexRoute=Ember.Route.extend({
  model: function(){
    return App.Post.find();
  }
});

Add a link to post.new at the bottom of the posts.index template

{{#linkTo posts.new}}New Post{{/linkTo}}

We need to tell the PostsNewRoute how to set up the controller and we also require a controller to handle the save action. Add the following code to the router.js file.

App.PostsNewRoute=Ember.Route.extend({
  model: function(){
    return App.Post.createRecord();
  }
});

Inside the model hook we return a new Post model to the controller. Create controllers/postsNewController.js with the following code

App.PostsNewController=Ember.ObjectController.extend({
  save: function(post) {
    var me = this;
    post.get("store").commit();
    post.on('didCreate', function() {
      me.get('target').transitionTo('posts.index');
    });
  }
});

The reason we have the post.on('didCreate', function() { part is so that we do not transition to the posts.index route before the model has been saved on the server side and has an id. Otherwise we will render the page with a link to /posts/null.

NOTE 8/7/13 : I have just noticed that the post with a null id is still shown on the index page, what we need to do is change the response format for new posts and comments to stop this. In the rails side PostsController we need the create method to respond with the following { “post”: {“id”: 1, “text”: “blah”….} and similarly for the CommentsController. In the create action under if @post.save change

format.json { render :json => @post, :status => :created, :location => @post }

to

format.json { render :json => { :post => @post.as_json}, :status => :created, :location => @post }

Change the CommentsController create method in a similar fashion. We also need the 'didCreate' listener hook for comments save method on the ember side like we already have for the posts.

The code for this article is available here.
(Code recently updated due to ember/jquery-rails/handlebars-assets version compatibility issues and the json response format changes noted above)

Ember JS: Server side

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?