Helping contributors to help you with 5 simple rules

Helping out in open source projects is easier than ever using systems like git and github. However, you need to ensure that any code sent to you is as easy as possible to merge. On our OPS.js project we added these 5 simple rules to make everyone’s life easier.

1. Fork the project.
2. Checkout develop branch.
3. Create a new branch based on develop and change the code.
4. Write some tests.
5. Submit patch.

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.

Hundreds of source files, forgot the copyright statement…….

So your new super project is all ready to go live. Add the licence file, check the copyright. Oh wait, you mean you forgot to add the copyright statement to your eclipse template. Never fear, the linux command line to the rescue, no need for hours of cut and paste:
find . -name "*.java" -print | xargs sed -i '1s/^/\/* Copyright (c) 2013 The University of Manchester, UK. *\/\n/'

Don’t forget to replace Copyright (c) 2013 The University of Manchester, UK with your own statement.

handle_asynchronously missing method with delayed job and passenger: ‘require’ needed

We use delayed job in lots of our applications to handle long running tasks but we came across a weird issue when deploying our explorer2 rails app on apache using passenger. Passenger would throw out a 500 with ‘missing method handle_ayncronously’ whenever the application was accessed. Commenting out the handle_asynchronously part in the class (one of the models) and all would work fine. Running in development all was perfect and delayed job worked without a hitch. To see if we could replicate it we created the most basic rails app possible with one model with a method that used handle_asynchronously and guess what……it did not work either. Really left us a bit stumped how to solve it until I had the idea to use require 'delayed_job' in the model class using it. Then everything worked again.
So, we have the solution but we are not really sure what the cause is. The problem occurs in ruby 1.8 or 1.9 with rails 2.3.12 or 2.3.13 and delayed job 4.0.0 or 3.0.5 (via delayed job active record). However, we have code that uses handle_asynchronously (eg the original openPHACTS gui) in the exact same way without a hitch! Anyway, I thought that bundler etc meant that ‘require’ was no longer required but be warned….

Rubygems, bundler and ssl error

I experienced a ‘Could not verify the SSL certificate for https://rubygems.org/’ error when doing a bundle install on OSX with RVM recently. A bit of googling revealed that there was a good chance that the CA certificates that curl uses were out of date

Solution:
Update rubygems gem update --system
Update RVM rvm get head
List certificate status rvm osx-ssl-certs status all
Update certificates rvm osx-ssl-certs update all

RVM was 1.18.15 before the update and 1.21.7 after the update.

Ember JS: Ruby gem version hell

We are used to adding a gem to our Gemfile and just quietly getting on with things. However. with Ember things are not that easy. It is a moving target and you may see things like

Uncaught Error: assertion failed: Ember Handlebars requires Handlebars 1.0.0-rc.3 or greater. Include a SCRIPT tag in the HTML HEAD linking to the Handlebars file before you link to Ember.

and

Uncaught Error: assertion failed: Ember Views require jQuery 1.8, 1.9 or 2.0

Ember seems to have some difficulty figuring out what version numbers it can actually use: eg 1.0.0 is greater than 1.0.0.rc.3 but not to Ember. I have had to freeze handlebars-assets at 0.12.0 and jquery-rails at 2.2.1 to get round these issues. I have seen this with Ember 1.0.0.rc-6 and earlier. Be careful. One day things will hopefully get a lot simpler but until then keep your eyes open for version incompatibilities. There may be later versions of these gems that are compatible but these were versions that I know worked for me.

Update: I played around with all the gems and libraries and found that with Ember 1.0.0 rc6 you can use handlebars_assets 0.12.3. This in turn uses handlebars 1.0.0-rc.4. I also saw from the handlebars_assets github page that you can tell it to use specific versions of handlebars with

HandlebarsAssets::Config.compiler = 'my_handlebars.js' # Change the name of the compiler file
HandlebarsAssets::Config.compiler_path = Rails.root.join('app/assets/javascripts') # Change the location of the compiler file

Ember JS: Dynamic Tree View

A tree consists of nodes and branches. Each branch can contain more nodes and/or branches. The branches can be represented by nested
ul tags and the nodes by li tags within them. We will create 2 Ember views, one for the nodes and the other for the branches. An Ember view has a tagName property which will be assigned to the html tag it creates. We will use an Ember CollectionView to represent the branches. A collection view renders its content within the itemViewClass you specify. Within our TreeBranchView we will use a TreeNodeView to render the content

