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 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">
    {{#each comment in comments}}

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">

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() {

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

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

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


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>

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

  needs: 'post',
  text: null,
  save: function() {
    var post = this.get('controllers.post.content');
    App.Comment.createRecord({ post: post, text: this.get('text') });

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

  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.

Leave a Reply

Your email address will not be published. Required fields are marked *