GET Request called multiple times with 1.0.0-beta.10 - ember.js

When upgrading EmberData from 1.0.0-beta.9 to 1.0.0-beta.10 I've noticed that the same OPTIONS/GET request is made multiple times when using a computed property in various places of the page.
Sample Code
http://jsbin.com/moruj/1/
Then in my request, I see 3 option calls to my events API. This wasn't happening in beta.9 so I'm curious what may have happened?

Before answering your question, I have to say the code in your question is wrong. Please check and modify your code so more people can understand it.
For example in your code:
/* It should be App.Customer right? */
App.CustomerModel = DS.Model.extend({});
/* should be App.CustomerIndexRoute ? */
App.CustomerIndexController = Ember.Route.extend({
model: function(params) {
/* it returns a promise which resolves a customer array */
return this.store.find('customer');
}
});
App.CustomerIndexController = Ember.Controller.extend({
/*
* But for a customer array you can not get events property.
* you can only get events from a single customer.
*/
filteredEvents: function() {
return this.get('events').slice(0, 10);
}.property('events')
});
For multiple GET requests for events
No matter how you construct your route & controller code, when you iterate events for a customer, Ember Data sends a request /events/:id for each un-fetched event. You can set coalesceFindRequests: true in your adapter to load events in one request, like /events/ids[]=1&ids[]=2&ids[]=3.
For multiple OPTION requests
Please check if you use proxy to delegate ajax requests to a remote server or a mock server. As I know an OPTION request will appear when your proxy can not delegate the request correctly. In this case you may see an OPTION request and a GET request at the same time. It's not an Ember Data problem.

It looks like this was a legit but in beta.10
http://discuss.emberjs.com/t/requests-triggering-multiple-times/6316

Related

Ember DS.Store.findAll() returns empty collection

Hey I need to modify some records which I get from the DataStore. If I add the following code in my router I can see that the requests get passed to my template, but I can't modify each request of the collection because the collection is empty.
model() {
return this.store.findAll('user').then(function(users) {
console.log(users.get('length')); // 0
return users;
});
}
I thought that the promise gets resolved when all the records have been fetched from the server but this doesn't seem to be the case. Or did I completely miss something.
I also tried to modify the model in the afterModel callback with the same result.
I'm using Ember 1.13.0 (with Ember-CLI), Ember-Data 1.13.4 and ember-cli-mirage for Mocking my HTTP Requests.
UPDATE:
I managed to create a workaround for this issue. In my controller, I created a new property which listens for model.#each and then I was able to modify model and pass it to the view.
export default Ember.Controller.extend({
users: function() {
return this.get('model.users').filter(function(user) {
// The Promise is resolved twice
// The first time with an empty model and the second time with
// the actual data. So I filter the empty model.
return user.get('id');
}).map(function(user) {
// do fancy stuff with our user
return user
});
}.property('model.#each')
});
Ember Data 1.13
So after spending some time on this topic i found the solution to this issue. It's basically the way how ember works. So under the hood findAll is returning two promises.
findAll without data in the store
find records from the store (resolve first promise -> length 0,
because no data is in the store)
fetch new data in the background (resolves second promise)
findAll with data in the store
find records from the store (resolve first promise with cached data)
fetch new data in the background (resolves second promise with new
data)
If you want to wait for all the data to be loaded you can use query which is returning only one promise.
model() {
return this.store.query('user', {});
}
For findRecord I found the following workaround, which is only working if your backend supports any kind of filtering on the id of your record.
model() {
return this.store.query('user', {
'filter[id]': 1
}).then((users) => {
return users.objectAt(0);
});
}
You can have a look on the following discussion on github
Ember Data 2.0
On Ember Data 2.0 this issue is resolved.
First you should make sure the data is coming in from Mirage as you expect. Open your Ember inspector and verify the models made it into your store. If not, you likely have a problem with the format of the JSON response from your mock route.
To diagnose, check out your console for a log of the JSON response, and ensure it matches what you expect. If you have a custom route handler in your /mirage/config.js for this route, you could also put a debugger statement in there and verify the data is what you think it should be.
If you're using default Ember Data 1.13, it probably means you're using the JSON API serializer/adapter. Is this what you intend? What is the backend for this app ultimately going to look like? If it's going to be JSON API, you'll need to do a bit more work in the Mirage config.js file for now, something like
this.get('/contacts', function(db, request) {
return {
data: db.contacts.map(attrs => {
type: 'contacts',
id: attrs.id,
attributes: attrs
})
};
});
I had a similar problem to the one you are describing when using Ember and Ember data version 1.13. At the time, I was reading the updated documentation of Ember 2.0 without Ember Data 2.0. Once I upgraded both libraries I was able to get the behavior you are trying to achieve with the first code snippet. Namely, the promise is handled correctly with nonzero records with ember data 2.0.

EmberJS model hook: this.store.find returns no data. How do I redirect to a 404 page?

