How to handle 404 of Ember Data in route? - ember.js

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.

Related

Ember Data error when deleting model

I have an Ember-cli app (v2.1), and I'm having an issue with Ember Data when deleting a record from my blog model. Here's the code I'm calling for the deletion:
deleteBlog (blog) {
this.store.findRecord('blog', blog.get('id')).then(blog => {
blog.destroyRecord();
});
}
As expected, Ember Data makes the DELETE request, which works perfectly with my express.js backend service.
The only problem is that the record does not get removed from the Ember Data store, and I get the following error message:
Error: Attempted to handle event pushedData on my-app#model:blog::ember529:56455037f9cf29a325ae72b9 while in state root.deleted.inFlight
I appreciate any feedback!
The problem was in trying to select the model using an instance of the model that I already had. Changing it to a simple:
deleteBlog (blog) {
blog.destroyRecord();
}
got rid of the error! Thanks again to #torazaboro for helping me to take a closer look at the mistake.
Try this:
deleteBlog (blog) {
this.store.findRecord('blog', blog.get('id')).then(function(blog) {
blog.deleteRecord(); // => remove blog from the store
blog.save(); // => DELETE request
});
}
But this is supposed to do the same as blog.destroyRecord(); so it may not work.
What is probably happenning is that if you have a request to the server in-flight and you delete the model then you get an exception Attempted to handle event pushedData on model while in state root.deleted.uncommitted.

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 maintain data changes after a transaction is committed, including setting a belongsTo relationship

I am have difficulty setting a belongsTo relationship using ember data. I have a jsfiddle example to demonstrate. My original problem was that I was getting the following error:
Uncaught Error: <DS.StateManager:ember5035> could not respond to event loadedData in state rootState.loaded.updated.inFlight.
This only occurred when I updated the belongsTo relation on the model using the select. That was using Ember Data's built in RestAdapter. In the current example I am using the FixtureAdapter and I can't seem to persist any changes (if you check out the fiddle and edit the name of one the books, you will see that it reverts back to the original name)? They seem to revert as soon as the "server response" is simulated by the adapter? Is there a way to simulate a proper server response?
Also, I am wondering if my woes here are the result of how I've set up the data store transaction. I am using the following approach in the controller:
App.EditBookController = Ember.ObjectController.extend({
enterEditing: function() {
this.transaction = App.router.get('store').transaction();
this.transaction.add(this.get('content'));
},
updateRecord: function() {
this.transaction.commit();
this.transaction = null;
App.router.transitionTo('books');
}
});
Ok, you are using the very edge version of ember-data. This is great, but I have to put myself update too. With the help of #tchak, I can begin to make it working:
http://jsfiddle.net/Sly7/2XHZ2/45/
You will note I'm using App.Book.find() in order to load the fixtures for the first time, and App.Book.all() which seems to be a live Array, so things are kept updated.
I have to going further in order to make the select working.
Ok, my last try for the moment... http://jsfiddle.net/Sly7/2XHZ2/57/
LAST EDIT Got it working by adding the RESTSerializer to the FixtureAdapter. see http://jsfiddle.net/Sly7/2XHZ2/61/