Fetching an async hasMany relationship in the model hook - ember.js

Lets say I have two models, Book and Chapter.
App.Book = DS.Model.extend({
name: DS.attr(),
chapters: DS.hasMany('chapter', { async: true })
});
App.Chapter = DS.Model.extend({
name: DS.attr(),
book: DS.belongsTo('book')
});
I'm using the RESTAdapter.
App.ApplicationAdapter = DS.RESTAdapter;
In the IndexRoute let's say I want to fetch the first book (id = 1) and it's associated chapters. Since the hasMany relationship is marked as async: true, I want to fetch this association before the template is rendered.
App.IndexRoute = Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
book: this.store.find('book', 1)
}).then(function(hash) {
hash.chapters = hash.book.get('chapters');
return hash;
})
},
setupController: function(controller, model) {
controller.set('model', model.book);
}
});
My index template simply displays the book title, and iterates over its chapters.
<script type="text/x-handlebars" id="index">
<h3>Book: {{name}}</h3>
<ul>
{{#each chapters}}
<li>{{name}}</li>
{{/each}}
</ul>
</script>
When using mockjax, I setup the mock responses.
$.mockjax({
url: '/books/1',
responseText: {
book: {
id: 1,
name: 'Book 1',
chapters: [1, 2]
}
}
});
$.mockjax({
url: '/chapters?ids[]=1&ids[]=2',
responseText: {
chapters: [{
id: 1,
name: 'Chapter 1',
book_id: 1
}, {
id: 2,
name: 'Chapter 2',
book_id: 1
}]
}
});
Problem 1: According to the RESTAdapter docs, accessing book.get('chapters') should issue a GET request to /chapters?ids[]=1&ids[]=2. However, my console is showing two separate requests to /chapters/1 and /chapters/2.
Problem 2: Furthermore, I believe the template is rendered before the chapters requests are happening because the template is rendered a second or two before I see the two requests to /chapters/1 and /chapters/2. If I remove the call to hash.book.get('chapters') from the route, the same problem happens. In other words, I don't think the route is sending the request, I think the template is.
Here is a jsbin. You'll notice it doesn't show any chapters because I haven't setup the two routes it's requesting (/chapters/1 and /chapters/2).

Cause for the first issue is simple: you used Dev(Canary) build of Ember Data which behaves differently to a beta build you can find on EmberJS site. It is bleeding edge so it's hard to tell if it's broken or if those are upcoming changes. If you want behavior consistent with the guide I would suggest using the latest beta build (Ember Data isn't marked as stable yet).
As for the second one: this is desired behavior since you're using async request along with Promises. This means, that you're immediately returning the result (there is no need for waiting for anything), however those results are empty until asynchronous requests are resolved.
You could wrap this behavior and (for example) inject a template only when your async routine is finished, however I would suggest rethinking it since based on personal experience it's not common pattern and most likely your app don't need it.
If you're are interested in how exactly those mechanisms work (you don't need this, it's only to satisfy potential curiosity on subject):
Article on JavaScript threading and pseudo multi-threading
Wikipedia - Definition of AJAX
Ember.RSVP.Promise - Promise used in Ember.js
Kriskowal's Q - Non-related library but I think it greatly introduces the concept of Promises
Ember.js Guide - Approach to asynchrony
Ember.js Guide - Handling of templates with async relationships

Related

Could someone explain how async works with belongsTo/hasMany relationships?

I had this code in my ember app:
var FooModel = DS.Model.extend({
bars: DS.hasMany( 'bar', { async: true, inverse: 'foo'} )
});
var BarModel = DS.Model.extend({
foo: DS.belongsTo( 'foo', { async: true, inverse: 'bars'} )
});
Edit: Using
Ember : 1.13.7
Ember Data : 1.13.8
But when I went to render foo.bars, they wouldn't be loaded unless I used the browser back and forward buttons. Reloading the page would cause the foo.bars to disappear again.
When I changed the code to this:
var FooModel = DS.Model.extend({
bars: DS.hasMany( 'bar', { async: true } )
});
var BarModel = DS.Model.extend({
});
Everything works as it should, and I'm just really confused as to why. Especially since I took that original code from another ember app where it was working just fine (although there might have been some adapter/serializer magic going on that I don't know about). Edit: The app where it was working is using
Ember : 1.4.0
Ember Data : 1.0.0-beta.7+canary.b45e23ba
Handlebars : 1.3.0
Edit: Using REST adapter for both
Async in relatioships
{ async: true} is the default value for relationships in Ember since 1.13 which means that in most cases you define the relationship without specifying the async value at all. It means that related records will not be loaded into the store until required. This is almost always the desired way to go since it prevents blocking by returning a promise. Remember that promises require a different way of programming since the process no longer is procedural step by step through the code. You manage promises by chaining a .then(function(param){// handle fulfilled promise here});.
There are cases where { async: false} could be beneficial. For example in a one to one relationship and you wanted both sides of the relationship to be loaded immediately.
Why your code does not work
I don't know for sure but it seems that the code as you have written it and it could be a bug in the code. The same as with async above you also do not actually need to specify the inverse value here either. According to Ember documentation:
Ember Data will do its best to discover which relationships map to one another. Explicit Inverses
Your application as a simple one to many relationship should work fine simply with:
var FooModel = DS.Model.extend({
bars: DS.hasMany( 'bar' )
});
var BarModel = DS.Model.extend({
foo: DS.belongsTo( 'foo' )
});
I have replied a similar question What is an “async relationship”? / {async: true} vs. {async: false}
Async relationships (default)
Accessing the relationship will return a promise.
post.get('comments').then((comments) => {
// now we can work with the comments
});
If the data is not available and the relationship is accessed (for example, from a template or a "consumed" computed property), Ember Data will automatically fetch the resources.
Read more about Relationships as Promises
Sync relationships (from docs)
Ember Data resolves sync relationships with the related resources
available in its local store, hence it is expected these resources
to be loaded before or along-side the primary resource.
BelongsTo
export default DS.Model.extend({
post: DS.belongsTo('post', {
async: false
})
});
In contrast to async relationship, accessing a sync relationship
will always return the record (Model instance) for the existing
local resource, or null. But it will error on access when
a related resource is known to exist and it has not been loaded.
let post = comment.get('post');
HasMany
export default DS.Model.extend({
comments: DS.hasMany('comment', {
async: false
})
});
In contrast to async relationship, accessing a sync relationship
will always return a DS.ManyArray instance
containing the existing local resources. But it will error on access
when any of the known related resources have not been loaded.
post.get('comments').forEach((comment) => {
});
If you are using links with sync relationships, you have to use
ref.reload to fetch the resources.

How to get model properties

I have model:
App.Item = DS.Model.extend({
itemId: DS.attr('string'),
itemName: DS.attr('string'),
itemType: DS.attr('string'),
});
I successfully create some items from JSON. I can put them to page by {{#each items}}{{ itemName}}{{/each}}. But I don't know, how to get itemName in javascript.
I tried this:
var item = App.Item.find(1);
console.log(item.itemName);
--> undefined
I can't find anything useful from emberjs and ember-data docs. Can anyone help me?
Thanks
I tried this:
var item = App.Item.find(1);
console.log(item.itemName);
--> undefined
This is normal because the call to .find(1); is asyncronous and returns a promise and not the item you are expecting.
Therefore you should try:
App.Item.find(1).then(function(result) {
console.log(record.get('itemName'));
});
It also depends from where you are doing App.Item.find() if it's from inside a route you should wait until the afterModel hook is called to access your items:
App.FooRoute = Ember.Route.extend({
model: function() {
return App.Item.find(1);
},
afterModel: function(record) {
console.log(record.get('itemName'));
}
});
Also be aware that if you where calling find() without parameter then you will receive a RecordArray which you need to loop over to get access to your items. Also worth mentioning is that in ember you should always use .get() and .set() instead of the vanilla dot-notation otherwise you hijack the binding mecanism resulting in no updates in your view etc.
Note, if you are using the latest ember.js release (1.0.0) then the call to .find() should be made somewhat different. But that's not clear from your question.
Hope it helps.

Ember todos: an Ember.CollectionView's content must implement Ember.Array

I'm trying to get my head around Ember and going through the todos tutorial. I get stuck on the displaying-model-data step here
http://emberjs.com/guides/getting-started/displaying-model-data/
here's the javascript i copied and pasted from the tutorial:
window.Todos = Ember.Application.create();
Todos.Router.map(function () {
this.resource('todos', { path: '/' });
});
Todos.TodosRoute = Ember.Route.extend({
model: function () {
return Todos.Todo.find();
}
});
Todos.Store = DS.Store.extend({
revision: 12,
adapter: 'DS.FixtureAdapter'
});
Todos.Todo = DS.Model.extend({
title: DS.attr('string'),
isCompleted: DS.attr('boolean')
});
Todos.Todo.FIXTURES = [
{
id: 1,
title: 'Learn Ember.js',
isCompleted: true
},
{
id: 2,
title: '...',
isCompleted: false
},
{
id: 3,
title: 'Profit!',
isCompleted: false
}
];
Then here's my handlebars template:
...
{{#each controller}}
<li>
<input type="checkbox" class="toggle">
<label>{{title}}</label><button class="destroy"></button>
</li>
{{/each}}
And yet I get this error
Uncaught Error: assertion failed: an Ember.CollectionView's content must implement Ember.Array. You passed <(generated todos controller):ember257>
It looks to me like whatever default controller object Ember generates should be of type Ember.Array but it is not happening for some reason. I am wondering if it is a problem with ember-data?
I am using all the files from the starter kit which are
ember 1.0.0 rc5
handlebars 1.0.0 rc4
jquery 1.9.1
and ember-data, the only versioning indication i can tell is from a comment
// Last commit: 3981a7c (2013-05-28 05:00:14 -0700)
Is there a dependency problem someone knows about or did I do something wrong?
I wouldn't say its a problem with ember data, since that module is responsible only for talking to the api and giving you clever model objects.
You were right in saying ember is generating the wrong type of controller. By default Ember will probably generate a Controller, when what you need is an ArrayController. To get around the issue, simply create an empty controller like this
Todo.TodosController = Em.ArrayController.extend({});
The guide does say that ember creates an ArrayController, but perhaps it doesn't anymore!? let me know if it works by explicitly creating an arraycontroller. If it does we can let the ember team know.
I had this exact same issue today walking through the Getting Started Guide but it appeared to be due to a typo.
According to the documentation, the generated controller is supposed to be of type ArrayController. I dug into the Ember source and found the Ember.generateController method that generates the controller depending on the context. I set a break point and found that when Ember was trying to create a controller for the "Todos" route, the context was undefined, so the basic controller was generated.
Working backward from there, I set a breakpoint on the model function of my router to see what it was returning but found it was not being called at all. At this point, I began to get suspicious that I had done something wrong. And that is when I noticed that I had named the TodosRoute as TodosRouter (as you have in your original question). Changing the name to TodosRoute correctly called my model function and everything worked as expected. It was not necessary to include the line that explicitly created the TodosController as an ArrayController.
While it appears you had it correct in your question, I wanted to post this here in case someone else has the same issue.
Adding the line Gevious suggested corrected this issue for me. For clarification my router.js file now looks like this:
Todos.Router.map(function(){
this.resource('todos', {path: '/'});
});
Todos.TodosRoute = Ember.Route.extend({
model: function () {
return Todos.Todo.find();
}
});
Todos.TodosController = Em.ArrayController.extend({});

Ember.js sorting and filtering children of a hasMany relationship in parent route

Update #2
I found that when I refactored the filtering logic to take place in a compound computed property within the PostController instead of within individual routes, I was able to get it working. The solution was ultimately dependent upon a single dynamic variable set by the specific #linkTo route action that triggered filtering changes within a PostController computed property. I have a lot of work to catch up on so I can't post the solution to this specific question now, but when I can I will detail an explanation of the solution below. For now I have marked #twinturbo's answer as correct for the partial but incredibly helpful guidance he gave below. Thanks again man!! Much appreciated!!
Update #1
The latest fiddle is at http://jsfiddle.net/aZNRu/14/ with #twinturbo's help, sorting the "rank" attribute of Comments in its Post parent controller is working, along with basic filtering. Still having the problem of not getting auto updating views when in a filtered route and a new comment is created.
Original Question
I see that there is talk of combining the sortable mixin with filtering functionality, but for now, as you can see in my jsfiddle example, I'm having issues with both sorting and filtering:
1) I can't figure out how to sort by a specific child attribute in the controller of its parent. If we have:
App.Post = DS.Model.extend({
title: DS.attr('string'),
post: DS.attr('string'),
comments: DS.hasMany('App.Comment')
});
App.Comment = DS.Model.extend({
post: DS.belongsTo('App.Post'),
description: DS.attr('string'),
isActive: DS.attr('boolean'),
rank: DS.attr('number')
});
App.Router.map(function() {
this.resource("posts", { path: "/" }, function() {
this.resource('post', { path: ':post_id' }, function() {
this.route('active');
this.route('inactive');
});
});
});
I want to be able to sort each post's comments in ascending order by it's "rank" attribute. I want to do something like:
App.PostController = Ember.ObjectController.extend({
sortProperties: ['comments.rank']
but for one, I think sortProperties only works on arrayControllers, and I don't think it can work more than one level deep. How could I achieve this?
2) The second problem is not getting auto-updating views when in a filtered route. For example, if you view the jsfiddle and go into the active filtered route by clicking "Active Comments" you get a nice filtering effect on the current data. But if you remain in the active route and create a new record that is active by clicking "Add New Comment," the record does not automatically render under "Active," and only appears if you click on another route and then return to it.
Am I setting up the route filtering incorrectly in the route or referencing it wrong in the template?
App.PostActiveRoute = Ember.Route.extend({
setupController: function() {
var post = this.controllerFor('post').get('model'),
comments = post.get('comments');
var activeComments = comments.filter(function(comment) {
if (comment.get('isActive')) { return true; }
});
this.controllerFor('post').set('filteredComments', activeComments);
}
});
<ul>
{{#each comment in filteredComments}}
<li>{{comment.rank}} {{comment.description}} - isActive: {{comment.isActive}}</li>
{{/each}}
</ul>
Any insight you could give on these issues would be greatly appreciated!
but for one, I think sortProperties only works on arrayControllers, and I don't think it can work more than one level deep. How could I achieve this?
You are correct that sortProperties only works on Ember.ArrayController.
You really don't need to do anything fancy to achieve this. Simply wrap the comments array in a new ArrayProxy that includes the sortable mixin. Then you can sort the comments. Then you don't need a nest property because you're sorting an array of comments.
Please don't extend DS.ManyArray. There is no need for that.
As for sorting and filtering, you need to use composition here. That means creating something like filteredContent and sortedContent. Then you can have sortedContent use filteredContent.
Update:
PostController = Ember.ObjectController.extend({
comments: (function() {
return Ember.ArrayProxy.createWithMixins(Ember.SortableMixin, {
sortProperties: ['rank'],
content: this.get('content.comments')
});
}).property('content.comments')
This can be done with a computed property macro, too.
PostController = Ember.ObjectController.extend({
commentSortProperties: ['rank:desc', 'createdAt:asc'],
comments: Em.computed.sort('model.comments', 'commentSortProperties')
});
Take a look at the code here.
Perhaps you can make your many-arrays sortable by extending the store and add the sortable mixin in the createManyArray method? (I did not test if it works)
App.store = DS.Store.create({
createManyArray: function (type, clientIds) {
var array = DS.ManyArray.create(Ember.SortableMixin, { type: type, content: clientIds, store: this });
clientIds.forEach(function (clientId) {
var recordArrays = this.recordArraysForClientId(clientId);
recordArrays.add(array);
}, this);
return array;
}
});
then you can bind to the arrangedContent of the array
{#each comment in filteredComments.arrangedContent}}

failed to get embedded's object property using ember.js with ember-data

I'm new to ember, and try to understand how it works.
I've defined a store with a fixturesAdapter as adapter (rev 7).
I've defined two models:
App.Tag = DS.Model.extend({
name: DS.attr('string'),
item: DS.belongsTo('App.Item')
});
and:
App.Item = DS.Model.extend({
name: DS.attr('string'),
tags: DS.hasMany(App.Tag, { embedded:true }),
})
I also fill their associated fixtures and at last a controller:
App.itemsController = Ember.ArrayController.create({
content: App.store.findAll(App.Item)
});
I've defined a function inside App.Item model:
tagline: function(){
return this.get('tags').toArray().map(function(tag){
return tag.get('name');
}).join(',');
}.property('tags.#each.isLoaded')
Here is the corresponding jsfiddle: http://jsfiddle.net/K286Q/29/
My questions are:
What am I doing wrong?
Why does it see several tags associated to first item, but is not able to get their name?
You're running up against a few breaking changes in the current version of ember-data.
The first is that, since revision 6 of ember-data, IDs are string-normalized and must be represented as strings in fixtures. Note that the REST adapter will convert numbers/strings, but the fixture adapter doesn't do any conversions. This is a common source of confusion (see the previous question).
The second is that support for embedded data objects has been temporarily removed from ember-data. I'm pretty sure that this feature will be re-introduced in a better way than supporting {embedded: true} in the attributes. IMO, embedding is more of an adapter concern and doesn't really belong with the definition of the model.
I adjusted your fixtures and got your example working here: http://jsfiddle.net/dgeb/zHz4Y/