App.TreeBranchView = Ember.CollectionView.extend({ 
    tagName: 'ul',
    content: [{'name': 'Paul', 'age': 10, 'branch': true},{'name': 'Tom','age': 5, 'branch': false},{'name': 'Paul', 'age': 7, 'branch': false}], 
    classNames: ['treebranch'],
    itemViewClass: 'App.TreeNodeView'
});

You can see that the content has been added to the view but you could load it from a controller if you wanted.

In order to show a different icon depending on whether the branch is open or closed or if it is a node we will use css and the Ember view property classNameBindings. A classNameBindingtells ember to either always assign the css class name or to assign it depending on another property of the view. We will use the class names ‘opened’ and ‘branch’ and specify them as

classNameBindings: ['opened: tree-branch-open', 'branch:tree-branch-icon:tree-node-icon']

The class tree-branch-open will be added to the view and therefore the li tag that represents it if the view property opened is true. Otherwise the class name tree-branch-open will not be applied to the view. The class name tree-branch-icon will be applied to the view if the property branch is true otherwise the class name tree-node-icon will be applied to it. Remember that this all happens dynamically, you do not have to do anything programatically.

So that we can open and close branches and add more data we will listen to the click handler for the node. If the item clicked on is a branch then first time round we will create a new TreeBranchView programatically within the click handler, dynamically insert it as a sub view under this branch and then save this sub view as a property of the parent view. If the branch has already been opened, ie the property opened is true, then we will remove the sub views (remember that we saved the sub view as a property of the parent). Ember will handle all the rendering for us.

We also need a template for the node view. In the code below you can see that the template is specified within the class, this is due to an issue I was having with ember finding the template within the html when inserting the view dynamically. Maybe it will have been fixed by now or maybe I was not doing it correctly? Anyway specifying the template in the view works just fine. In this example the template is not very exciting and shows the same whether it is a node or a branch, you may want to use some logic to show the data appropriate to your app.

App.TreeNodeView = Ember.View.extend({ 
    opened: false,
    branch: function(){
        return this.get('content').branch;
    }.property(),
    subBranch: undefined,
    fetchedData: false,
    tagName: 'li',
    // class names that determine what icons are used beside the node
    classNameBindings: ['opened: tree-branch-open', 'branch:tree-branch-icon:tree-node-icon'], //templateName: 'treenode',
    // Ember had some issues with finding the treenode template when the branch view is dynamically added to
   // the parent collection view in the click event. Had to compile the template here instead
   template: Ember.Handlebars.compile('{{view.content.name}} {{view.content.age}}'),
   click: function (evt) {
       if (this.get('opened')) {
           // user wants to close the branch
           var index = this.get('parentView').indexOf(this) + 1;
           this.get('parentView').removeAt(index);
           this.set('opened', false);
       } else if (this.get('fetchedData')) {
           // user wants to open the branch and we have already created the view before
           var index = this.get('parentView').indexOf(this) + 1;
           this.get('parentView').insertAt(index, this.get('subBranch'));
           this.set('opened', true);
       } else if (this.get('branch')) {
           // user wants to open the branch for the first time
           var name, age; 
           var me = this;
           name = this.get('content').name;
           age = this.get('content').age;
           var treeBranchView = App.TreeBranchView.create();
           treeBranchView.set('content', [{ 'name': 'John', 'age': 10, 'branch': true }, { 'name': 'Tom', 'age': 5, 'branch': false }, { 'name': 'Paul', 'age': 7, 'branch': true }]);
           var index = me.get('parentView').indexOf(me) + 1;
           me.get('parentView').insertAt(index, treeBranchView);
           me.set('opened', true);
           me.set('subBranch', treeBranchView);
           me.set('fetchedData', true); 
       } 
   }
});

Don’t forget that you need a handlebars template in the html for your initial TreeBranchView, {{view App.TreeBranchView}}

Here is a JSfiddle with all the code plus the CSS used for the icons.

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?

Ember JS: MVC in the browser. Part 3 – Nested Resources

In part 2 we created templates so that we could view any of our blog posts along with the models and controller actions to support this. We also used ember-data to create a store which uses a FixtureAdapter along with some hardcoded Post fixtures to load our models. A Post with a title is not much use on its own, we also want people to be able to add comments. We will have to extend the post route to add a nested resource. We will also need a new model to represent the nested resource. The code for this part is available here. Open app.js in an editor and change the post route to match the following:

