I thought I understood how the store.findAll and the Promise.All works. But I have run into some strange behavior.
I have two findAll(), but only one of them is fullfilled after the Promise.All gets into the then()-part.
See this code
export default Route.extend({
model() {
var self = this;
return Ember.RSVP.Promise.all([
self.store.findAll('contact'),
self.store.findAll('message')
]).then(function(values) {
var contacts = values[0];
var messages = values[1];
var numberOfContacts = contacts.get('length'); // This is 39 as expected.
var numberOfMessages = messages.get('length'); // This is 0. Expected is 1.
...
There must be something different with messages and contacts, but I cannot figure out what it is. They have very similar models and they have very similar backend API handling. From the network traffic I can see that a message object is returned and if I call (later in the code, after the model hook):
store.peekAll('message');
I get the message object I expect.
I use ember 3.0.0
I figured it out. It is due to a strange behavior of findAll() in Ember.
FindAll() will return immediately with the elements that was already present in the store. Later, when more objects have been retrieved from the server, the store is updated, but the promise of the findAll()-call is long gone.
To work around this strange behavior, there is an option to the findAll() method.
{reload: true}
It is used this way:
return self.store.findAll('message', { reload: true }).then(messages => {
var messageLength = messages.get('length');
...
With this reload-option set, findAll() and promises work as expected.
Related
I have the following code in a controller:
ingredients: function() {
var self = this;
this.store.findAll('ingredient').then(function() {
var ingredients = self.get('model').get('ingredientsWithQuantities').map(function(item) {
return {
name: self.store.peekRecord('ingredient', item.ingredientId).get('name'),
quantity: item.quantity
};
});
self.set('ingredients', ingredients);
});
}.property('model.ingredientsWithQuantities')
It's a computed property related to one of the routes. It works fine if 'ingredients' model is loaded somewhere earlier. But if it's loading for the first time, when then function is called there is no data in the store.
I was expecting that findAll would call then only after data was fetched. How can I handle this?
UPDATE:
When I add shouldReloadAll() { return true; } to my application RestAdapter it works fine except one thing. It actually reloads always even if data exists. But I need just one request of this type during user's session.
It is possible to achieve the next behavior?
when data doesn't exist in the store - then load data and call function after it
when data exists in the store - then just call function
this.store.findAll('ingredient')will return all ingredients in the store and then it will update it with a request to the backend, or that's how I think it works but I'm not sure why it isn't working like that.
A solution is to use this.store.query which will make a request to the backendfor sure.
Example:
return this.store.query('ingredient', { filter: { } }).then(function(result) {
// do something with the result
});
I have a simple hasMany/belongsTo relationship that looks like this
App.Foo = DS.Model.extend({
bar: belongsTo('bar', { async: true})
});
App.Bar = DS.Model.extend({
foos: hasMany('foo', { async: true})
});
I have situations in my route code that fire off a request and when the response comes back I access the related "bar" model as I filter down / etc
this.store.all('foo').clear();
var fooz = self.store.all('foo'); //new code
return this.store.find('foo').then(function(response) {
var filtered = response.filter(function(foo) {
return foo.get('bar').get('name') === 'bazz';
});
//other code that would normally be executed top-down
//including side-effect stuff like this
//self.store.createRecord('foo', someHash);
return fooz; //new code
});
The above doesn't work the first time around as foo.get('bar') is a promise. But this is only a problem the first time through (subsequent $.ajax requests seems to have all the bar objects cached so it's a non issue)
What's strange is that before I even boot the app I've already pulled down all the bar data in the init (shown below). So why does ember-data even need to resolve the promise for "bar" when technically that data should already be in the store locally?
App.initializer({
name: 'bootstrap',
initialize: function() {
App.deferReadiness();
var store = App.__container__.lookup("store:main");
var bars = store.find('bar');
var configurations = store.find('configuration');
Ember.RSVP.all([bars, configurations]).then(results) {
App.advanceReadiness();
});
}
});
Let's separate a few things here
Store cache
this.store.all('foo').clear();
just breaks the internal all filter until a foo record is modified/added/removed forcing the filter to recalculate for the record in the store. I say this to show that clear isn't removing the records from ED's store.
Example (click the button, watch the console, read the fun action code)
http://emberjs.jsbin.com/OxIDiVU/103/edit
That being said it isn't the ajax that's being cached, it's the property/relationship on the record instance that's being cached (and the record).
The proper way to remove the records of a type from the store is store.unloadAll('foo')
Promiselandia
I know you're already familiar with promises, so this part may be worthless, but, worth documenting
The async relationships are really cool because they return PromiseObject/PromiseArray for belongsTo/hasMany. The PromiseObject/PromiseArray extend ObjectProxy/ArrayProxy (these are the same things that ObjectController/ArrayController extend). This essentially gives the PromiseObject/PromiseArray the ability to proxy getting/setting of properties to the model underneath. In this case the setting/getting happens on the promise doesn't "work" until the promise has been resolved (it won't crash, just return undefined). *Caveat, methods don't exist on the promise, so you can't call save on the promise and expect it to work.
Ex. using your models.
var foo = this.store.find('foo', 1);
var bar = foo.get('bar'); // PromiseObject
bar.get('name'); // undefined
later, bar has resolved, bar is still the PromiseObject
bar.get('name'); // billy
foo will keep returning the PromiseObject
var bar2 = foo.get('bar'); // PromiseObject
bar2.get('name'); // billy
saving
bar.save(); // Boom no workey
bar.then(function(realBar){
realBar.save(); // workey
});
In your case I have 3 recommendations
Build your own promise, resolve when you want, use Ember.RSVP.all on the records needed (respecting that they may or may not be resolved yet, hence async)
var self = this;
var promise = new Ember.RSVP.Promise(function(resolve, reject){
self.store.find('foo').then(function(foos) {
Em.RSVP.all(foos.getEach('bar')).then(function(bars){
var filtered = bars.filterBy('name', 'bazz');
resolve(filtered);
});
});
});
return promise;
http://emberjs.jsbin.com/OxIDiVU/104/edit
async properties
A lot of times with async objects/properties that aren't resolved during the model hook (which blocks on promises and wait's until they are resolved) a good trick is to set a placeholder object etc.
var items = [];
controller.set('model', items);
// promise from above
promise.then(function(records){
items.pushObjects(records.toArray()); // toArray may or may not apply
});
http://emberjs.jsbin.com/OxIDiVU/106/edit
I'm having trouble with a computed property.
It's a complex manipulation on an ArrayController. The problem is, Ember attempts to calculate it before the data has loaded. For example, part of it is
var counts = this.getEach('hours').forEach(function(hours) {
var d = hours.find(function(_hour) {
return +(_hour.date.substring(11, 13)) === 10;
});
return d.count;
});
I get an error because this.getEach('hours') returns something like
[ Array[24], undefined ]
while the AJAX request is loading, so the code breaks.
I'm sure others have run into this before - what's the solution?
Update: Here's how I get the data. When a user clicks a month in a view, I pass the clicked month's id to my MonthsController. It has a toggleMonth method:
App.MonthsController = Ember.ArrayController.extend({
toggleMonth: function(id) {
var month = App.Month.find(id),
index = this.indexOf(month);
if (index === -1) {
this.pushObject(month);
} else {
this.removeAt(index);
}
}
});
App.Month.find(id) sends the correct AjAX request + the data returns, but perhaps this is not the correct way to populate the months controller.
Also, this is happening within the IndexRoute (i.e. I have no separate route for the MonthsController. So, I never specify a model hook or setupController for the MonthsController.
The general approach to this problem is promises: asynchronous requests immediately return a promise, which is basically a promise of value, which can be resolved later down the line. All Ember models are promises behind the scenes. See ember models as promises, and How are Ember's Promises related to Promises in general, and specifically jQuery's Promises?
Could you explain the context of the first block of code? What is this in this.getEach('hours').forEach and when is that block executed?
I would like to load an entire collection and then just peel off records to use as models one at a time, without doing a roundtrip to the server every time.
I've figured out how to use Ember.Deferred to return a promise, but I can't get the promise to resolve at the right time. The following code just outputs "Found 0" ever time:
App.PersonRoute = Ember.Route.extend({
model: function(params) {
var name = "Erik";
var promise = Ember.Deferred.create();
App.people = App.Person.find();
App.people.then(function() {
console.log('Found ' + App.people.get('length'));
var person = App.people.findProperty('name', name)
promise.resolve(person);
});
return promise;
}
});
If I wrap the body of the then() in a setTimeout, and make it wait a couple seconds, everything works great.
Is there another event I can somehow bind to? I tried App.people.on('isLoaded'), but isLoaded is always true.
Thanks!
Is there another event I can somehow bind to?
Indeed there is an event you can listen to and that is didLoad.
I tried App.people.on('isLoaded'), but isLoaded is always true.
As for isLoaded there was a lot of confusion about this, see here for example, the confusion comes from the fact that the isLoaded flag is set to true by design when the store has finished loading the RecordArray for the records, even when initially empty because no record was already available locally. Then when the request to the server comes back the RecordArray will be populated with the records received from the backend, and bindings will kick off and your templates are updated.
As stated in the guides:
A record that is both loaded and clean means that is has received information about its attributes and relationships from the server, and no changes have been made locally on the client.
Was is stated above is what makes didLoad fire.
For more model related events you can listen to have a look at the guides under model lifecycle
Now to your setup, you could rewrite your code to something like this:
App.PersonRoute = Ember.Route.extend({
model: function(params) {
var name = "Erik";
var promise = Ember.Deferred.create();
App.people = App.Person.find();
App.people.on('didLoad', function() {
console.log('Found ' + App.people.get('length'));
var person = App.people.findProperty('name', name)
promise.resolve(person);
});
return promise;
}
});
Hope it helps.
If the promise isn't resolving at the right time, I think it would be a (fairly major)bug in Ember/data. I would suggest filling a bug with Emberjs/data.
I suspect though that your use of the different promise may be causing this bug.
App.Person.find is already returning a promise. You should use that promise itself when returning from the model(). Also to resolve that promise you simply return a result.
The implementation style for Promises that you have used is typically needed when you are integrating some external async api that doesn't support Promises.
I would refactor your model() like so. That might fix your async timing issue.
App.PersonRoute = Ember.Route.extend({
model: function(params) {
var name = "Erik";
var promise = App.Person.find();
promise.then(function() {
console.log('Found ' + App.people.get('length'));
return App.people.findProperty('name', name)
});
return promise;
}
});
I've found the docs on the Q library to be very useful when figuring out best way to use promises. Ember uses RSVP which is a different library but the principles are similar.
I'm having an issue with Ember-Data transactions.
I have a DS.Model like so
App.MyModel = DS.Model.extend({
id: DS.attr(),
answers: DS.hasMany('App.Answer') // another model
});
Then it is initiated later like so, in the a Route
model: function(){
var transaction = this.get('store').transaction();
return transaction.createRecord(App.MyModel, {
id: '1'
});
}
I have a Model that makes a request to my back end server using transaction and commit.
this.get('content.transaction').commit();
With the intent that answers is updated on the server side and sent back to me.
If the content hasn't been updated yet, I call this
this.get('content').reload();
And the request is sent again.
This all works fine. answers gets populated if the id is found.
My issue is that occasionally, depending on what I get back from the server, I have to make another server request. The initial request works fine with
this.get('content.transaction').commit();
but when I try to reload the transaction, I get an error, as follows
Uncaught Error: Attempted to handle event `loadedData` on <App.Answer> while in state rootState.loaded.updated.uncommitted. Called with undefined
Now when I remove the reload, I no longer get the error, also when I check the console of Chrome under the network tab, I can see that the results I want are being sent back but they are not being updated in my DS Model. answers is left undefined.
Anyone know why this is happening? Am I using the transactions wrong?
EDIT
Application.SearchController = Ember.ObjectController.extend({
isComplete: function () {
return this.get('content.answers.length') !== 0;
},
search: function () {
this.get('content.transaction').commit();
var record = this.get('content');
var interval = setInterval(function (controller) {
if (controller.get('isComplete')) {
controller.transitionToRoute("search.view");
clearInterval(interval);
} else {
record.reload();
}
}, 5000, this);
}
});
SO basically some work in done in my route to set up my models and set them to the content, the model has an id that will be used on the server side and sent back with the results of the search then added to "answers".
This work fine until there are multiple results are found. Then a new model is created and the search function is called again on a different controller, with different content. This time round on the line
record.reload();
I get the error
Uncaught Error: Attempted to handle event loadedData on while in state rootState.loaded.updated.uncommitted. Called with undefined
So the server still responds with the correct results but the "answers" is not updated on the client side.
After the first commit the transaction is placed on the default transaction.
Error Attempted to handle event `loadedData` : Object not updated after deleteRecord
And remember always setup the router first.
Your MyModel record is locally modified (client side). Calling reload will try to update it, which is prohibited in the current state of the record.
You can check this with a command:
console.log( this.get('content.stateManager.currentState.path') );
this.get('content').reload();
This should display in your console that the record is in the uncommitted state.
UPDATE:
You can't use a timer. Everything is asynchronous and you have no guarantee that your model will be updated during that interval. Which means that while you commit your record, you may reload it at the same time (this would generate the error you see).
What you want is something like that:
Application.SearchController = Ember.ObjectController.extend({
search: function () {
var record = this.get('content'),
controller = this;
record.one('didCommit', function() {
controller.transitionToRoute("search.view");
});
record.transaction.commit();
}
});