Data not shown when directly accessing a URL in nested Ember Route - javascript

I have a nested route setup as I would like to have my templates nested as well. The route setup looks like this:
...
this.route('posts', function() {
this.route('post', {path: ':post_id'}, function() {
this.route('comments', {path: 'comments'}, function() {
this.route('comment', {path: ':comment_id'});
});
});
});
...
Potentially, my URL could look something like this:
/posts/:post_id/comments/:comment_id
If I navigate via {{link-to}} then I have no problem, however, if I go directly to a specific URL via the browser, that's when things going wrong. Imagine that my comments page lists the comments associated with the post (post_id) I'm looking at. The problem is going there directly, like typing in /posts/123/comments/56 doesn't load the list of comments, only the comment itself. Here is how my routes are setup:
// routes/posts.js
...
model() {
return this.get('store').findAll('post');
}
...
// routes/posts/post.js
...
model(params) {
return this.get('store').findRecord('post', params.post_id);
}
...
// routes/posts/post/comments.js
...
model(params) {
let post=this.modelFor('posts/post');
return post.get('comments');
}
...
// routes/posts/post/comments/comment.js
...
model(params) {
return this.store.findRecord('comment', params.comment_id);
}
...
If I type in /posts/123/comments/56, no list is displayed from the comments.js template.
If I type in /posts/123/comments, only the first comment is displayed from the comments.js.
I'm not sure what I'm doing wrong though I feel like it has something to do with modelFor. How do I properly populate/hydrate the post's comments list when I go directly to a comment in that post URL rather than navigating there via link-to?
UPDATE 1:
Here are the models:
// post.js
export default DS.Model.extend({
title: DS.attr("string"),
articles: DS.hasMany('comment', {async: true})
});
// comment.js
export default DS.Model.extend({
title: DS.attr("string")
});
I've connected it to mirage.
Here is a sample of the comments fixture:
[
{
id: 1,
title: "Apple"
},
...
and the posts fixture:
[
{
id: 1,
title: "A post with fruits for comments",
commentIds: [1, 2]
},
...
UPDATE 2:
I've added my template code for templates/posts/post/comments.js
Comments.js!
{{outlet}}
<ol>
{{#each model as |comment|}}
<li>{{#link-to "posts.post.comments.comment" comment}} {{comment.title}}{{/link-to}}</li>
{{/each}}
</ol>
I've also updated the comment.js route above, reducing its complexity.

Based on your router definitions - the URL /post/123/comments/56 does not exists. Your router defines URL with the following structure /posts/56/comment/123 (notice singular comment not comments) - your route name is comments however you told ember that the path it should resolve is comment - singular. So, try accessing /post/123/comment/56 instead of comments.
Also, no reason to call this.modelFor('posts/post') from nested comments route - all you should need is modeFor('post').
And the last thing, inside the comment route you should only need to look for a give comment no need to resolve the parent model - you already have a unique comment_id - store.findRecord('comment', comment_id) should work.

Related

Ember not calling setupController of router

So, I have two paths in my route. I created the two routes as the doc recommends.
My router is the following:
// router.js
Router.map(function() {
this.route('photos');
this.route('photo', { path: '/photo/:photo_id' });
});
If I visit firstly the route /photo/ID and then go to /photos, it will only show one object on the latter. (wrong)
If I visit /photos first it shows all the objects and I can go to /photo/ID later on and it will be fine. (right)
I want to make it work both ways. How to do this? You can see my code for each route down below:
// photo.js
export default Ember.Route.extend({
model(params) {
return this.get('store').findRecord('photo', params.photo_id);
}
});
// photos.js
export default Ember.Route.extend({
setupController(controller, model) {
let photos = this.get('store').findAll('photo');
console.log('add all...');
// convert to an array so I can modify it later
photos.then(()=> {
controller.set('photos', photos.toArray());
});
},
});
I can always call the findAll() function regardless where the user goes, but I don't think this is smart.
The way I am dealing with the page transitions:
To go to photos I use:
{{#link-to 'photos'}}All{{/link-to}}
To go to /photo/ID I inject the service '-routing' and I use in one event click like this:
routing: Ember.inject.service('-routing'),
actions() {
selectRow(row) {
this.get("routing").transitionTo('photo', [row.get('id')]);
}
}
findAll will get it from a store and return immediately and later on it will request the server and update the store. but in your case, as you are not using route model hook, so this live array will not be updated so it will not reflect it in the template.
If I visit firstly the route /photo/ID and then go to /photos, it will
only show one object on the latter.
In the above case, store will contain only one reocrd, so when you ask for store data using findAll it will return the existing single record.
Another option is,
avoiding this photos.toArray() - It will break live-array update, I am not sure why do you need it here. since photos is DS.RecordArray.
Note: It's important to note that DS.RecordArray is not a JavaScript
array, it's an object that implements Ember.Enumerable. This is
important because, for example, if you want to retrieve records by
index, the [] notation will not work--you'll have to use
objectAt(index) instead.

Trouble with nested resources

I'm new to Ember, and I think I bit off more than I can chew with this practice app, but I intend to learn. I might be completely conceptually off, if so, feel free to offer a better structure for my use case.
My (abbreviated) routing looks more or less like this:
Router.map(function() {
this.resource('shops', { path: '/' }, function() {
this.resource('shop', { path: ':shop_id' }, function() {
this.resource('items', { path: 'admin'}, function() {
});
});
});
});
The intention is that the user will select a shop, then get a list of all possible items with checkboxes where he can decide which are available in that shop and which aren't. So far, I'm just trying to display the list of all items, but it's not working. However, the list of shops - no problem whatsoever.
URL: /
Works. model is all shops.
URL: /1
Works. model is the shop with ID 1.
URL: /1/admin
Error while processing route: items.index Assertion Failed: ArrayProxy expects an Array or Ember.ArrayProxy, but you passed object
Both shops and items controllers are identical:
// app/controllers/shops.js
// app/controllers/items.js
export default Ember.ArrayController.extend({});
The routes are nearly identical:
// app/routes/shops/index.js
export default Ember.Route.extend({
model: function() {
return this.store.find('shop');
}
});
// app/routes/items/index.js
export default Ember.Route.extend({
model: function() {
return this.store.find('item');
}
});
The shop controller does not exist, and the shop.index route is trivial:
// app/routes/shop/index.js
export default Ember.Route.extend({});
What gives?
EDIT: JSBin
The problem with your JSBin turns out to be rather simple. In the simplified router in your original post you have this.resource('items', { path: 'admin'}, function() {});.
Since you pass a function to this.resource that means it has an implicit nested this.route('index').
However, in your JSBin, you have this.resource('items, { path: 'admin' });.
Since you are not passing a function in this case, there is no implicit index route.
The solution is either to add the function bit, or rename App.ItemsIndexRoute to App.ItemsRoute and data-template-name="items/index" to data-template-name="items".
JSBin with the latter: http://emberjs.jsbin.com/dahuge/2/edit?html,js
P.S. I've also prepared a JSBin using just this.route which is currently more future-friendly: http://jsbin.com/mifamu/9/edit?html,js,output
Answered on IRC by one very helpful "locks". Some problems remain, but the big question has been answered with this JSBin. My biggest confusion came from misunderstanding how URLs are handled, and what the role of link-to helper was. What I needed most was the change in the ItemsController:
App.ItemsController = Ember.ArrayController.extend({
needs: ['shop'],
shop: Ember.computed.alias('controllers.shop.model')
});
which would make shop accessible, and a mistake in a template saying items instead of model.

EmberJS: retrieving model from route when multiple models are retrieved

I have a route that has two models associated with it as shown below:
App.IndexRoute = Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
sites: this.store.find('site'),
songs: this.store.find('song')
})
},
Now later on, I need to be able to retrieve the first object in the sites model in order to do a transition I'll show below. I figured I can set the models using setupController, but when dealing with multiple models as depicated above, I'm not sure how to fill this part in:
setupController: function(controller, ???) {
controller.set('model1', ???);
controller.set('model2', ???);
}
And finally, I'd like to be able to retrieve the first object in model1 (it's multiple instances of site as described above)
afterModel: function() {
firstRecord = this.('sites').objectAt(0);
this.transitionTo('site', firstRecord.id);
}
It's also possible that I'm not designing my application properly. sites in this case is a component I built that displays different sites within a few different controllers. The controllers are dependent on this component in that they need to know which site is selected in order to do their own thing. So in controllers that need access to the component, I do something like:
{{site-nav sites=sites}}
Where site-nav is my component. It needs its own model, as does the controller itself.
First, you're going to need to modify your model hook slightly, to make sure you stay in the right scope:
model:function(){
var self = this;
return Ember.RSVP.hash({
sites: self.store.find('site'),
songs: self.store.find('song')
})
}
To get the different models in setupController, you just access it from the second parameters, like this:
setupController:function(controller,models) {
controller.set('sites',models.sites);
controller.set('songs',models.songs);
}
afterModel provides two parameters, this first being the resolved model for your route, so you'd do it something like this:
afterModel:function(models){
var site = models.sites.get('firstObject');
this.transitionTo('site',site);
}

Ember.js Dynamic segment not working

I'm using Ember 1.0.0 and the latest build of Ember Data (beta), and I have a route with a dynamic segment that isn't working.
I have defined the routes below:
PwdMgr.Router.map(function() {
this.resource("passwords", function(){
this.resource("password", {path: "/:password_id"}, function(){
this.route("edit");
});
});
}
In the template passwords.index I display a list of models like this:
{{#each}}
<tr>
<td>{{id}}</td>
<td>{{name}}</td>
<td>{{client.name}}</td>
<td>{{service.name}}</td>
<td>{{localisation.name}}</td>
<td>{{status.name}}</td>
<td>{{login}}</td>
<td>{{password}}</td>
<td>
{{#link-to 'password.index' this}}<span class="glyphicon glyphicon-search"></span>{{/link-to}}
{{#link-to 'password.edit' this}}<span class="glyphicon glyphicon-pencil"></span>{{/link-to}}
<span class="glyphicon glyphicon-remove" {{action 'edit' password}}></span>
</td>
</tr>
{{/each}}
I have two links, one that goes to the route password.index and one to the route passwword.edit. I provide the model for the dynamic segment and the handlebars creates the URL correctly (/passwords/1 and /passwords/1/edit).
My problem is that when I get to the URL /password/1 (or /password/1/edit), the model is not a single object but an array of objects.
Since I'm using the default pattern, as explained in the guides, I didn't setup Route objects. But for debugging purposes I created a route object for the password.index route. Here's what it looks like:
PwdMgr.PasswordIndexRoute = Ember.Route.extend({
model: function(params){
console.log(params);
return this.get('store').find('password',params.password_id);
},
setupController: function(controller, model){
console.log(model);
}
});
And here's my console log:
Object {} app.js (line 31)
<DS.RecordArray:ember435> { content=[3], store=<DS.Store:ember506>, isLoaded=true, more...} app.js (line 35)
The empty object explains why I get an array of object but is there a reason why the params variable is an empty object?
Thanks a lot
[EDIT]
I have changed my Router.map like so:
PwdMgr.Router.map(function() {
this.resource("passwords", function(){
this.route("detail", {path: "/:password_id"});
this.route("edit", {path: "/:password_id/edit"});
});
}):
And the dynamic segment for both "detail" and "edit" routes works fine. I think the problem comes from the fact that the dynamic segment is in the nested resource, which is strange because the Emberjs guides' examples are with dynamic segments in nested resources.
password/1 doesn't appear to be a real route, I'd try passwords/1, and get rid of the slash on the path.
PwdMgr.Router.map(function() {
this.resource("passwords", function(){
this.resource("password", {path: ":password_id"}, function(){
this.route("edit");
});
});
}
Or change it to
PwdMgr.Router.map(function() {
this.resource("passwords", function(){});
this.resource("password", {path: "password/:password_id"}, function(){
this.route("edit");
});
}
I found my mistake thanks to Daniel's comment.
My routes were setup like this (before my edit):
passwords
password
password.detail
password.edit
password.index
I was using the route PwdMgr.PasswordsRoute to setup my models and its corresponding template passwords.
The problem was that I was in the passwords route and going directly to the password.detail route. I think there was a problem going from passwords to password.detail (or password.edit) with the model parameter.
Anyway, once I changed my route to PwdMgr.PasswordsIndexRoute and the corresponding template to passwords/index, everything was going as expected. Models were passed correctly through the dynamic segment.
Thanks a lot Daniel for pointing out my error.

Ember.js router and dynamic segments

I have been using Ember's Router (v1.0.pre) with single dynamic segments and really happy with it.
So much magic.
However, I'm struggeling with multiple dynamic segments:
What should serialize()/deserialize() return?
How should the transitionTo() call and the contex there look like?
Can somebody shed some light onto this?
serialize and deserialize should only be implemented when your context object has custom serialization (i.e is not an ember-data model instance). So you should not have to implement these methods while using the full ember stack.
transitionTo should be called from routes event handlers, and the context is passed as follow:
showPost: function (router, event) {
var post = event.context;
router.transitionTo('posts.show', post);
}
Given the showPost event has been trigged by action helper like that:
{{#each post in controller}}
<a {{action showPost post}}>Show post {{post.title}}</a>
{{/each}}
More complex transitions can be achieved passing several context objects (for deeply nested routes):
router.transitionTo('posts.member.comments.show', post, comment);
post & comment contexts will be passed to appropriated routes while routing will descend into nested routes.
EDIT
Without ember-data, it would look like:
posts: Ember.Route.extend({
route: 'posts',
member: Ember.Route.extend({
route: '/:post_id',
show: Ember.Route.extend({
route: '/'
}),
comments: Ember.Route.extend({
route: 'comments',
show: Ember.Route.extend({
route: '/:comment_id'
})
})
})
})
And you would have two classes App.Post & App.Comment, with find class methods, and id instances property.
App.Post = Ember.Object.extend({
id: null
});
App.Post.reopenClass({
find: function (id) {
// retrieve data, instanciate & return a new Post
}
});
App.Comment = Ember.Object.extend({
id: null
});
App.Comment.reopenClass({
find: function (id) {
// retrieve data, instanciate & return a new Comment
}
});

Categories