I'm using Ember.js 1.0.0 and Ember-Data-beta2.
I have a model Product which belongsTo Company. When creating a product, the user can select which company it belongsTo in a dropdown menu. This adds "company":"25" to the form post, which for the most part is just what I want. Instead of "company", however, I want the form to submit "company_id". How can I change this?
From what I can tell, Ember-Data serializers only normalize incoming data, not outgoing data. Would this be handled in the adapter? If so, how do I communicate this convention to Ember?
Use the ActiveModelAdapter (see PR here):
App.ApplicationAdapter = DS.ActiveModelAdapter.extend();
Edit: This solution is outdated, use ActiveModelAdapter instead, as suggested by Panagiotis Panagi.
With the recent versions of ember-data you have to override the serializer, in order to get "rails freindly behaivor". Like so:
// See https://github.com/emberjs/data/blob/master/TRANSITION.md
// and http://discuss.emberjs.com/t/changes-with-ds-restadapter/2406/8
App.ApplicationSerializer = DS.RESTSerializer.extend({
normalize: function(type, hash, property) {
var normalized = {}, normalizedProp;
for (var prop in hash) {
if (prop.substr(-3) === '_id') {
// belongsTo relationships
normalizedProp = prop.slice(0, -3);
} else if (prop.substr(-4) === '_ids') {
// hasMany relationship
normalizedProp = Ember.String.pluralize(prop.slice(0, -4));
} else {
// regualarAttribute
normalizedProp = prop;
}
normalizedProp = Ember.String.camelize(normalizedProp);
normalized[normalizedProp] = hash[prop];
}
return this._super(type, normalized, property);
},
serialize: function(record, options) {
json = {}
record.eachAttribute(function(name) {
json[name.underscore()] = record.get(name)
})
record.eachRelationship(function(name, relationship) {
if (relationship.kind == 'hasMany') {
key = name.singularize().underscore() + '_ids'
json[key] = record.get(name).mapBy('id')
} else {
key = name.underscore() + '_id'
json[key] = record.get(name + '.id')
}
});
if (options && options.includeId) {
json.id = record.get('id')
}
return json
},
typeForRoot: function(root) {
var camelized = Ember.String.camelize(root);
return Ember.String.singularize(camelized);
},
serializeIntoHash: function(data, type, record, options) {
var root = Ember.String.decamelize(type.typeKey);
data[root] = this.serialize(record, options);
},
serializeAttribute: function(record, json, key, attribute) {
var attrs = Ember.get(this, 'attrs');
var value = Ember.get(record, key), type = attribute.type;
if (type) {
var transform = this.transformFor(type);
value = transform.serialize(value);
}
// if provided, use the mapping provided by `attrs` in
// the serializer
key = attrs && attrs[key] || Ember.String.decamelize(key);
json[key] = value;
}
});
I hope this helps.
Related
I'm migrating an ember application from ember-data v0.13 to v1.0.0 .
In version v0.13, RESTSerializer used to have a materialize callback that allowed me to map rails STI models to ember models.
So when I get a list of events with different types, i would convert each of them to the appropriate ember model
"events": [
{
"id": 1,
"type": "cash_inflow_event",
"time": "2012-05-31T00:00:00-03:00",
"value": 30000
},
{
"id": 2,
"type": "asset_bought_event",
"asset_id": 119,
"time": "2012-08-16T00:00:00-03:00",
"quantity": 100
}
]
Ember models
App.Event = DS.Model.extend({...})
App.AssetBoughtEventMixin = Em.Mixin.create({...})
App.AssetBoughtEvent = App.Event.extend(App.AssetBoughtEventMixin)
App.CashInflowEventMixin = Em.Mixin.create({...})
App.CashInflowEvent = App.Event.extend(App.CashInflowEventMixin)
Ember-data code v0.13 that created the STI-like ember models
App.RESTSerializer = DS.RESTSerializer.extend({
materialize:function (record, serialized, prematerialized) {
var type = serialized.type;
if (type) {
var mixin = App.get(type.classify() + 'Mixin');
var klass = App.get(type.classify());
record.constructor = klass;
record.reopen(mixin);
}
this._super(record, serialized, prematerialized);
},
rootForType:function (type) {
if (type.__super__.constructor == DS.Model) {
return this._super(type);
}
else {
return this._super(type.__super__.constructor);
}
}
});
How can I do the same thing in ember-data v1.0.0 ?
I think I've got a solution...
There's a setupData callback on models.
I did the following
App.Event = DS.Model.extend({
...
setupData:function (data, partial) {
var type = data.type;
if (type) {
var mixin = App.get(type.classify() + 'Mixin');
this.reopen(mixin);
}
delete data.type;
this._super(data, partial);
},
eachAttribute: function() {
if(this.get('type')){
var constructor = App.get(this.get('type').classify());
constructor.eachAttribute.apply(constructor, arguments);
}
this._super.apply(this, arguments);
}
});
Ember experts, is this a good idea??
First, since you are using Rails, you may want to use the ActiveModelAdapter & extend your custom serializer from its serializer:
App.ApplicationAdapter = DS.ActiveModelAdapter;
App.ApplicationSerializer = DS.ActiveModelSerializer.extend({...});
It looks like your custom serializer should override typeForRoot & probably normalize. Here is what those methods looks like now:
DS.ActiveModelSerializer#typeForRoot:
typeForRoot: function(root) {
var camelized = Ember.String.camelize(root);
return Ember.String.singularize(camelized);
}
DS.JSONSerializer#normalize:
normalize: function(type, hash) {
if (!hash) { return hash; }
this.applyTransforms(type, hash);
return hash;
}
I am trying to link ember-models to the ember-table to pull paginated records from the server and add them to the table when scrolling down.
I can get it working by just requesting my api url with page number like in the ajax example on http://addepar.github.io/ember-table/ but i cant figure out how to integrate it with ember-model to create and ember objects and then add them to the table.
Here is my code to just make an ajax request and add to table. Can anyone tell me how i can change this to use ember-model / ember-data instead.
App.TableAjaxExample = Ember.Namespace.create()
App.TableAjaxExample.LazyDataSource = Ember.ArrayProxy.extend
createGithubEvent: (row, event) ->
row.set 'id', event.id
row.set 'name', event.name
row.set 'isLoaded', yes
requestGithubEvent: (page) ->
content = #get 'content'
start = (page - 1) * 30
end = start + 30
per_page = 40
# something like this ???
#App.Detail.find(type: 'companies', page: page, per_page: per_page).on 'didLoad', ->
url = "http:/myurl.dev/admin/details.json?type=companies&page=#{page}&per_page=30"
Ember.$.getJSON url, (json) =>
json.details.forEach (event, index) =>
row = content[start + index]
#createGithubEvent row, event
[start...end].forEach (index) ->
content[index] = Ember.Object.create eventId: index, isLoaded: no
objectAt: (index) ->
content = #get 'content'
#if index is content.get('length') - 1
# content.pushObjects(new Array(30))
row = content[index]
return row if row and not row.get('error')
#requestGithubEvent Math.floor(index / 30 + 1)
content[index]
App.TableAjaxExample.TableController =
Ember.Table.TableController.extend
hasHeader: yes
hasFooter: no
numFixedColumns: 0
numRows: 21054
rowHeight: 35
columns: Ember.computed ->
columnNames = ['id', 'name']
columns = columnNames.map (key, index) ->
Ember.Table.ColumnDefinition.create
columnWidth: 150
headerCellName: key.w()
contentPath: key
columns
.property()
content: Ember.computed ->
App.TableAjaxExample.LazyDataSource.create
content: new Array(#get('numRows'))
.property 'numRows'
Is the possible or does this slow it down to much?
Thanks for the help.
Rick
Here's a JSBin that I got working with Ember Data and the RESTAdapter: http://jsbin.com/eVOgUrE/3/edit
It works very similarly to the AJAX loading example, but uses Ember Data to load the data. I created a RowProxy object that is returned immediately to the Ember Table so that it can render a row. After Ember Data loads a page full of data it sets the object property on the RowProxy which updates the view.
window.App = Ember.Application.create();
// The main model that will be loaded into Ember Table
App.Gallery = DS.Model.extend({
name: DS.attr('string'),
smallUrl: DS.attr('string')
});
// This is a temporary buffer object that sits between
// Ember Table and the model object (Gallery, in this case).
App.RowProxy = Ember.Object.extend({
object:null,
getObjectProperty : function(prop){
var obj = this.get('object');
if(obj){ console.log(prop + " : " + obj.get(prop)); }
return obj ? obj.get(prop) : 'loading...';
},
isLoaded : function(){ return !!this.get('object'); }.property('object'),
name : function(){ return this.getObjectProperty('name'); }.property('object.name'),
id : function(){ return this.getObjectProperty('id'); }.property('object.id'),
smallUrl : function(){ return this.getObjectProperty('smallUrl'); }.property('object.smallUrl')
});
App.ApplicationController = Ember.Controller.extend({
tableController: Ember.computed(function() {
return Ember.get('App.TableAjaxExample.TableController').create({
// We need to pass in the store so that the table can use it
store : this.get('store')
});
})
});
App.TableAjaxExample = Ember.Namespace.create();
App.TableAjaxExample.ImageTableCell = Ember.Table.TableCell.extend({
templateName: 'img-table-cell',
classNames: 'img-table-cell'
});
App.TableAjaxExample.LazyDataSource = Ember.ArrayProxy.extend({
requestPage : function(page){
var content, end, start, url, _i, _results,
_this = this;
content = this.get('content');
start = (page - 1) * 3;
end = start + 3;
// Find galleries and then update the RowProxy to hold a gallery as 'object'
this.get('store').find('gallery',{page_size:3,page:page}).then(function(galleries){
return galleries.forEach(function(gallery, index) {
var position = start + index;
content[position].set('object',gallery);
});
});
// Fill the 'content' array with RowProxy objects
// Taken from the 'requestGithubEvent' method of the original example
return (function() {
_results = [];
for (var _i = start; start <= end ? _i < end : _i > end; start <= end ? _i++ : _i--){ _results.push(_i); }
return _results;
}).apply(this).forEach(function(index) {
return content[index] = App.RowProxy.create({
index: index
});
});
},
objectAt: function(index) {
var content, row;
content = this.get('content');
row = content[index];
if (row && !row.get('error')) {
return row;
}
this.requestPage(Math.floor(index / 3 + 1));
return content[index];
}
});
App.TableAjaxExample.TableController = Ember.Table.TableController.extend({
hasHeader: true,
hasFooter: false,
numFixedColumns: 0,
numRows: 19,
rowHeight: 35,
columns: Ember.computed(function() {
var avatar, columnNames, columns;
avatar = Ember.Table.ColumnDefinition.create({
columnWidth: 80,
headerCellName: 'smallUrl',
tableCellViewClass: 'App.TableAjaxExample.ImageTableCell',
contentPath: 'smallUrl'
});
columnNames = ['id', 'name'];
columns = columnNames.map(function(key, index) {
return Ember.Table.ColumnDefinition.create({
columnWidth: 150,
headerCellName: key.w(),
contentPath: key
});
});
columns.unshift(avatar);
return columns;
}).property(),
content: Ember.computed(function() {
return App.TableAjaxExample.LazyDataSource.create({
content: new Array(this.get('numRows')),
store : this.get('store')
});
}).property('numRows')
});
App.ApplicationAdapter = DS.RESTAdapter.extend({
host: 'http://files.cloudhdr.com/api/v1/public',
// This is here to use underscores in API params
pathForType: function(type) {
var underscored = Ember.String.underscore(type);
return Ember.String.pluralize(underscored);
}
});
// Everything below is all here to use underscores in API params.
// You may or may not need this.
DS.RESTSerializer.reopen({
modelTypeFromRoot: function(root) {
console.log("modelTypeFromRoot " + root);
var camelized = Ember.String.camelize(root);
return Ember.String.singularize(camelized);
}
});
App.ApplicationSerializer = DS.RESTSerializer.extend({
normalize: function(type, hash, property) {
var normalized = {}, normalizedProp;
for (var prop in hash) {
if (prop.substr(-3) === '_id') {
// belongsTo relationships
normalizedProp = prop.slice(0, -3);
} else if (prop.substr(-4) === '_ids') {
// hasMany relationship
normalizedProp = Ember.String.pluralize(prop.slice(0, -4));
} else {
// regualarAttribute
normalizedProp = prop;
}
normalizedProp = Ember.String.camelize(normalizedProp);
normalized[normalizedProp] = hash[prop];
}
return this._super(type, normalized, property);
}
});
I am migrating from Ember data 0.13 to 1.0.0 beta. According to the doc https://github.com/emberjs/data/blob/master/TRANSITION.md, there are now per type adapters and per type serializers.
This means that I can no longer define a "myRestAdapter" with some specific overrides for the primary key and the authentication. I need to implement this code now for each model type resulting in duplicating xx times the same code.
Code in Ember data 0.13:
App.AuthenticatedRestAdapter = DS.RESTAdapter.extend({
serializer: DS.RESTSerializer.extend({
primaryKey: function() {
return '_id';
}
}),
ajax: function (url, type, hash) {
hash = hash || {};
hash.headers = hash.headers || {};
hash.headers['Authorization'] = App.Store.authToken;
return this._super(url, type, hash);
}
});
Code in Ember data 1.0.0 (only for setting the primary key to _id instead of _id:
App.AuthorSerializer = DS.RESTSerializer.extend({
normalize: function (type, property, hash) {
// property will be "post" for the post and "comments" for the
// comments (the name in the payload)
// normalize the `_id`
var json = { id: hash._id };
delete hash._id;
// normalize the underscored properties
for (var prop in hash) {
json[prop.camelize()] = hash[prop];
}
// delegate to any type-specific normalizations
return this._super(type, property, json);
}
});
Have I understood it correct that I need to copy this same block now for every model that requires the _id as primary key ? Is there no longer a way to specify this once for the whole application ?
Since the code seams to be type agnostic, why you don't just create your custom serializer that your models can extend from, something like:
App.Serializer = DS.RESTSerializer.extend({
normalize: function (type, hash, property) {
// property will be "post" for the post and "comments" for the
// comments (the name in the payload)
// normalize the `_id`
var json = { id: hash._id };
delete hash._id;
// normalize the underscored properties
for (var prop in hash) {
json[prop.camelize()] = hash[prop];
}
// delegate to any type-specific normalizations
return this._super(type, json, property);
}
});
And then use App.Serializer for all your models:
App.AuthorSerializer = App.Serializer.extend();
App.PostSerializer = App.Serializer.extend();
...
Hope it helps.
You can also set App.ApplicationSerializer. This will work if you want this normalization applied to every model.
App.ApplicationSerializer = DS.RESTSerializer.extend({
normalize: function (type, property, hash) {
var json = { id: hash._id };
// ...
return this._super(type, property, json);
}
});
I don't really know if this is recommended, but since I need the primary key to be "_id" for every model, I just did this:
DS.JSONSerializer.reopen({
primaryKey: '_id'
});
I found this to work with primary key ids of _id:
MediaUi.ApplicationSerializer = DS.RESTSerializer.extend({
normalize: function (type, property, hash) {
// property will be "post" for the post and "comments" for the
// comments (the name in the payload)
// normalize the `_id`
var json = { id: hash._id };
delete hash._id;
// normalize the underscored properties
for (var prop in property) {
json[prop.camelize()] = property[prop];
}
// delegate to any type-specific normalizations
return this._super(type, json, hash);
}
});
The difference here is that I'm switching hash in the for loop to property and passing in hash into the super. Maybe this is a bug with Ember Data 1.0 Beta?
I have model with custom attribute(array of objects). Like this
App.Adapter.registerTransform('images', {
serialize: function(value) {
var ret = []
value.forEach(function(img){
ret.pushObject(img.get('uuid'))
})
if (ret.get('length')) {
return ret.join(',')
} else
return false
},
deserialize: function(value) {
ret = []
if (typeof value !== 'undefined') {
uuids = value.split(',')
for (var i = 0; i < uuids.length; i++) {
var id = uuids[i]
ret.pushObject( App.Image.create({'uuid': id}) )
}
}
return ret
}
})
And my model.
App.Item = DS.Model.extend({
…
images: DS.attr('images')
})
in controller I need commit data, after pushing changes in this property. What I need to do for this case?
uploadImage: function(){
var self = this
uploading.done(function(result) {
self.get('images').pushObject(App.Image.create({uuid:result.uuid}))
console.log(self.get('isDirty')) // false
self.get('store').commit() //nothing to change
}).fail(function(result) {
…
}).always(function() {
…
})
},
Have you tried to do this?
self.notifyPropertyChange('images');
I have two models:
App.User = DS.Model.create({
comments: DS.hasMany('App.Comment')
});
App.Comment = DS.Model.create({
user: DS.belongsTo('App.User')
});
When a user is deleted, it also will delete all its comments on the backend, so I should delete them from the client-side identity map.
I'm listing all the comments on the system from another place, so after deleting a user it would just crash.
Is there any way to specify this kind of dependency on the association? Thanks!
I use a mixin when I want to implement this behaviour. My models are defined as follows:
App.Post = DS.Model.extend(App.DeletesDependentRelationships, {
dependentRelationships: ['comments'],
comments: DS.hasMany('App.Comment'),
author: DS.belongsTo('App.User')
});
App.User = DS.Model.extend();
App.Comment = DS.Model.extend({
post: DS.belongsTo('App.Post')
});
The mixin itself:
App.DeletesDependentRelationships = Ember.Mixin.create({
// an array of relationship names to delete
dependentRelationships: null,
// set to 'delete' or 'unload' depending on whether or not you want
// to actually send the deletions to the server
deleteMethod: 'unload',
deleteRecord: function() {
var transaction = this.get('store').transaction();
transaction.add(this);
this.deleteDependentRelationships(transaction);
this._super();
},
deleteDependentRelationships: function(transaction) {
var self = this;
var klass = Ember.get(this.constructor.toString());
var fields = Ember.get(klass, 'fields');
this.get('dependentRelationships').forEach(function(name) {
var relationshipType = fields.get(name);
switch(relationshipType) {
case 'belongsTo': return self.deleteBelongsToRelationship(name, transaction);
case 'hasMany': return self.deleteHasManyRelationship(name, transaction);
}
});
},
deleteBelongsToRelationship: function(name, transaction) {
var record = this.get(name);
if (record) this.deleteOrUnloadRecord(record, transaction);
},
deleteHasManyRelationship: function(key, transaction) {
var self = this;
// deleting from a RecordArray doesn't play well with forEach,
// so convert to a normal array first
this.get(key).toArray().forEach(function(record) {
self.deleteOrUnloadRecord(record, transaction);
});
},
deleteOrUnloadRecord: function(record, transaction) {
var deleteMethod = this.get('deleteMethod');
if (deleteMethod === 'delete') {
transaction.add(record);
record.deleteRecord();
}
else if (deleteMethod === 'unload') {
var store = this.get('store');
store.unloadRecord(record);
}
}
});
Note that you can specify via deleteMethod whether or not you want to send the DELETE requests to your API. If your back-end is configured to delete dependent records automatically, then you will want to use the default.
Here's a jsfiddle that shows it in action.
A quick-and-dirty way would be to add the following to your user model
destroyRecord: ->
#get('comments').invoke('unloadRecord')
#_super()
I adapted the answer of #ahmacleod to work with ember-cli 2.13.1 and ember-data 2.13.0. I had an issue with nested relationships and the fact that after deleting an entity from the database its id was reused. This lead to conflicts with remnants in the ember-data model.
import Ember from 'ember';
export default Ember.Mixin.create({
dependentRelationships: null,
destroyRecord: function() {
this.deleteDependentRelationships();
return this._super()
.then(function (model) {
model.unloadRecord();
return model;
});
},
unloadRecord: function() {
this.deleteDependentRelationships();
this._super();
},
deleteDependentRelationships: function() {
var self = this;
var fields = Ember.get(this.constructor, 'fields');
this.get('dependentRelationships').forEach(function(name) {
self.deleteRelationship(name);
});
},
deleteRelationship (name) {
var self = this;
self.get(name).then(function (records) {
if (!records) {
return;
}
var reset = [];
if (!Ember.isArray(records)) {
records = [records];
reset = null;
}
records.forEach(function(record) {
if (record) {
record.unloadRecord();
}
});
self.set(name, reset);
});
},
});
Eventually, I had to set the relationship to [] (hasMany) or null (belongsTo). Else I would have run into the following error message:
Assertion Failed: You cannot update the id index of an InternalModel once set. Attempted to update <id>.
Maybe this is helpful for somebody else.