When my path /map/:id finds no value via this.store.find('location', route.id), I'd like to redirect to another page instead of receiving an "adapter's response did not have any data" error. It seems to stop processing before it even gets to the controller.
I thought the best way to do this was to extend DS.FixtureAdapter or to return a proxy object until this.store.find resolves. I read the documentation and it said to extend DS.FixtureAdapter via find or findMany hooks, among others. When I tried, none of the events seemed to fire, and I can't figure out an appropriate alternative return object. What am I doing wrong?
this.store.find() returns a promise. Promise resolution has 2 outcomes: 1. good and 2. bad. You can pass in 2 functions into the then() method to tell a promise what to do in each scenario.
So, let's say you are looking for a record and it's not there (bad outcome), you can tell ember to transition to another route.
App.DudeRoute = Ember.Route.extend({
model: function() {
var route = this;
return this.store.find('dude', 5).then(
function(dude){
return dude;
},
function(error){
route.transitionTo('nomansland');
});
}
});
Also note that you need to create a route variable, because just using this inside the bad scenario won't work, since this gets a new context.
Working example here

Nested routes: handling failed child find in error action loses parent context

I've recently been porting a number of Ember apps I maintain to RC 8 and ran into this.
Before the router facelift landed I would sometimes manage control flow via promises returned by Ember Data find calls.
For example:
SomeRoute = Ember.Route.extend({
model: function(params) {
var resolve = function(model) { return model; };
var route = this;
var reject = function() { this.transitionTo('someOtherRoute'); };
return SomeModel.find(params.some_model_id).then(resolve, reject);
}
...
});
With the recent changes, it is now possible to handle errors created in model callbacks via the error action:
SomeRoute = Ember.Route.extend({
// note: model callback no longer needed--default suffices
actions: {
error: function(reason, transition) {
// check the reason object to determine how/if to handle this error
this.transitionTo('someOtherRoute');
}
}
...
});
I much prefer the latter approach as it makes the code easier to read and better separates concerns.
This works well in most cases but I encountered an issue in an app that uses nested routes. I've included a simplified example followed by a jsbin that demonstrates the issue.
Lets say we want to show Articles that belong to Authors and the URLs look like: /authors/:author_slug/articles/:article_slug. We want to redirect to a Not Found page when someone tries to view an article that doesn't exist.
When managing control flow in the model callback as above, you can browse to /authors/some_author/articles/some_invalid_slug and be redirected to /authors/some_author/articles/not_found as expected.
However, if the redirect to the Not Found page is instead managed via the error action, the parent context is lost at some point and you end up at /authors/undefined/articles/not_found.
You can see this in the following jsbins:
http://jsbin.com/eJOXifo/1#/authors/schneier/articles/12345
(redirects to http://jsbin.com/eJOXifo/1#/authors/schneier/articles/not_found)
http://jsbin.com/oNaWelo/1#/authors/schneier/articles/12345
(redirects to http://jsbin.com/oNaWelo/1#/authors/undefined/articles/not_found)
Does anyone know why this happens or how to avoid it?
Notes:
I know this doesn't have anything to do with Ember Data. However, implementing something equivalent without Ember Data just makes the example more complicated without adding anything
There are a few small hacks to make Ember Data work as expected in jsbin:
I'm preloading the parent model to avoid having to load it from anywhere.
I'm not doing anything special to provide data for the child model. The app just makes a request to http://jsbin.com/articles/12345. This actually returns a 200 but bombs anyway because the response is html. An API that correctly returns a 404 response gives the same behvaiour.
I remember a while ago reading about some service that could be used to build fake API responses for use with services like jsfiddle or jsbin. If anyone knows what it is please comment.
Your right that the parent context is getting lost. The trick is to extract that context from the transition and pass it as an argument when calling transitionTo. So:
App.ArticlesArticleRoute = Em.Route.extend({
actions: {
error: function(reason, transition) {
console.log('in error handler');
this.transitionTo('articles.notFound', transition.resolvedModels.authors);
}
}
});
With this change, visiting the url:
http://jsbin.com/iVOYEvA/2#/authors/schneier/articles/my-fake-article
will redirect to:
http://jsbin.com/iVOYEvA/2#/authors/schneier/articles/not_found

How to handle 404 of Ember Data in route?

In my route I have a method that tries to request a list of models from the server
model: ->
App.MyModel.find
projectId: (#modelFor "project").id
Now obviously sometimes this might return a 404.
At the moment when this happens, Ember just stops doing anything. No view is rendered, no controller is setup.
So how can I properly handle the 404 (ie show an error view)?
Bad news: right now, ember-data doesn't do anything when it gets a 404 on find(). At all. The model sits in the 'loading' state forever.
There are no non-completely-stupid options, here, in my opinion. What I would probably do as a last resort is add a notFound attribute on my DS.Model, and instead of returning 404, return JSON with notFound set to true. It's painful, I know...
--- I had originally offered a solution of overriding find in a subclass of RESTAdapter. Then I noticed that find DOES NOT get passed the record instance it is supposedly loading. So, no go on handling 404s by putting the record into an error state.
[NOTE: ember-data has changed dramatically since March 2013, the information in this answer may no longer be operative]
Incidentally, the "new" BasicAdapter was just released now. The question for me was, will this make things easier to handle 404 errors.
Approach #1
My first approach - similar to what Christopher was suggesting - was to add an additional field containing the HTTP status.
status: DS.attr("number");
And then I used this AJAX call:
$.getJSON(url, data).then(null, function(xhr) {
return {
id: id,
statusCode: xhr.status
};
}).always(function(data) {
return process(data).load();
});
What this does is to transform the error response (xhr) to a hash containing the requested id and the status code. Finally, the successful result or the failed hash are passed to the store.
This kind of works, but isn't very practical: When you show a list of all model instances those "mock" instances have to be filtered out manually.
Approach #2
Another idea was to create a special error model.
App.Error = App.Model.extend({
status: DS.attr("number")
});
And the according query:
$.getJSON(url, data).then(null, function(xhr) {
return App.store.load(App.Error, {}, {
id: 0,
status: xhr.status
});
}).done(function(data) {
return process(data).load();
});
This will load and create a new instance of the error model and put it into the store.
The problem with this is that Ember wasn't really "accepting" this. The application just stopped routing, doing nothing anymore. So this seems like a dead end as well :(
I hit this issue as well today.
However, after looking at the source, it appears the model is actually setup to utilize Ember.Evented, and we can add our own handlers for these cases.
The two events that caught my eye were becameError and didLoad.
In my case I was able to do something like the following:
// Grab a user by id.
var user_rec = App.User.find( user.id );
// Oh no! Error!
user_rec.on('becameError', function() {
alert('I could not find you!');
});
// We should be good! Proceed!
user_rec.on('didLoad', function() {
alert('Your email: '+this.get('email'));
});
Here's the source on github: https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/model/model.js
Hopefully, if this is indeed the way we should be handling things, there will be some added documentation in the guides in the near future.

Ember.js 1.0-pre4 + jQ UI sortable + localstorage adapter

Day 2 learning ember.js...
I'm working on a offline app that needs to save draggable/sortable tile positions to localstorage, and if there is no existing data, load & save from a fixture.
Using: ember 1.0.0-pre4, ember-data rev11, ember-localstorage-adapter, jQ 1.9, jQ UI 1.9
https://github.com/rpflorence/ember-localstorage-adapter
It's working, but I'm a bit of a novice, feel it's not pretty and could use some community advice.
http://jsfiddle.net/Nsbcu/4/
Questions
What is the proper way to check if your DS.Store has loaded and is empty? My method of looking directly at localstorage didn't feel right.
After I createRecords from the App.Tile.DEFAULTS I feel I should commit them, but an error is thrown. I don't have to commit the known defaults, but curious what causes the error and how I should go about committing properly. Also is the App.ready() callback the right place for loading defaults? Error only happens when localstorage is empty
Uncaught Error: Attempted to handle event loadedData on <App.Tile:ember231:1> while in state rootState.loaded.created.inFlight. Called with undefined
On the TilesController I'm using sortProperties which works great until jQ UI Sortable changes the DOM and Ember wants to update my tile order, before I get a chance to set the new order. My current solution is to turn off sortProperties temporarily while updating the model. Again this feels hacky, suggestions on proper way to do this?
=== Edit Feb 3 ===
If I do an async commit the initial error in question #2 is avoided.
App.TilesRoute = Ember.Route.extend({
model: function() {
return App.Tile.find();
},
setupController: function(controller) {
if (localStorage.getItem('fusion-emberjs') == null) {
App.Tile.DEFAULTS.forEach(function(item) {
App.Tile.createRecord(item);
});
// Commit async, else generates error
var _this = this;
setTimeout(function() {
_this.store.commit();
}, 1);
}
}
});
I would put any initial code inside the application or the index Route within the setupController method
if (localStorage.getItem('fusion-emberjs') == null) {
App.Tile.DEFAULTS.forEach(function(item) {
App.Tile.createRecord(item);
});
//*** WARNING: Generates Error ***/
App.Tile.find().get('store').commit();
}
Once you move the code inside the route, replace App.Tile.find().get('store').commit(); by App.store.commit() inside your route
Create your own transaction instead of using the default one, each time you make a call to the store directly you're using the default transaction. You can create a transaction this way
var transaction = App.store.transaction()
transaction.createRecord(App.Foo);
transaction.commit()
transaction.rollback();
Any call to App.store assumes you already created a store, right now you're only extending the DS.Store. Try instead
App.Store = DS.Store.create({
revision: 11,
adapter: 'App.LSAdapter'
});
I would suggest that you do any event handling or transaction management in the router unless it's purely for styling or animation. In that case, the view is the right place for it. I like the router to orchestrate communication between all the assets (controllers, routes, models, views)
A good pattern to remember is a view talks only to a controller, a controller is a mere proxy to a model, a router orchestrates communication between controllers and manages routes.