Answered
I've solved the problem, it has to do with my adapter, and I'll post more in an answer,
but here's a link to a problem I'm having with the fix Ember get not getting certain attribute
Original Question
I have the following code:
customerSignUp: function () {
var customer = this.get('store').createRecord('customer', {
description: 'Why hello sir'
});
var model = this.get('model');
model.set('customer', customer);
customer.save().then(function() {
model.save().then(function() {
customer.set('user', model);
customer.save();
});
});
}
With the following backing models:
App.User = App.Person.extend({
name: DS.attr('string'), // Actually their primary email.
customer: DS.belongsTo('customer', {async: true })
});
App.Customer = DS.Model.extend({
user: DS.belongsTo('user', {async: true}),
description: DS.attr('string')
});
(App.Person just passes along some naming conventions)
The customerSignUp function is trying to get both objects to refer to the other, so that either one could get attributes from the other as needed (the user model is planned to have more relationships like this in the future, so that one user can have multiple "roles" on the site).
The problem is that I can't get both to stably refer to each other. With this current implementation the model (user) points to the customer just fine, but the customer for some reason simply has it's user field set to <computed> in the Ember Debugger, and the record saved in the database doesn't even have a user field. It seems to me like some of my saves are overwriting the values or changing the underlying objects so they're no longer true? Honestly I'm just confused.
I've tried all kinds of different orderings of the saves and sets, but so far only one at a time works, or if both work, it's because I haven't saved one of them to the database. Any advice? Is this even necessary? Will the customer object have access to it's user even if there isn't an id explicitly stored in the customer?
Could this have to do with the adapter I'm using?
Update
With this implementation of customerSignUp:
customerSignUp: function () {
var model = this.get('model');
var customer = this.get('store').createRecord('customer', {
description: 'Why hello sir',
user: model
});
customer.save().then(function() {
model.set('customer', customer);
model.save();
});
}
The customer relationship on the user is set for a moment, but then resets to null. The user relationship is just <computed> and I don't think was ever set.
It had to do with my adapter.
This function:
serializeBelongsTo: function(record, json, relationship) {
console.log("serializeBelongsTo");
var attribute, belongsTo, key;
attribute = relationship.options.attribute || "id";
console.log(attribute);
key = relationship.key;
console.log(key);
belongsTo = Ember.get(record, key);
console.log(belongsTo);
if (Ember.isNone(belongsTo)) {
return;
}
var content = belongsTo.content;
console.log(content);
var temp = Ember.get(belongsTo, attribute);
console.log(temp);
json[key] = temp;
console.log(json);
if (relationship.options.polymorphic) {
return json[key + "_type"] = belongsTo.constructor.typeKey;
}
else {
return json;
}
returns a value of undefined from Ember.get(belongsTo, attribute) even though belongsTo and attribute are correctly set, although belongsTo has it's data buried in a content object, which this SO post details.
Related
I have 3 different fixture models, as shown below.
var Room = DS.Model.extend({
title: DS.attr('string'),
categories: DS.hasMany('Category', { async: true }),
isSelected: DS.attr('boolean')
});
var Category = DS.Model.extend({
title: DS.attr('string'),
room: DS.belongsTo('Room', {async: true }),
materials: DS.hasMany('Material', { async: true }),
isSelected: DS.attr('boolean')
});
var Material = DS.Model.extend({
title: DS.attr('string'),
category: DS.belongsTo('Category', {async: true} ),
isSelected: DS.attr('boolean')
});
I find when I try to view the contents inside the Materials model it is blank. In my controller I expose the materials by doing this:
currentMaterials: function() {
var room = this.filterBy('isSelected', true).get('firstObject');
var categories = room.get('categories');
var selectedCategory = categories.get('firstObject');
var material = selectedCategory.get('materials');
return material;
}.property('#each.isSelected')
However when I try to access currentMaterials the value is null. I am ONLY able to access its values if I first access the Rooms/Categories using a {{#each} loop. Oddly once I do the {{#each}} I am then able to access the values in currentMaterials.
Does anyone understand why?
It's due to fact of promises existance. Your categories relationship is async, which means that it's not present initially and ember-data should fetch it if needed. However, it takes time to fetch data, therefore ember-data returns a promise from this: var categories = room.get('categories'). After that promise, you first get firstObject from it, which does not exist for a promise (is null), and than you get materials relationship from that null. It simply is null.
However, ember templates are smart and if you put an each on them, they know that these relationships are needed and makes ember-data fetch these data.
What you can do? If you need this data to perform page-specific job, you should make sure that you have access to it before showing the page to the user - therefore in the model hook. You can use Ember.RSVP to make multiple fetch calls and set them up in the controller:
model: function() {
data =
room: store.find("room")
categories: store.find("category)
materials: store.find("material")
return Ember.RSVP.hash(data)
}
However, take notice that it will fetch all the materials, etc. If you need only the ones connected to your model, you should consider speeding up your data fetching using side loading. If you are using fixtures, it won't work.
Last that I can think of is making computed property a method that would fetch the data, but set them on other variable. You can use some kind of flag to inform the app when the data is ready:
currentMaterials: function() {
var room = this.filterBy('isSelected', true).get('firstObject');
room.get('categories').then(function(categories) {
return categories.get('firstObject').get('materials');
}).then(function(materials) {
// here you have your materials
// you can pass _this to that method and set these materials
// on some kind of controller property (e.g. materialsChosen)
// and use a flag like setting 'is Fetching' on the start of this
// computed property and setting down right here
});
}.property('#each.isSelected')
My data comes from REST API like this:
customers:[
id:3,
name:"Joue",
currency:{
id:5
iso_code:"BDT"
}
]
My model:
App.Customer = DS.Model.extend({
name: DS.attr('string'),
currency: DS.attr('string')
});
i populated a select box with the availabe currencies and now i want to select by "id" 5.
Since currency is embedded and its interpreted as string i cant access it.
As far as i know embedded records are no longer supported in ember-data 1.0.
do i have to rewrite my REST Api and get rid of the relationships or there is a workaround .
You can just create a custom serializer for the data.
Using your data (slightly modified, since the json isn't valid, and I'm guessing that's just cause it was hand written?)
{
customers:[
{
id:3,
name:"Joue",
currency:{
id:5,
iso_code:"BDT"
}
}
]
}
Here's a serializer for that particular response type (read more about it here https://github.com/emberjs/data/blob/master/TRANSITION.md)
App.CustomerSerializer = DS.RESTSerializer.extend({
extractArray: function(store, type, payload, id, requestType) {
var customers = payload.customers,
currencies = [];
customers.forEach(function(cust) {
var currency = cust.currency;
delete cust.currency;
if(currency){
currencies.push(currency);
cust.currency = currency.id;
}
});
payload = { customers:customers, currencies: currencies };
return this._super(store, type, payload, id, requestType);
}
});
And your models defined with a relationship
App.Customer = DS.Model.extend({
name: DS.attr('string'),
currency: DS.belongsTo('currency')
});
App.Currency = DS.Model.extend({
iso_code: DS.attr('string')
});
Example:
http://emberjs.jsbin.com/OxIDiVU/535/edit
currency is not "embedded", it's just an object. Don't declare it as a string in your model:
currency: DS.attr()
You say you want to "select" by id--what do you actually want to do? You can access the properties of currency directly:
{{! handlebars }}
Your currency id is {{currency.id}}.
// Javascript
id = model.get('currency.id');
No need for additional complexity involving serializers or additional models. However, you need to be careful when changing currency id, since
model.set('currency.id', 6)
will not dirty the model and it won't save. You'll need to also incant
model.notifyPropertyChange('currency')
I'm not sure if it's the correct way to express my requirement. But the word "fork" appears in the roadmap of Ember Data github page. And it's a killer feature in EPF. I'm wondering if I can do it in Ember Data.
The fork feature is useful when we have an edit page and bind a model. When we edit the information, I don't want the model properties to be changed because if the model properties are also displayed in other place they will be changed automatically. That's not what I want.
An example is a list on the left side of the page and a edit form for a specific model on the right side of the page. When I modify role name in the text field, the role name on the left side is changed because of data binding.
EPF solves this problem by "fork" the existing model and set it in a child session. The session in EPF is similar with store in Ember Data. When you modify the forked model it does not effect the model in the main session. After the forked model is updated it can be merged back to main session, and the corresponding model in main session are updated.
What I can think a solution in Ember Data is to create a different store and copy the model to that store. But it is a bit complicated. Does anyone have a better solution? I searched on stackoverflow and ember discuss forum and didn't find an answer.
I'm not sure if there is a standard or common way to do this in Ember, but I have written a Mixin that I can put on my routes to give some basic 'buffering' of the model:
App.BufferedRouteMixin = Ember.Mixin.create({
setupController: function(controller, model) {
this.setBufferFromModel(controller, model);
this._super(controller, model);
},
setBufferFromModel: function(controller, model) {
var buffer = {};
controller.set('model', model);
model.eachAttribute(function(name, meta) {
buffer[name] = model.get(name);
});
controller.set('buffer', buffer);
},
setModelFromBuffer: function() {
var model = this.get('model'),
buffer = this.get('buffer');
model.eachAttribute(function(name, meta) {
if (buffer[name]) {
model.set(name, buffer[name]);
}
});
}
});
Once this is added to my Edit Route, I can call setModelFromBuffer in my save action. In my templates, I can use the {{#with buffer}} helper.
What I believe to be the simplest solution, is to have an Ember.Object that mimics the structure of your model. When entering an edit mode, copy the properties from the model to the Ember.Object and then have them update there until the user clicks 'Save' or whichever action you wish to merge the changes back in. One thing I did that was important was to add the mixin Ember.Copyable to my object. Below is some code I used to solve this issue for myself.
NOTE:: This code was to prevent a model from being created before it was submitted, so instead of edit, mine is create new.
App.SomeModel = DS.Model.extend({
user: DS.belongsTo('user'),
home: DS.belongsTo('home'),
cost: DS.attr('number'),
title: DS.attr('string'),
description: DS.attr('string'),
category: DS.attr('number'),
categoryName: function () {
return Roomy.enums.TransactionCategories[this.get('category')]
}.property('category'),
date: DS.attr('date', {
defaultValue: function() { return new Date(); }
}),
fuzzyDate: function () {
return moment(this.get('date')).fromNow();
}.property('date'),
split: DS.attr('boolean'),
contributors: DS.attr('array'),
points: DS.attr('number')
});
App.SomeModelNew = Ember.Object.extend(Ember.Copyable, {
user: null,
home: null,
cost: null,
title: null,
description: null,
category: null,
date: new Date(),
split: false,
contributors: [],
points: null,
copy: function () {
return this.getProperties('cost', 'title', 'description', 'category', 'date', 'split', 'contributors', 'user', 'home');
}
});
Then to save this model I did something like this.
NOTE:: The code with User and Home I had to use because of the relationships, simply copying the json form of the User and Home would not persist the relationship and give the model the ID's it needed in the database.
Contoller code below:
//Before this function is called, all the inputs in the form have been filled in and the instance now has values for all the fields that were defined for it
saveTxn: function (txn) {
// txn is the App.SomeModelNew instance
copy = this.store.createRecord('transaction', txn); // this returns the App.SomeModelNew.copy() object
copy.set('user', txn.get('user')); // OVerwrite user for relationship
copy.set('home', txn.get('home')); // Overwrite home for relationship
return copy.save();
}
I hope this helps.
I'm using ember 1.0 and ember-data 1.0.0 beta 1. I have the following routes and controller to create and save simple notes ('AuthenticatedRoute' is just a custom made route for logged-in users):
App.Note = DS.Model.extend({
title: DS.attr(),
author: DS.attr(),
body: DS.attr(),
createdAt: DS.attr()
});
App.NotesRoute = App.AuthenticatedRoute.extend({
model: function() { return this.store.find('note'); },
});
App.NotesNewRoute = App.AuthenticatedRoute.extend({
model: function() {
return this.store.createRecord('note');
}
});
App.NotesNewController = Ember.ObjectController.extend({
actions: {
save: function() {
var self = this, model = this.get('model');
model.set('author', localStorage.username);
model.set('createdAt', new Date());
model.save().then(function() {
self.get('target.router').transitionTo('notes.index');
});
}
}
});
When I save a new note everything works as expected. But when I navigate away from the notes route and then back into it, the notes list is populated with a duplicate entry. One entry has an id and can be edited, deleted etc, the other has all the data of the first entry except the id attribute is null. It seems to me ember-data keeps the newly created record (that hasn't been committed to the database and thus has no id yet) alive even when the record becomes committed but I am uncertain as to why. When I reload the page, the list is correctly displayed, no duplicates appear. What am I doing wrong?
For the record, I am using mongodb so I use a custom serializer to convert '_id' attributes to ember-data friendly 'id's, essentially copied from here:
App.NoteSerializer = DS.RESTSerializer.extend({
normalize: function(type, hash, property) {
// 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);
}
});
I should also mention that this problem existed in ember-data 0.13 as well.
It was a stupid mistake in my RESTful server. I was responding to POST requests with a 204 (empty) response instead of what ember-data expected, that is a 201 ("created") response with the newly created record as the payload. This post made me realize it.
It would be nice though to include this information in the official REST adapter documentation.
That is certainly strange behaviour indeed. Unfortunately I'm not able to explain why you're experiencing this, however:
You can use the willTransition callback in the actions object in your Route to ensure that when it is transitioned away from, if NotesNewController's content property is dirty (i.e. has not been persisted yet), it will have its transaction rolled back.
App.NotesNewRoute = App.AuthenticatedRoute.extend({
model: function() {
return this.store.createRecord('note');
},
actions: {
willTransition: function (transition) {
var model = this.controllerFor('notesNew').get('content');
if (model.get('isDirty') === true) {
model.get('transaction').rollback();
}
return this._super(transition);
}
}
});
I have an application logic that requires two models to have reciprocal hasMany relationships. As an example, imagine a set of GitHub issues that can be tagged with several labels.
I am trying to use an adapter that extends the default RESTAdapter. All the application works fine but the double hasMany relationship throws an exception. Digging into the code, a method inverseBelongsToForHasMany throws an exception.
So, I guess that Ember.Data does not support the association of two models with hasMany relationships in both sides and every hasMany requires an associated belongsTo. My questions are:
Is this supported and the issue is just I am doing something wrong?
If it is not supported, is it a feature planned to appear?
Is this a association type to be avoided in this kind of applications? If so, which is the best approach or workaround?
Thanks in advance
We use a similar method of creating the association object. However, instead of overriding the methods in store, we just added the join objects to the api.
so in the models we create:
App.Hashtag = DS.Model.extend({
hashtagUsers: DS.hasMany('App.HashtagUser', {key: 'hashtag_user_ids'})
});
App.User = DS.Model.extend({
hashtagUsers: DS.hasMany('App.HashtagUser', {key: 'hashtag_user_ids'})
});
App.HashtagUser = DS.Model.extend({
user: DS.belongsTo('App.User'),
hashtag: DS.belongsTo('App.Hashtag')
});
Then for the transactions we simply alter and commit the join object.
App.UserController = Ember.ObjectController.extend({
followHashtag: function(tag) {
var hashtagUser;
hashtagUser = this.get('hashtagUsers').createRecord({
hashtag: tag
});
tag.get('hashtagUsers').pushObject(hashtagUser);
App.store.commit();
}
unfollowHashtag: function(tag) {
var itemToRemove;
itemToRemove = this.get('hashtagUsers').find(function(hashtagUser) {
if (hashtagUser.get('hashtag') === this) {
return true;
}
}, tag);
this.get('hashtagUser').removeObject(itemToRemove);
tag.get('hashtagUser').removeObject(itemToRemove);
itemToRemove.deleteRecord();
App.store.commit();
});
The API creates a HashtagUser object and the follow method just adds that user to both the associated pieces.
For removal, it pops the associated objects and destroys the association object.
Although it's not as elegant as it could be, our big motivation was that when Ember Data gets updated then we should be able to transition it to a simple stock Ember Data supported version more easily than if we've messed with the Store itself.
Many to Many relationships are not yet supported in ember-data. For the moment, one possible workaround is to manually manage the join table.
A = DS.Model.extend({
abs: DS.hasMany('Ab'),
bs: function () {
return this.get('abs').getEach('b');
}
});
Ab = DS.Model.extend({
a: DS.belongsTo('A'),
b: DS.belongsTo('b')
});
B = DS.Model.extend({
abs: DS.hasMany('Ab'),
bs: function () {
return this.get('abs').getEach('a');
}
});
This is just the starting point. You need then to customize your models and adapter in order to send/receive/persist records in a working manner
For example, in our app, we introduce an { includedJoin: true } option inside the hasMany relations, and declare the join table as a JoinModel
A = DS.Model.extend({
abs: DS.hasMany('Ab', {includeJoin: true}),
...
});
DS.JoinModel = DS.Model.extend();
Ab = DS.JoinModel.extend({
... belongsTo relationships ...
});
Then in the Adapter, we override the create/update/delete methods in order to ignore the joins table lifecycle in the store
createRecords: function (store, type, records) {
if (!DS.JoinModel.detect(type)) {
this._super(store, type, records);
}
}
Finally, in the serializer, we override the addHasMany function in order to send the join data to the server as embedded ids in the parent models.
addHasMany: function (hash, record, key, relationship) {
var
options = relationship.options,
children = [];
//we only add join models, use of `includeJoin`
if (options.includedJoin) {
record.get(relationship.key).forEach(function (child) {
children.pushObject(child.toJSON({
includeId: true
}));
});
hash[key] = children;
}
}
Server-side we are using Rails with ActiveModelSerializer, so the only little-tricky-customization is when when we update the parent models, we manually manage the joins relation, and create/delete entries in the join table.