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
}
});
Related
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.
I followed the Query Params guide (http://guides.emberjs.com/v1.11.0/routing/query-params/) and it worked great. Specifically, refreshing the model did exactly what I wanted.
I'm moving the filter to the json-api spec and filtering takes place in a filter object. So rather than:
http://localhost:3000/accounts?id=1
The server responds to:
http://localhost:3000/accounts?filter[id]=1
I tried to get the query params to work refreshing the model based on an object, but it doesn't seem to update.
// app/controllers/accounts/index.js
import Ember from 'ember';
export default Ember.Controller.extend({
queryParams: ['filter', 'sort'],
filter: {},
sort: '-id'
});
// app/routes/accounts/index.js
import Ember from 'ember';
export default Ember.Route.extend({
queryParams: {
filter: { refreshModel: true },
sort: { refreshModel: true }
},
model: function(params) {
return this.store.find('account', params);
},
});
// template
<th>{{input type="text" placeholder="ID" value=filter.id}}</th>
Is it possible to have query params work with an object?
This answer is as of Ember version 1.13.0-beta.1+canary.
The short answer: No. Query params will not work with an object.
The long answer:
As of now, a private function named _serializeQueryParams in the Router serializes the queryParams.
_serializeQueryParams(targetRouteName, queryParams) {
var groupedByUrlKey = {};
forEachQueryParam(this, targetRouteName, queryParams, function(key, value, qp) {
var urlKey = qp.urlKey;
if (!groupedByUrlKey[urlKey]) {
groupedByUrlKey[urlKey] = [];
}
groupedByUrlKey[urlKey].push({
qp: qp,
value: value
});
delete queryParams[key];
});
for (var key in groupedByUrlKey) {
var qps = groupedByUrlKey[key];
var qp = qps[0].qp;
queryParams[qp.urlKey] = qp.route.serializeQueryParam(qps[0].value, qp.urlKey, qp.type);
}
},
qp.urlKey would evaluate to 'filter' in your example, and object would be serialized as 'object [Object]'. Even though you could override the serializeQueryParam method in your route, that wouldn't help because the queryParam key would still be 'filter', and you'd need it to be 'filter%5Bid%5D'
Based on this comment in the Ember Discussion Forum, it sounds like object query params are unlikely, and you'd be better off just flattening and unflattening the filtered fields.
I know this is a bit late but you can use this workaround to allow for objects with query params. It's pretty easy to get working and so far I haven't found any issues with it.
I ran into the same problem when building an Ember app on top of my JSON API (http://jsonapi.org/).
The JSON API specification provides recommended syntax for both paging and filtering that requires object based query params.
For paging it suggests syntax like this:
/books?page[size]=100&page[number]=1
and for filtering it suggest syntax like this:
/books?filter[author]=Bob
While Ember.js Query Params (as of Ember v2.1) do not support this out of the box it is fairly simple to get working. In your controller you should map a controller property to the query param "object" as a string.
So for example, in the above "filter" example you would map a controller property called filterByAuthorValue to the query param filter[author].
The code to do this would look like this:
export default Ember.Controller.extend({
queryParams: ['sort',{
filterByAuthorValue: 'filter[author]'
}],
sort: '',
filterByAuthorValue: ''
});
Note in the example above I also have a query param called sort (which also follows JSON API recommendations but doesn't require an object). For more information on mapping a controller property to a query param see this section of the official Ember.js guide:
http://guides.emberjs.com/v2.1.0/routing/query-params/#toc_map-a-controller-s-property-to-a-different-query-param-key
Once you have the query param created you then need to handle the query param in your router. First, the router should force the model to be refreshed when the controller property filterByAuthor changes:
export default Ember.Route.extend({
queryParams: {
sort: {
refreshModel: true
},
filterByAuthor:{
refreshModel: true
}
}
});
Finally, you now need to translate the controller property filterByAuthor into an actual object when you load the model in the router's model method and assign the value from the controller property filterByAuthor. The full router code would then look like:
export default Ember.Route.extend({
queryParams: {
sort: {
refreshModel: true
},
filterByAuthor:{
refreshModel: true
}
},
model: function(params){
// The params value for filtering the entity name
if(params.filterByAuthor){
params.filter = {};
params.filter['author'] = params.filterByAuthor;
delete params.filterByAuthor;
}
return this.store.query('book', params);
},
});
Settings things up like this allows for an object based query param to be used with Ember and thus follow the JSON API recommendations.
The above has been tested with the following versions:
Ember : 2.1.0
Ember Data : 2.1.0
jQuery : 1.11.3
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.
I am trying to work with Ember.js
Can I expose my data model as JSON through a route or controller?
I have an object like this saved in the store:
this.store.createRecord('Person', {
id: 1,
name: this.get('name'),
email: this.get('email')
});
I want to expose this data from a route or controller as JSON object. I don't want to use any view.
Is it possible to do this?
Thanks for help!
EDIT
My route is:
App.ResultRoute = Ember.Route.extend({
model: function() {
return this.store.find('person', 1);
}
});
There is '1' because I want only this record.
In this way It works and I see in the view the {{name}} and the {{email} of the Person object.
I want to see only the JSON, I tried to do how you suggest me :
App.ResultRoute = Ember.Route.extend({
afterModel: function (model) {
model.get('content').forEach(function (item) {
console.log(item.get('content'));
});
}
});
But I receive this error:
Uncaught Error: Assertion Failed: Error: More context objects were passed than there are dynamic segments for the route: error
What is my error?
The way I would do this would be, I would have an api in my model which would return a plain json object to whoever asked it. So the Person model would have a getPersonDetails method which will hide all the internal details, including the attributes and associations and whatever else, and return the state of the person object it is invoked upon.
So, for example, if you wanted to display a table of persons or something, you would do a createRecord, and just ask the newly created person object for it's details.
Start from the beginning of this guide. http://emberjs.com/guides/routing/specifying-a-routes-model/ It will show you how to specify a model for a route.
Then, read this entire guide on controllers: http://emberjs.com/guides/controllers/
In general, you would access that data from the route's model hook with:
this.store.find('person') // All records
If you wanted to access that first object as JSON, you could do:
var person_JSON = this.store.find('person').then(function (persons) {
//The persons records are now available so you can do whatever you want with them
console.log(persons.objectAt(0).get('content'));
});
You could also iterate over all records and strip out the content to produce raw json without the Ember wrapping... Just depends on what you need to really do.
Really the best place to put this would be the route's afterModel hook, though. You wouldn't be working with a promise, as Ember would have dealt with that for you:
afterModel: function (model) {
model.get('content').forEach(function (item) {
console.log(item.get('content'));
});
}
Hope that helps.
Edit: Since you have one record try this:
afterModel: function (model) {
console.log(model.get('content'));
}
I read a lot of, still can't solve by myself. I've an Ember Route, with az EmberFire model.
App.NewRoute = Ember.Route.extend({
model: function() {
return EmberFire.Object.create({
ref: window.ref
});
},
setupController: function(controller, model) {
controller.set('model', model);
})
After that for example I've in the controller:
App.NewController = Ember.Controller.extend({
actions: {
save: function() {
this.get('model').set('questions', Ember.A([]));
this.get('model').get('questions').pushObject(Ember.Object.create({title: 'foo'}));
this.get('model').get('questions').get('lastObject').set('title', 'bar');
this.get('model').set('title', 'foobar');
}
}
});
When the save action called only the title change is made on fire base.
Can someone please explain how can I changed the array too?
I would change your model from an EmberFire.Object.create to an EmberFire.Array.create. Instead of having an object with an array as a property. EmberFire is a simple wrapper that bridges ember objects to firebase objects. I don't think it works with nested arrays inside an object yet as I just started working with the library.
If you need a property identified to that array in an object, create a second object EmberFire.Object and add a property questions that references this EmberFire.Array firebase object instead.
So your save action would look more like this:
var array = this.get('model');
array.pushObject(Ember.Object.create({title: 'foo'}));
array.get('lastObject').set('title', 'bar');