this.resource('post', { path: '/posts/:post_id' }, function() {
  this.resource('comments');
});

This means we will now have a /posts/:post_id/comments route.

Open the index.html page in a browser and examine the routes in the console. Remember our old friend Router.router.recognizer.names? (btw. I noticed in Firefox you may need to use App.Router.router.recognizer.names). You will see that comments route has been added. Lets add a comments model and fixtures. Add the following to app.js

App.Comment = DS.Model.extend({
  post: DS.belongsTo('App.Post'),
  text: DS.attr('string')
});

and add the following within the App.Post model making sure that you have a comma between any lines

comments: DS.hasMany('App.Comment')

Each Post can now have many Comment(s). Add some comment fixtures to app.js:

App.Comment.FIXTURES = [{id:"1", text: "First Comment"}, {id:"2", text: "Second Comment"}, {id:"3", text: "Third Comment"}, {id:"4", text: "Fourth Comment"}, {id:"5", text: "Fifth Comment"}, {id:"6", text: "Sixth Comment"}, {id:"7", text: "Seventh Comment"}, {id:"8", text: "Eighth Comment"}, {id:"9", text: "Ninth Comment"}];

We also need to change the Post fixtures to tell them that they have some comments:

App.Post.FIXTURES=[{ id: "1", title: "First Post", comments: [1, 2, 3] }, { id: "2", title: "Second Post", comments: [4, 5, 6] }, { id: "3", title: "Third Post", comments: [7, 8, 9] }];

Note that we have added a ‘comments’ attribute in each Post fixture with an array of the Comments they have. [1, 2, 3] means that this Post has the Comments with ids 1, 2 & 3. Go to /posts/1/comments/1 to check that they render correctly.

Change the post/index template to the following so that we can see the comments:

<script type="text/x-handlebars" data-template-name="post/index">
  {{title}}
  <ul>
    {{#each comment in comments}}
      <li>{{comment.text}}</li>
    {{/each}}
  </ul>
</script>

Go to /posts/1 and you should see the text for each Comment appearing under the Post title.

We can now add a link to each comment but before this we need to tell the router how to route to them. We want the route to look like /posts/:post_id/comments/:comment_id
To achieve this we need to add the following to the posts resource router definition

this.route('comment', { path: 'comments/:comment_id'});

We will also need a post.comment template in index.html

<script type="text/x-handlebars" data-template-name="post/comment">
  {{text}}
</script>

What if we wanted to add a new comment to a post? We will add a posts/:post_id/comments/new route and templates. Change the comments resource under the post resource in the router to

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

Then add a comments outlet template and a ‘new’ one

<script type="text/x-handlebars" data-template-name="comments">
  {{outlet}}
</script>

<script type="text/x-handlebars" data-template-name="comments/new">
  Comments New
</script>

 

If you refresh and look at the available routes we now have comments.new. Go to /posts/1 and click on the New Comment link and you should see the “Comments New” text. Not much use though, we need a text box for the comment and a button to submit. Add the following to the comments/new handlebars template

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

Here we tell the template that when the forms “Create Comment” button is clicked, ie on submit, it should call the “save” action in the CommentsNewController. We need to create that controller. In the TextArea we have bound the “text” value to the controller. Add the following to app.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');
  }
});

Firstly we tell this controller that it needs to know about the PostController so that it can tell what post the comment is for. We do this through needs: 'post'. The text: null bit is the value we have bound to the TextArea in the template. The save function is what happens when we click the button. Firstly it finds the post through controllers.post.content (facilitated via the ‘needs’ syntax), the we create a new Comment with the text value. Finally we transition back to the post. this.get('target') gives us an instance of the router. Try adding a new comment and see what happens. Note that since we are using the FixturesAdapter the ids may look a little bit odd.

One final thing we should do is reset the comments TextArea box in the comments/new template when we transition to the route. If we don’t then it will contain the last thing we entered in to it. Add this to app.js

App.CommentsNewRoute=Ember.Route.extend({
  setupController: function(controller, model) {
    controller.set('text', null);
  }
});

setupController is a hook that all routes have and allows you to set the model or modify the controller.

To recap, we created a comments nested resource for post and added the route ‘new’ to it. We added a Comment model and fixtures and linked them to the Post model via hasMany and belongsTo. We added the comments to the post.index template and added a route with a path for each post.comment so we could link to them. We created the comments.new template with a form that we could submit and the CommentsNewController with a save action to create the new comment.

The developers of myGrid tell of their quest of the code