Ember-Data Transformed Value not Updated With Bindings - ember.js

Here is my transformer:
DS.attr.transforms.category = {
from: function(serialized) {
return OO.Category.create({
tag: serialized
});
},
to: function(deserialized) {
return deserialized.get('tag');
}
}
And my model:
OO.Event = DS.Model.extend({
category: DS.attr('category', { key: 'tag' })
}
When I bring up the form for creating an OO.Event, I create a "blank" OO.Event and bind the form element to the OO.Event's properties. The transformer is called when the blank OO.Event is created, but it is not called again when the tag property of the OO.Category changes (due to bindings.) Because of this, in the JSON representation of the record, "tag" is always null!
What should I do? Thanks for your help!

Related

Ember Data model.save misses fields in request body when mapping changed in keyForAttribute

I run into a problem when use Ember-data to save a model. The JSON structure for my model looks like:
{ post: {
id: 1,
name: 'post-1',
trigger: ['trigger-1', 'trigger-2'],
data: ['data-1', 'data-2']
}
}
Because 'data' and 'trigger' are reserved keywords for DS.Model, I created a mapping and renamed them to sc_data and sc_trigger as suggestion by Jurre using
Application.SERIALIZATION_KEY_MAPPINGS = {
'sc_data': 'data',
'sc_trigger': 'trigger'
};
Application.ApplicationSerializer = DS.ActiveModelSerializer.extend({
keyForAttribute: function (attr) {
if (Application.SERIALIZATION_KEY_MAPPINGS.hasOwnProperty(attr)) {
return Application.SERIALIZATION_KEY_MAPPINGS[attr];
} else {
return this._super(attr);
}
}
});
So my model for post looks like:
Application.Post = DS.Model.extend({
name: DS.attr('string'),
sc_trigger: DS.attr(),
sc_data: DS.attr()
});
the sc_trigger and sc_data are renmaed mapping for data and trigger.
It all worked fine when use this.store.find('post') and this.store.find('post', 1), i.e. GET calls. When I try to create a record using this.store.createRecord('post'), it creates a record with the correct attribute name sc_data and sc_trigger.
var newPost = this.store.create('post', {
name: 'test post',
sc_data: [],
sc_trigger: []
})
And the serialize function interprets the mapping correctly as well. newPost.serialize() returns
{
name: 'test post',
data: [],
trigger: []
}
But when I call newPost.save(), in the HTTP request body of the POST call, data and trigger field is missing. It only has
{
name: 'test post'
}
I have no idea why newPost.save() doesn't generate the correct request body when serialize() is working just fine.
Update
I managed to get around this by removing the keyForAttribute mapping and using
Application.ApplicationSerializer = DS.ActiveModelSerializer.extend({
attrs: {
sc_data: {key: 'data'},
sc_trigger: {key: 'trigger'}
}
});
This seems to be the suggested way to handle data with reserved keywords.
Which ember data version and emberjs version are you using?
Try saving with id like-
var newPost = this.store.create('post', {
id:1
name: 'test post',
sc_data: [],
sc_trigger: []
});
Save and create always expects id. So it's better to save/create record with id.

EmbeddedRecordsMixin not working as expected, what am I missing?

I'm trying to use embedded records in ember data and I think I'm missing something fundamental.
I have two models
app/models/video.js:
export default DS.Model.extend({
title: DS.attr('string'),
transcriptions: DS.hasMany('transcription', { embedded: 'always' })
});
app/models/transcription.js:
export default DS.Model.extend({
video: DS.belongsTo('video')
});
I also have a custom serializer app/serializers/video.js:
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs:{
transcriptions: { embedded: 'always' }
},
extractSingle: function (store, type, payload, id) {
var data = payload.data;
return {
id: data._id,
title: data.Title,
transcriptions: [{ id: "1" }]
}
}
});
I would expect that this would result in my video model being populated with transcriptions being an array of transcription object but instead I get the following error:
"Error while processing route: videos.show" "Assertion Failed: Ember
Data expected a number or string to represent the record(s) in the
transcriptions relationship instead it found an object. If this is a
polymorphic relationship please specify a type key. If this is an
embedded relationship please include the DS.EmbeddedRecordsMixin and
specify the transcriptions property in your serializer's attrs
object."
Any suggestions of what I'm doing wrong here would be greatly appreciated.
UPDATE: The solution was to modify my custom serializer to the following:
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
attrs:{
transcriptions: { embedded: 'always' }
},
extractSingle: function (store, type, payload, id) {
var data = payload.data;
var videoPayload = {
id: data._id,
title: data.Title,
transcriptions: [{ id: "1" }]
};
return this._super(store, type, videoPayload, id);
}
}
The problem is the fact you're reimplementing extractSingle yourself.
You should call this.super if you're doing this..
In extractSingle on the REST Serializer it calls the normalize function - this normalise function is where the EmbeddedRecordsMixin does all it's work.
Because you're not calling either this.super or manually calling this.normalize you miss out on what the mixin is doing.

