Idiomatic Emberjs for nested routes but non-nested templates - ember.js

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.

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.

Related

How are ember links to reused nested routes defined using link-to?

Using the example route defined below how can a link be defined to /post/123/comments?
Router.map(function() {
this.route('post', { path: '/post/:post_id' }, function() {
this.route('edit');
this.route('comments', { resetNamespace: true }, function() {
this.route('new');
});
});
this.route('comments');
});
Since resetNamespace: true is set on comments, the route post.comments does not exist. Otherwise the following would work.
{{#link-to "post.comments" "123"}}Link{{/link-to}}
When trying to link to news using the comment id, the error More context objects were passed than there are dynamic segments for the route: comments occurs.
{{#link-to "comments" "123"}}Link{{/link-to}}
An example of the second case can be seen here: https://ember-twiddle.com/d9f3b5e692573c80e803
Thanks to locks for pointing me in the direction. The specific issue is how to reuse as much of the route and template as possible for different paths.
router.js:
Router.map(function() {
this.route('post', { path: '/post/:post_id' }, function() {
this.route('edit');
this.route('comments', function() {
this.route('new');
});
});
this.route('comments');
});
routes/post/comments.js:
import Comments from '../comments';
export default Comments.extend({
renderTemplate() {
this.render('comments');
}
});
This will extend the existing comments route to reuse the model and actions defined in the base route. The renderTemplate is still necessary to load the comments template instead of the post.comments template.
Since you're resetting the namespace, you are effectively overriding one of the comments route with the other. You're doing something akin to
obj['comments'] = firstRoute;
obj['comments'] = secondRoute;
Route names need to be unique, which they would be if you didn't reset the namespace, as well as the path. If two routes have the /foobar path, then Ember won't know which one to transition to.
If what you want is to reuse parts from other routes, there are ways to do that.

Ember access array controller's content from another controller

I have two controllers: postsController and postController. the post route is NOT nested under posts ( I do not want to do so because I want the posts view to be replaced by the post view, not added to it.)
This is what my router looks like.
this.resource('posts', {path: '/'}, function(){
// this.route('index', {path: '/'});
});
this.resource('post', { path: 'posts/:id' }, function(){
this.route('show');
}
Now, the postsController:
App.PostsRoute = Ember.Route.extend({
model: function(){
return this.store.find('post');
},
})
App.PostsController = Ember.ArrayController.extend({
testProperty: "This is a test"
})
And the PostController:
App.PostRoute = Ember.Route.extend({
model: function(params){
return this.store.find('post', params.id);
},
})
App.PostController = Ember.ObjectController.extend({
needs: ['posts'],
percentValue: 100,
progressBarWidth: null,
advancePost: function(delta) {
var that = this;
var posts = that.get('controllers.posts');
// debugger <-- This is where all my questions are concerned with.
...
},
actions: {
nextPost: function() {
this.advancePost(1);
},
previousPost: function() {
this.advancePost(-1);
},
}
})
So. PostController 'needs' PostsController, and on action advancePost, needs to access PostsController's model (which is supposed to contain an array of Post Objects.) At the point where my debugger is, the variable posts accessed postsController. At this point, if I run
posts.get('testProperty') // => gives "This is a test", which is correct.
on the chrome console, I get "This is a test". So I know that I have gained access to the Posts Controller itself. However, any of the following attempts:
posts.get('model') // gives []??
posts.get('content') // gives []??
returns an empty array ( [] ).
I fail to understand why. If I have access to the Posts arrayController, why do I not have access to its data? Consequently, how would I gain access to its data?
I would greatly appreciate clarity on this.
First, if I understand your context, I think you should rather nest post under posts by doing this :
this.resource('posts', {path: '/'}, function(){
this.route('post', {path: '/:id'});
});
And then, since posts is your "parent" route, you no longer need to declare the needs dependency in your controller.
To access get your posts, apply the following in your Route :
setupController: function(controller, model) {
this._super(controller, model);
controller.set('posts', this.modelFor("posts").get("content"));
}
You should think about the interaction patterns you want to support. Is it typical for a user to go back to the list of items they were just looking at? Reloading posts whilst keeping the scroll position in the list they were at will be problematic. Also re-fetching from the server and tearing down and setting up the DOM has a lot more overhead than just leaving it there.
Are you intending to use animation on your transitions? Using something like liquid-fire will need both outlets rendered to perform an animation so it makes no sense to tear down the list in that case.
CSS can be used to have the nested outlet fill whatever container it is put in allowing the approach provided by Pascal Boutin to be used.

Ember router naming conventions

I have a need for deep nesting some routes in ember, I have something like this.
this.resource('wizards', {
path: '/wizards'
}, function() {
this.resource('wizards.google', {
path: '/google'
}, function() {
this.resource('wizards.google.register', {
path: '/register'
}, function() {
this.route('step1');
this.route('step2');
this.route('step3');
this.route('summary');
});
});
});
What I was expecting was as structure like this:
url /wizards/google/register/step1
route name wizards.google.register.step1
route Wizards.Google.Register.Step1Route
Controller Wizards.Google.Register.Step1Controller
template wizards/google/register/step1
but I got this:
url /wizards/google/register/step1 //as expected
route name wizards.google.register.step1 //as expected
route WizardsGoogle.Register.Step1Route
Controller WizardsGoogle.Register.Step1Controller
template wizards/google.register.step1
What I don't get is when does ember stop using capitalization (WizardsGoogle) and start using namespaces (WizardsGoogle.Register). The seemingly inconsistency confuses me. I would have expected either of them.
I met the same things with deep nested resources. Although I didn't know how this happens, what I can tell is that you can always use CapitalizedNestedRoute without namespace, and Ember can recognize it. Although in Ember Inspector it displays "WizardsGoogle.Register.Step1Route".
In your example I defined such route:
App = Em.Application.create();
App.Router.map(function() {
this.resource('wizards', function() {
this.resource('wizards.google', function() {
this.resource('wizards.google.register', function() {
this.route('step1');
this.route('step2');
this.route('step3');
});
});
});
});
App.IndexRoute = Em.Route.extend({
beforeModel: function() {
// Transition to step1 route
this.transitionTo('wizards.google.register.step1');
}
});
App.WizardsGoogleRegisterStep1Route = Em.Route.extend({
model: function() {
// You can see this alert when you enter index page.
alert('a');
}
});
In this example the app will transition to WizardsGoogleRegisterStep1Route with no problem. And if you use container to find route like this:
App.__container__.lookup('route:wizards.google.register.step1').constructor
It will also display App.WizardsGoogleRegisterStep1Route. It's the same as Ember Guide describes. http://emberjs.com/guides/routing/defining-your-routes/#toc_nested-resources And Ember Guide doesn't introduce namespace route.
So I think it's better to according to what Ember Guide suggests (always use CapitalizedNestedRoute). And in my opinion it's easier to define CapitalizedNestedRoute than nested.namespace.route.
Finally, if you really want to use namespace route/controller/template, you can have a look at Ember.DefaultResolver. Check the API to learn how to extend it so container can lookup modules by your own rules.
Routes are "namespaced" inside resources. And resources uses what you call capitalization, where they sort of define a namespace (for routes to use).
So this set of routes:
App.Router.map(function() {
this.resource('posts', function() {
this.route('new');
this.route('old');
this.route('edit');
this.route('whatever');
});
});
Would result in routes with the following name:
PostsRoute
PostsNewRoute
PostsOldRoute
PostsEditRoute
PostsWhateverRoute
Whereas, the following set of routes:
App.Router.map(function() {
this.resource('posts', function() {
this.resource('photos');
this.resource('comments');
this.resource('likes');
this.resource('teets');
});
});
Would result in route with the following names:
PostsRoute
PhotosRoute
CommentsRoute
LikesRoute
TeetsRoute
Also note, that resources within resources don't get "namespaced" to the "parent" resource, so you'll always ever have the form:
{CapitalizedResourceName}Route // for resources
{CapitalizedParentResourceName}{RouteName}Route // for routes
I hope this helps you!

How does EmberJS select template for a Route?

I have these routes defined:
this.resource('projects', function() {
this.resource('project', { path: ':project_id'}, function() {
this.route('details');
this.route('members');
});
});
What I thought was that by convention project.details route would look for "project/details" template. It does but strangely it does not get the correct model. See http://jsbin.com/ELaxigE/19/edit
Now instead of providing "project/details" template if I create "project" template then it works. See http://jsbin.com/ELaxigE/21/edit
I am confused what is happening. Can someone explain?
This has nothing to do with templates. You haven't defined the model for the ProjectDetails route. You can do so like this:
App.ProjectDetailsRoute = Em.Route.extend({
model: function() {
return this.modelFor('project');
}
});
Given a route. When the model hook ins't defined, and have a dynamic segment that ends with _id:
this.route('edit', { path: ':user_id' });
This will generate a route like this:
App.EditRoute = Ember.Route.extend({
model: function(params) {
return App.User.find(params.id);
}
});
In your case the only dynamic segmented route is project, because the :project_id.
this.resource('project', { path: ':project_id'}, function() { ... });
So because details and members, are just normal routes, it doesn't have a model.
When you change the template project/details to project, the things work because:
You transition to project.details, first is transitioned to project route, since you have declared this.resource('project'...). And because it's a dynamic segmented route, the App.Project instance is returned, and the your template is rendered bound to this model.
After this, the child route project.details is transitioned, but this time, the template project.details not exist. So nothing is rendered.
I think that the solutions are the #alexspeller answer, or:
this.resource('project', function() {
this.route('details', { path: 'details/:project_id' });
this.route('members', { path: 'members/:project_id' });
});
I hope it helps.

How to render into parent resource's index {{outlet}}

I have 2 nested resources, Post and Comment in an Ember router. I want this to reflect the url:
/posts/1/comments/1
Going to this page should,
Render the Post for id = 1 using post/index template.
Render the Comment for the Post with Comment id = 1 using the comment/index template
Here is the example on jsbin.
My routing code is,
App.Router.map(function() {
this.resource('home', { path: '/' });
this.resource('posts', { path: '/posts' }, function() {
this.resource('post', { path: ':post_id' }, function() {
this.resource('comments', { path: 'comments' }, function() {
this.resource('comment', { path: ':comment_id' }, function() {
// need explicit index
});
});
});
});
});
The templates and the rest of the Ember code is pretty much stock. Only thing different is I am redirecting to /posts/1/comments/1 from the home route.
I can't get the post or the comment to render inside the /index template. Both the Post body and Comment Body are blank.
It works if I embed the contents of the index template inside the main posts or comments template. But this isn't what I need, the comment needs to be nested inside the Post.
Any ideas how to get this working? Thanks.
PS: I am using ember-latest and ember-data latest.
Usually this sort of problem comes down to naming conventions. Probably ember is looking for controllers and templates that you do not expect. Try adding the following to your application then watch console, it will help you see what routes/controllers/templates are being used.
App = Ember.Application.create({
LOG_ACTIVE_GENERATION: true,
LOG_TRANSITIONS: true,
LOG_VIEW_LOOKUPS: true
});
Also, those routes are probably more complicated than they need to be. Typically it does not make sense for 'post' route to be nested within a 'posts' resource. Same goes for comment/comments. Try this instead:
App.Router.map(function() {
this.route('posts');
this.resource('post', { path: '/posts/:post_id' }, function() {
this.route('comments')
this.resource('comment', { path: '/comments/:comment_id' });
});
});