Canonical way to load nested resources


#1

I am trying to figure out what the best practice is for loading nested resources in Ember. I thought I had this figured out, but there are a few cases that aren’t completely documented or discussed elsewhere.

Consider a simple Blog which has 2 resources Posts and Comments. The relationships are,

  • Post hasMany Comments
  • Comment belongsTo Post

The UI design is typical of a Blog, it needs to,

  • show a list of Posts with links to each Post.
  • show each Post with it’s comments below.
  • lazy load and show comments of each post after displaying the Post body.

The desired URLs are,

  • /posts - list of posts
  • /post/:id - each individual post

From what I can tell, The standard approach to these scenarios is to have,

  • PostsRoute - load list of posts via model, render into posts template
  • PostRoute - load post for id via model, render into post template

So far so good. Now to render the comments into the post template. There are 2 approaches.

  • {{render ‘comments’ post}} - use the render helper in the post template
  • renderTemplate - call render(‘comments’, options) with an outlet, etc.

Both approaches generate a controller for the resource and that controller is used by the comments template to lookup bindings to display.

This CommentsController will use needs and postBinding to lookup the post that it is referring to.

But how do you load the comments for the individual post when on /post/1? There is no route used by the rendered Comments, so the model() hooks are out.

I haven’t been able to answer this satisfactorily so far. My solutions to this are,

  1. Add a load() method to the CommentsController.
  • Then load the comments for that post manually, called in init() directly or deferred.
  1. Make Comment a nested resource of Post.
  • change /posts links to /post/1/comments, instead of /post/1.
  • load the comments in the setupController of PostRoute

Solution 2 somewhat works. But isn’t really a solution at all, because a real world scenario would require rendering multiple such resources. And you can’t switch to N sub resource routes simultaneously.

Solution 1 works to an extent. However the same controller is used for different posts. So you end up having to figure out the post for whom the comments were rendered. And load the comments if they don’t match. Further there is no easy way to know when post model behind it is switched out when the route changes. ie:- comments once loaded show for subsequent posts. The enter and exit hooks are on the Route not the controller.

I have recently discovered the {{control}} helper, where the controller isn’t shared. But given the experimental warnings, I am reluctant to use it, if it could be removed. Also it isn’t really a solution, the actual loading still needs to be done.

The reason I am looking for a canonical solution to this, is that this pattern repeats consistently across nearly every UI design.

For instance, When a User is on a parent resource, the UI needs to either show top 10 type lists, aggregates, or stats of the child resources.

I am quite new to Ember, so please let me know if I am missing some key Ember idea that makes it possible to do this.

I especially like the async Router style hooks, which are declarative. Instead of loading a resource you tell Ember that a resource needs to be loaded.

I welcome any suggestions on how to go about building this, even if it isn’t currently feasible within core.

Thanks.


Nested UI, Routes and Resources: Pro and Con
#2

I’m not sure if I get your question right, but assuming that you use ember-data or ember-model, you can achieve both of the scenarios you mentioned. I would add needs property to comments controller in order to get access to post controller, like so:

App.CommentsController = App.ArrayController.extend({
  needs: ['post'],

  contentBinding: 'controllers.post.comments'
});

Now you can use comments controller as you would use it in regular template. Technically it doesn’t really matter if you render it using {{render}} or in routes in this particular situation. However, I would probably use {{render}}, because you probably want it to be there no matter what and it will be probably not swapped with some other template.


#3

Thanks @drogus. I am using ember-data.

Does this rely on sideloading comments along with the posts? I mean does ember-data make a request to /post/1/comments when the bound property in the template for the CommentsController is looked up?

This is very new to me. I was under the impression that you need model() hooks to load data, or you manually do $.getJSON.

Edit: I just tested this. Indeed, ember-data does a comments?ids[]= query when the comments ids are sideloaded, and post/1/comments when they aren’t.

Ember-data is whole lot smarter than I thought it was! Is this documented somewhere, I would like to explore this further.

Thank you!


#4

It depends on how you set your adapter. If it’s RESTAdapter and if comments url is set properly, comments should be fetched with an ajax request when you access them.

Ideally you should not have to do any $.getJSON if your model layer is set up correctly.

You could use model() hook to set comments for CommentsController, if you plug your comments to /posts/1/comments url, but then again, you could just do:

model: function() {
  var post = @controllerFor('post');
  return post.get('comments');
}

You may try to look at hasMany documentation, although I’m not sure if there’s much info there, Ember Data is still under development, so it’s not heavy documented in general.


#5

That pretty much covers my needs. I will look into the hasMany source documentation. Thanks.


#6

@dsawardekar, would you mind sharing your code for the case when comments are not side loaded? I’ve only been able to get this to work by traversing to a route and having the controller manually load the comments with something like this:

App.CommentsIndexRoute = Ember.Route.extend
  model: -> App.Comment.find(post_id: @modelFor('post').id)

#7

@heedspin The code to fetch the nested comments is simply post.get('comments') where post is your model which has already resolved.

You can place this in the afterModel hook of your PostsRoute route and it will get the comments after the post.

afterModel: function(post) {
    return post.get('comments')
}

The returned value is also a promise so you can chain things to it’s then as well. It can also go in setupController or elsewhere if you are loading it lazily. The actual call made to server varies as mentioned above, on whether the comment_ids are embedded in the post json.


#9

@drogus Are you sure about accessing controller instances in model hook? Is that good?


#10

Is this functionally equivalent to the following?

model: function() {
  @modelFor('post').get('comments')
}

#11

I have similar situation where I have a user that i want to show posts by him, by calling users/123/posts, and i can’t get it to work. No request get sent to the server.

User has_many posts, and Post belongs_to users

App.Router.map ->
  @resource 'user', path: '/user/:user_id', ->
    @route 'posts'
    @route 'replies'

App.UserRoute = Ember.Route.extend(
  model: (params) ->
    App.User.find params.user_id

  setupController: (controller, model) ->
    controller.set 'content', model
)

App.UserPostsRoute = Ember.Route.extend(
  model: (params) ->
    user = @modelFor 'user'
    user.get 'posts'

  setupController: (controller, model) ->
    controller.set 'content', model

Am i missing something here?


#12

I am not able to trigger the same behaviour. Trying to use ember-data-beta2 with ember 1.0.0. jsfiddle / jsbin or code example would be very much appreciated.


#13

Interesting, this is not working for me. Only solution I’ve gotten to work is the {async: true} one.