Idiomatic Emberjs for nested routes but non-nested templates
Asked Answered
T

1

17

This is a follow-up from Understanding Ember routes.

Master/detail views are great but I'm trying to have a a hierarchical URL route without nesting their templates. However, I still need access to the parent model for things like breadcrumb links and other references.

So /users/1/posts should display a list of posts for user 1. And /users/1/posts/1 should display post 1 for user 1, but it shouldn't render inside the user template's {{outlet}}. Instead, it should completely replace the user template. However, I still need access to the current user in the post template so I can link back to the user, show the user's name, etc.

First I tried something like this (Method #1):

App.Router.map(function() {
  this.resource('user', { path: '/users/:user_id' }, function() {
    this.resource('posts',  function() {
      this.resource('post', { path: '/:post_id' });
    });
  });
});

...

App.PostRoute = Ember.Route.extend({
  model: function(params) {
    return App.Post.find(params.post_id);
  },
  renderTemplate: function() {
    this.render('post', {
      into: 'application'
    });
  }
});

This replaced the the user template with the post one, as expected. But when I click the browser's back button the user template doesn't render again. Apparently the post view is destroyed but the parent view is not re-inserted. There are a few questions on here that mention this.

I then got it to work with something like this (Method #2):

App.Router.map(function() {
  this.resource('user', { path: '/users/:user_id' },  function() {
    this.resource('posts');
  this.resource('post', { path: '/users/:user_id/posts' },  function() {
    this.resource('post.index', { path: '/:post_id' });
  });
});

...

App.PostRoute = Ember.Route.extend({
  model: function(params) {
    return App.User.find(params.user_id);
  },
  setupController: function(controller, model) {
    controller.set('user', model);
  }
});

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

App.PostIndexController = Ember.ObjectController.extend({
  needs: 'post'
});

But this seems a bit hacky to me and not very DRY.

First, I need to retrieve the User again in the PostRoute and add it as an ad-hoc variable to the PostController (this wouldn't be necessary if the routes were properly nested and I could just set a needs: 'user' property in the PostController). In addition, this may or may not have an impact on the back-end depending on the adapter implementation of ember-data or whatever technology is used to retrieve the data from the server (i.e. it may cause an unnecessary second call to load User).

I also need an additional `PostIndexController' declaration just to add that new dependency, which is not a big deal.

Another thing that doesn't feel right is that /users/:user_id/posts appears twice in the router configuration (one nested, one not).

I can deal with these issues and it does work but i guess that, overall, it seems forced and not as graceful. I'm wondering if I'm missing some obvious configuration that will let me do this with regular nested routes or if someone has a recommendation for a more "Ember.js way" of doing this.

I should mention that regardless of the technical merits of Method #2, it took me quite a while to figure out how to make it work. It took a lot of searching, reading, experimenting, debugging, etc. to find just the right combination of route definitions. I would imagine that this is not a very unique use-case and it should be very straightforward for a user to set up something like this without spending hours of trial and error. I'll be happy to write up some tips for this in the Ember.js documentation if it ends up being the right approach.

Update:

Thanks to @spullen for clarifying this. My case was not as straightforward as the example because some sub-routes need nested templates and some don't, but the answer helped me figure it out. My final implementation looks something like this:

App.Router.map(function() {
  this.resource('users', { path: '/users/:user_id' }, function() {
    this.resource('users.index', { path: '' }, function() {
      this.resource('posts')
    });
    this.resource('post', { path: '/posts/:post_id' }, function() {
      this.resource('comments', function() {
        this.resource('comment', { path: '/:comment_id' });
      });
    });
  });
});

So now posts renders under the users template but post replaces everything. comments then renders under post and comment, in turn, renders under comments.

All of them are sub-routes of users so the user model is accessible to all of them without acrobatics, by doing this.modelFor('users') in each Route where needed.

So the templates look like this:

users
|- posts
post
|- comments
   |-comment

I don't know why the { path: '' } is needed for the users.index resource definition but if I take it out Ember doesn't find the users route. I would love to get rid of that last vestige.

Todhunter answered 9/6, 2013 at 18:28 Comment(0)
G
9

You could define the parent template to just display the outlet and have an index route which will get displayed inside that. Then for the nested resource you can do the same thing.

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

<script type="text/x-handlebars" data-template-name="user/index">
  <h2>user/index</h2>
</script>

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

<script type="text/x-handlebars" data-template-name="posts/index">
  <h2>posts/index</h2>
</script>

That way it won't be a master/detail.

The router would be:

App.Router.map(function() {
  this.resource('user', function() {
    this.resource('posts', function() { });
  });
});

Then if you need to get information about the parent you can use modelFor. So if you were in posts, you could do this.modelFor('user');

Here's a jsbin that demonstrates this.

Hope this is helpful.

Gunslinger answered 21/6, 2013 at 18:16 Comment(6)
Indeed, this works and feels ember-native. I still feel that it is not straightforward to figure out the use of "empty" ({{outlet}} only) templates and the right naming and nesting of resource/route definitions in the Router. Thanks for clarifying. In my particular app I actually need this to be partially nested. I'll edit the original post to add my final version.Todhunter
If you see above, at the end of my update I still need a seemingly unnecessary { path: '' } for the .index route or nothing works (Ember complains that the users route can't be found). Kind of odd but overall much better than before. Thanks again!Todhunter
Your implementation is slightly incorrect. I made a new example to better demonstrate what you're trying to do. jsbin.com/uzahuy/2/editGunslinger
Also, look at the console output to see what routes are being displayed on screen.Gunslinger
Well, not exactly. In your example all the templates replace each other. In my case, I need the first two to be nested and then the third should replace the previous 2. Then nesting should happen again. Like in the little ASCII hierarchy diagram. That's why I need that funky routes arrangement. There may still be a better way to do it though.Todhunter
Almost. One thing that makes it a bit confusing is that you have <h2>users/index</h2> on two templates. But if you rename the second one to user/index (user singular) then you'll notice that the first three levels replace each other. See here: jsbin.com/unoduy/1/edit. Only the last template is embedded. I was trying to do it in two steps: first two nested, second two nested but replacing first two. I guess it's a corner case but it works with the setup I described above. Thank you for your time and the examples. They've been very helpful.Todhunter

© 2022 - 2024 — McMap. All rights reserved.