The Ember.js REST Adapter expects the JSON to be returned as:
{
"person": {
"first_name": "Barack",
"last_name": "Obama",
"is_person_of_the_year": true
}
}
But my API returns the data without a root element:
{
"first_name": "Barack",
"last_name": "Obama",
"is_person_of_the_year": true
}
Is it possible to customize the REST Adapter so that it accepts my JSON data? Right now it's showing "Assertion failed: Your server returned a hash with the key 0 but you have no mapping for it"
UPDATE:
Based on Sherwin Yu's answer below, this is what I came up with, seems to work so far: https://gist.github.com/richardkall/5910875
You could also normalize it into something ember would expect.
App.PersonSerializer = DS.RESTSerializer.extend({
normalizePayload: function(type, payload) {
var typeKey = type.typeKey;
return {
typeKey: payload
}
}
});
Yes, you can write your own custom REST adapter. Take a look at the source code in the JSONSerializer, RESTSerializer (which extends the JSONSerializer), and the REST adapter.
Basically, the you need to override the extract* methods from the JSONSerializer.
Currently, it looks something like this:
extract: function(loader, json, type, record) {
var root = this.rootForType(type);
this.sideload(loader, type, json, root);
this.extractMeta(loader, type, json);
if (json[root]) {
if (record) { loader.updateId(record, json[root]); }
this.extractRecordRepresentation(loader, type, json[root]);
}
},
Notice how it checks json[root] -- you'd have to write your custom method based on your expected API response.
Another approach would be to "preprocess" the json from the API to use a root element. You could do this by finding out what methods call extract* (which passes it the json) and before it does so, modify the json to contain the root element.
Hope this helps, please let me know if it's unclear.
I solved this by extending DS.RESTSerializer. extractArray method needs to be overloaded when server response is array type.
App.PersonSerializer = DS.RESTSerializer.extend({
extractSingle: function (store, type, payload, id) {
var wrappedObj = {};
wrappedObj[type.typeKey] = payload;
return this._super(store, type, wrappedObj, id);
}});
The easiest way is to not use the RESTSerializer but the much simpler JSONSerializer, which does not expect a root element.
Good resources on understanding which serializer to use for a given API can be found in these two blog posts:
http://thejsguy.com/2015/12/05/which-ember-data-serializer-should-i-use.html
http://emberigniter.com/fit-any-backend-into-ember-custom-adapters-serializers/
Related
I receive next data from server after get request:
{ data: { items: [...], itemsCount: Number } }
I'm saving items in backbone collection in next way:
parse: function (response) {
return response.data.items;
}
How do I keep somehow in collection or outer itemsCount?
If I understand your question correctly, the answer is straight forward. You're not limited to simply saving data in a collection or in the model.attributes hash. Backbone objects are traditional javascript objects and you can create any custom property you'd like.
So, in your parse function, you could do...
parse: function (response) {
this.itemsCount = response.itemsCount
return response.data.items
}
Note that I am assuming that your parse function is scoped to the collection. If it's not, then I need to see more code to demonstrate how to properly scope the function.
I have a route that creates a new record like so:
App.ServicesNewRoute = Ember.Route.extend({
model : function() {
return this.store.createRecord('service');
},
setupController: function(controller, model) {
controller.set('model', model);
},
});
Then I bind that model's properties to the route's template using {{input type="text" value=model.serviceId ... }} which works great, the model gets populated as I fill up the form.
Then I save record:
App.ServicesNewController = Ember.ObjectController.extend({
actions : {
saveService : function() {
this.get('model').save(); // => POST to '/services'
}
}
});
Which works too.
Then I click the save button again, now the save method does a PUT as expected since the model has an id set (id: 102):
But then when I look at the PUT request in Dev Tools, I see that the id attribute was not serialized:
As a result, a new instance is created in the backend instead of updating the existing one.
Please ignore the serviceId property, it is just a regular string property unrelated to the record id which should be named just id.
I don't know why the id is not being serialized... I cannot define an id property on the model of course since Ember Data will not allow it, it is implicit. So I don't know what I am missing...
Any help is greatly appreciated!
The base JSONSerializer in Ember-Data only includes id in the payload when creating records. See DS.JSONAdapter.serialize docs.
The URL the RestAdapter generates for PUTting the update includes the ID in the path. In your case I believe it would be: PUT '/services/102'.
You can either extract it from the path in your backend service. Or you should be able to override the behavior of your serializer to add the id like this:
App.ServiceSerializer = DS.JSONSerializer.extend({
serialize: function(record, options) {
var json = this._super.apply(this, arguments); // Get default serialization
json.id = record.id; // tack on the id
return json;
}
});
There's plenty of additional info on serialization customization in the docs.
Hope that helps!
Initially I used ronco's answer and it worked well.
But when I looked at ember data's source code I noticed that this option is supported natively. You just need to pass the includeId option to the serializer.
Example code:
App.ApplicationSerializer = DS.RESTSerializer.extend({
serialize: function(record, options) {
options = options ? options : {}; // handle the case where options is undefined
options.includeId = true;
return this._super.apply(this, [record, options]); // Call the parent serializer
}
});
This will also handle custom primary key definitions nicely.
Well, as far as I know it's a sync issue. After first request you do the post request and then, it has been saved in the server, when you click next time the store haven't got enough time to refresh itself. I've got similar issue when I've created something and immediately after that (without any transition or actions) I've tried to delete it - the error appears, in your case there's a little bit another story but with the same source. I think the solution is to refresh state after promise resolving.
I have an API that returns one resource at a time.
As an example, say I have two related resources:
Topics
Posts
Topics hasMany Posts, and Posts belongsTo a Topic.
According to the documentation (and everything I've tried and have been reading about), Ember-Data expects the JSON out of the API to be similar to the following, where both the data for Topic and the data for Posts are provided simultaneously.
"topic": {
"id": ...,
"title": ...,
"author": ...
},
"posts": [{
"id": ...,
"commenter": ...,
"text": ...
}, {
"id": ...,
"commenter": ...,
"text": ...
}]
As I mentioned at the start of the post, my API does not do this. It would return the Topic data in one call, and the Posts data for that Topic in a second call. Changing my API to work exactly how Ember wants it is not an option.
My question is, how can I extend/override the RESTAdapter and RESTSerializer in Ember-Data to support loading data from multiple API calls? Is this even possible?
I tried following this example, but it looks like the data here is already loaded (it tries to sideload data that is already known to the system): http://mozmonkey.com/2013/12/loading-json-with-embedded-records-into-ember-data-1-0-0-beta/
I also tried to override the extractFindAll function of the ApplicationSerializer, but can't figure out how to get the rest (no pun intended) of the application to wait for all of the relationships to load. I iterate through all of the relationships on the model, and load their data, but I don't see a way to shuttle that data back into the original payload:
extractFindAll: function (store, type, payload, id, requestType) {
var promises = [];
type.eachRelationship(function (key, relationship) {
promise.push(Ember.RSVP.Promise(function (resolve, reject) {
this.store.find(key, payloadEntry[key]);
}));
});
// can't figure out how to get the relationship payloads
// back to the original payload
Ember.RSVP.addSettled(promises).then(function () {
return this._super(store, type, payload, id, requestType);
});
}
Any help is appreciated.
That exactly describes using the async property.
Models
App.Foo = DS.Model.extend({
bars: DS.hasMany('bar', {async: true});
})
App.Bar = DS.Model.extend({
baz: DS.attr()
})
Adapters
App.FooAdapter = DS.RESTAdapter.extend({
//anything custom
});
App.BarAdapter = DS.RESTAdapter.extend({
//anything custom
});
If you don't do anything custom, you should just define App.ApplicationAdapter = DS.RESTAdapter; to be a site wide adapter.
Serializer
App.FooSerializer = DS.RESTSerializer.extend({
//anything custom
});
App.BarSerializer = DS.RESTSerializer.extend({
//anything custom
});
If you don't do anything custom, you don't need to define the rest serializers, they will use their default serializer associated with whichever adapter they're using.
Example
Important to note, I was lazy and my mock response for bars always returns the same thing regardless of any call to /bar*
http://emberjs.jsbin.com/OxIDiVU/481/edit
I have an existing service written with the .NET Web API.
As an example, this service returns JSON in the following format:
[
{ "id": 1, "name": "John" },
{ "id": 2, "name": "Jane" }
]
However, as per the Ember.js Rest Adapter documentation, Ember would expect JSON in the following format:
{
"persons": [
{ "id": 1, "name": "John" },
{ "id": 2, "name": "Jane" }
]
}
Because of this, Ember is returning the following error:
Your server returned a hash with the key 0 but you have no mapping for it
By no means do I plan on changing my service API and how it returns data.
Would it be possible to get Ember.js (latest version) to work with the existing data my service is returning? And, if so, how can I implement that?
Ember is very flexible in that sense, giving the ability to extend the adapter and serializer in order to integrate your app with any backend API.
You should get the WebAPIAdapter, which is part of the Ember.js Template for Web API.
Additionally, you might wanna take a look into this project I wrote as an example, based on that same template (with some modifications I did on my own). It's still under development and it doesn't feature all the best practices (yet), but I'd say it's a valid example.
You should also take a look into this repo / library (You can install it via NuGet too), which allows you to pre-compile your Handlebars templates directly into Ember.TEMPLATES collection.
in the web api just return new { object }
var persons = _personbService.GetPeople;
return new { persons };
this will wrap your object in an object of the same name. If you need to call the object something else just do
new { SomeOtherName = persons; }
I was having this issue in ember and found the best solution for me was to build a new serializer and override the normalizePayload method. code below:
export default DS.RESTSerializer.extend({
normalizePayload: function(payload) {
var root = "posts";
var output = {};
output[root] = payload;
return output;
}
});
This wraps the initial response and adds the root to it, hope it helps!
I'm trying to use Backbone with an API.
The default API response format is:
{
somemetadatas:xxx ,
results:yyy
}
Whether it's a fetch for a single model or a collection.
So as far as I know I can override the Backbone parse function with:
parse: function (response) {
return response.results;
},
But I've seen in the documentation:
parse collection.parse(response)
parse is called by Backbone whenever
a collection's models are returned by the server, in fetch. The
function is passed the raw response object, and should return the
array of model attributes to be added to the collection. The default
implementation is a no-op, simply passing through the JSON response.
Override this if you need to work with a preexisting API, or better
namespace your responses. Note that afterwards, if your model class
already has a parse function, it will be run against each fetched
model.
So if I have a response for a collection fetch like that:
{
somemetadatas:xxx ,
results:[user1,user2]
}
The first parse function on the collection will extract [user1,user2].
But the doc says:
Note that afterwards, if your model class
already has a parse function, it will be run against each fetched
model.
So it will try to find response.results; on user1 and user2
I need both parse functions on the model and collection because both model and collection datas will be under the result attribute.
But if i fetch on a collection, I don't want the model parse function to be used againt a single array element.
So is there a solution to this problem?
I think of a solution where my collection parse function will transform something like this:
{
somemetadatas:xxx ,
results:[user1,user2]
}
into something like this:
[ {results.user1} , {results.user2} ]
So that the model parse function will not fail on a collection fetch.
But it's a bit hacky... is there any elegant solution to this problem?
By the way, as my API will always produce results of this form, is it possible to override by default the parse function of all my models and collections? (Sorry i'm a JS noob...)
You could test if the data you receive is wrapped by a results member and react accordingly. For example,
var M = Backbone.Model.extend({
parse: function (data) {
if (_.isObject(data.results)) {
return data.results;
} else {
return data;
}
}
});
And a Fiddle http://jsfiddle.net/9rCH3/
If you want to generalize this behavior, either derive all your model classes from this base class or modify Backbone's prototype to provide this function :
Backbone.Model.prototype.parse = function (data) {
if (_.isObject(data.results)) {
return data.results;
} else {
return data;
}
};
http://jsfiddle.net/9rCH3/1/
Parse also must be implemented in the Collection.
var EgCollection = Backbone.Collection.extend({
parse: function (data) {
if (_.isObject(data.results)) {
return data.results;
} else {
return data;
}
}
});
http://backbonejs.org/#Collection-parse