Detecting change in state for Ember object

I am using Ember-Data and one of my properties is a dictionary data structure. I'd like any update to this dictionary to be converted into an action which sets the parent Model into a "dirty" state.
So here's the config:
Model
export default DS.Model.extend({
// standard types
foo: DS.attr('string'),
bar: DS.attr('number'),
baz: DS.attr('boolean'),
// dictionary (aka, flexible set of name value pairs)
dictionary: DS.attr('object')
});
Transform
export default DS.Transform.extend({
deserialize: function(serialized) {
return Ember.Object.create(serialized);
},
serialize: function(deserialized) {
return deserialized;
}
});
This works and let's assume for a moment that the "dictionary" property is defined as:
{
one: { prop1: foo, prop2: bar, prop3: baz },
two: 2,
three: "howdy",
many: [{},{},{}]
}
This means that an Ember Object has four properties. These properties can be a string, a number, an array, or an object. What I'd like is to have some way of identifying any changes to this underlying basket of attributes so I can propagate that to the Model and have it adjust its state to "dirty".
TL;DR - Working JS Bin example
In order to accomplish this you have to do the following:
1. Deserialize the raw object and all its nested deep properties to Ember Objects so they could be Observable
2. Add observers to your model for all existing keys dynamically on every change of the raw object reference, because it can change its content and scheme.
3. Remove these dynamic observers on every raw object reference change and assign the new ones
4. All dynamic properties changes will set timestamp property so that controllers could listen to it
This is a "Deep" transform I wrote in order to accomplish (1):
// app/transforms/deep.js
export
default DS.Transform.extend({
deserializeRecursively: function(toTraverse) {
var hash;
if (Ember.isArray(toTraverse)) {
return Ember.A(toTraverse.map(function(item) {
return this.deserializeRecursively(item);
}, this));
} else if (!Ember.$.isPlainObject(toTraverse)) {
return toTraverse;
} else {
hash = this.generatePlainObject(Ember.keys(toTraverse), Ember.keys(toTraverse).map(function(key) {
return this.deserializeRecursively(Ember.get(toTraverse, key));
}, this));
return Ember.Object.create(hash);
}
},
deserialize: function(serialized) {
return this.deserializeRecursively(serialized);
},
serialize: function(deserialized) {
return deserialized;
},
generatePlainObject: function(keys, values) {
var ret = {};
keys.forEach(function(key, i) {
ret[key] = values[i];
});
return ret;
}
});
This is a mixin for Models with deep raw objects which accomplish (2) & (3) & (4)
// app/mixins/dynamic-observable.js
export
default Ember.Mixin.create({
propertiesToAnalyze: [],
registerRecursively: function(toTraverse, path, propsToObserve) {
if (Ember.isArray(toTraverse)) {
propsToObserve.addObject(path + '.#each');
if (toTraverse.length > 0) {
this.registerRecursively(toTraverse[0], path + '.#each', propsToObserve);
}
} else if (!(toTraverse instanceof Ember.Object)) {
propsToObserve.addObject(path);
} else {
Ember.keys(toTraverse).forEach(function(propertyName) {
this.registerRecursively(Ember.get(toTraverse, propertyName), path + '.' + propertyName, propsToObserve);
}, this);
}
},
addDynamicObserver: function(propertyNameToAnalyze) {
var propertyToAnalyze = this.get(propertyNameToAnalyze),
propsToObserve = Ember.A([]),
currentDynamicProps = this.get('currentDynamicProps'),
propsToRemove = currentDynamicProps.filter(function(prop) {
return new RegExp('^' + prop + '.').test(prop);
});
propsToRemove.forEach(function(prop) {
Ember.removeObserver(prop, this, dynamicPropertiesObserver)
}, this);
currentDynamicProps.removeObjects(propsToRemove);
this.registerRecursively(propertyToAnalyze, propertyNameToAnalyze, propsToObserve);
propsToObserve.forEach(function(prop) {
Ember.addObserver(this, prop, this, 'dynamicPropertiesObserver');
}, this);
currentDynamicProps.addObjects(propsToObserve);
},
dynamicPropertiesObserver: function(sender, key, value, rev) {
this.set('dynamicPropertyTimestamp', new Date().getTime())
},
addDynamicObservers: function() {
this.get('propertiesToAnalyze').forEach(this.addDynamicObserver, this);
},
init: function() {
this._super();
this.get('propertiesToAnalyze').forEach(function(prop) {
Ember.addObserver(this, prop, this, Ember.run.bind(this, this.addDynamicObserver, prop));
}, this);
},
dynamicPropertyTimestamp: null,
currentDynamicProps: Ember.A([])
});
This is how you use the mixin on a model:
// app/models/some-object.js
import DynamicObservable from 'app/mixins/dynamic-observable';
export
default DS.Model.extend(DynamicObservable, {
dictionary: DS.attr('deep'),
propertiesToAnalyze: ['dictionary']
});
Finally, this is an array controller which its model is an array of some-object models
export
default Ember.ArrayController.extend({
message: '',
observeDictionaries: function() {
this.set('message', 'A dictionary has been changed. change time: ' + new Date().getTime());
}.observes('#each.dynamicPropertyTimestamp')
});

Ember data 1.0.0: confused with per-type adapters and serializers

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?

Ember and Carrierwave

So when querying the model which returns a carrierwave hash, I'm getting his.get('pic')
"[object Object]" in Ember. So How do I get into the view?
{"record":{"id":1234,"first_name":"John","gravepic":{"url":"http://c185643.r43.cf1.rackcdn.com/uploads/record/gravepic/13267/IMG_0013.JPG","ios":{"url":"http://c185643.r43.cf1.rackcdn.com/uploads/record/gravepic/13267/ios_IMG_0013.JPG"},"thumb":{"url":"http://c185643.r43.cf1.rackcdn.com/uploads/record/gravepic/13267/thumb_IMG_0013.JPG"},"small":{"url":"http://c185643.r43.cf1.rackcdn.com/uploads/record/gravepic/13267/small_IMG_0013.JPG"},"medium":{"url":"http://c185643.r43.cf1.rackcdn.com/uploads/record/gravepic/13267/medium_IMG_0013.JPG"},"large":{"url":"http://c185643.r43.cf1.rackcdn.com/uploads/record/gravepic/13267/large_IMG_0013.JPG"}},"eulogy":"FATHER","deceased":true,"gender":"male","photos":[{"accuracy":null,"approval":null,"asset_caption":"","asset_name":null,"assetpic":{"url":"http://c185643.r43.cf1.rackcdn.com/uploads/photo/assetpic/270/iccfa_350x200_3.jpg","thumb":{"url":"http://c185643.r43.cf1.rackcdn.com/uploads/photo/assetpic/270/thumb_iccfa_350x200_3.jpg"},"small":{"url":"http://c185643.r43.cf1.rackcdn.com/uploads/photo/assetpic/270/small_iccfa_350x200_3.jpg"},"medium":{"url":"http://c185643.r43.cf1.rackcdn.com/uploads/photo/assetpic/270/medium_iccfa_350x200_3.jpg"},"large":{"url":"http://c185643.r43.cf1.rackcdn.com/uploads/photo/assetpic/270/large_iccfa_350x200_3.jpg"}},"copyright":null,"coredata_id":null,"created_at":"2013-02-09T10:36:13Z","id":270,"ignore":null,"is_approved":true,"latitude":null,"longitude":null,"record_id":"13267","updated_at":"2013-02-09T10:36:13Z","user_id":1},{"accuracy":null,"approval":null,"asset_caption":null,"asset_name":null,"assetpic":{"url":null,"thumb":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null}},"copyright":null,"coredata_id":null,"created_at":null,"id":null,"ignore":null,"is_approved":true,"latitude":null,"longitude":null,"record_id":13267,"updated_at":null,"user_id":null}],"copies":[{"content":null,"created_at":null,"id":null,"is_approved":true,"name":null,"record_id":13267,"summary":null,"updated_at":null,"user_id":null}],}}
The next questions will be something like record.photos.first.medium.
Or record.gravepic.small
Just not sure how to get to something beyond "object object"
I'm not sure if this is still the exact API, but if you want multiple values out of the hash I suggest adding a new DS.attr type.
DS.RESTAdapter.registerTransform('json', {
deserialize: function(serialized) {
return Em.isNone(serialized) ? {} : serialized;
},
serialize: function(deserialized) {
return Em.isNone(deserialized) ? {} : deserialized;
}
})
App.Something = DS.Model.extend({
gravepic: DS.attr("json")
})