Ember-data createRecord with hasMany relationship without saving - ember.js

I want to create a new site record. The model looks like:
var SiteModel = DS.Model.extend({
name: attr(),
...
languages: DS.hasMany('language'),
});
The language property describes in which languages the content of a site can be written. To create the form, I need to create a model in my route. So I want to create a new record, without saving this one to the db:
var WebsitesNewRoute = Ember.Route.extend({
model: function() {
return this.store.createRecord('site', {
languages: Ember.A()
});
}
}
That does not work as intended, as I got the following error: cannot set read-only property "languages" on object: <app#model:site::ember1012:null>>. Why is the languages property readOnly? As far as I know I did not configure that in my model...
I know the question Ember Data- createRecord in a hasMany relationship, but in my case I don't want to save anything yet (I only want to create the model, so I could use it in my template).

Ember-Data defines languages as a read-only property because it doesn't want you to replace the array. No matter if you're saving or not, Ember-Data wants you to add relationships with addObject and remove relationships with removeObject.
So if you wanted to add a language, you would do this:
model: function() {
var model = this.store.createRecord('site');
var language = getLanguage();
model.get('languages').addObject(language);
return model;
}
What you're doing by giving languages to createRecord, is essentially calling model.set('languages', Ember.A()), and Ember-Data doesn't like that.
It's dumb, I know, but that's just how Ember-Data works.

Related

How do I properly bind controller changes to models?

I am relatively new to Ember.js, so I am giving myself a project to figure things out.
I believe I understand the very basics. Controllers contain state-logic, while models contain model attribute-logic.
In my example, I have a collection of models. These models contain an attribute that represents an id of another model:
App.Pokeball = DS.Model.extend({
name: DS.attr('string'),
rate: DS.attr('number'),
pokemon: DS.belongsTo('pokemon')
});
I have a Controller that contains selectedPokemonId and selectedPokemon attributes. When selectedPokemonId changes, I want to automatically update all the Pokeball models.
I know its awful, but here is the function I am using to update the Models:
selectedPokemon: function(selectedPokemonId) {
var pokemonId = this.get('selectedPokemonId'),
store = this.store,
id = 1,
max = App.Pokeball.FIXTURES.length;
for (id,max; id<= max;id++) {
store.update('pokeball', {
id: id,
pokemon: pokemonId
});
}
return store.find('pokemon', this.get('selectedPokemonId'));
}.property('selectedPokemonId'),
Technically, this does what I need it to... but I am certain I am not doing this the "ember way", there has to be a cleaner way to bind the relationship between controller state and models.
Github Example Code here
Working example
I like to work directly with models as objects instead of managing record ids. Doing this greatly simplifies your code. Here's how I would accomplish this.
First, your route should return all the models you want to work with using the model hook.
The route's model hook should look something like:
model: function()
{
return Ember.RSVP.hash ({
pokeballs: this.store.find('pokeball'),
pokemon: this.store.find('pokemon')
});
}
In general you want to do store.find calls in the route model hook because they can be asynchronous (return a Promise) and the model hooks waits for promises to resolve before proceeding. This ensures your data will always be ready for your controller to work with it. More here: http://emberjs.com/guides/models/finding-records/. Note that the model we'll be working with is an object with two properties, pokeballs and pokemon, which are both collections representing all the respective objects in the store.
In your controller, instead of a selectedPokemonId, you can reference a selectedPokemon model object directly. You can then observe the change to the selectedPokemon using 'observes' and then simply set the selectedPokemon on each pokeball and save each pokeball model to persist it back to the store. If you're just using fixtures you could get away without even saving each pokeball because 'set'-ing a property on the model object is enough to change it in the store.
selectedPokemonObserver: function()
{
var thePokemonToSet = this.get('selectedPokemon');
this.get('pokeballs').forEach( function( aPokeball ) { // note you can also do this.get('model.pokeballs') since the model is an object with two properties, pokeballs and pokemon
aPokeball.set('pokemon', thePokemonToSet); //note that instead of an id, i'm setting the pokemon model object here to satisfy the belongsTo relationship
aPokeball.save(); // you might not need this if using only fixtures and not persisting to db.
});
}.observes('selectedPokemon')
Anything referencing these model objects in your templates will automatically be updated.
I think the "Ember way" to do what you want to accomplish is to use an observer instead of a property:
...
selectedPokemonObserver: function() {
var pokemonId = this.get('selectedPokemonId'),
store = this.store,
id = 1,
max = App.Pokeball.FIXTURES.length;
for (id, max; id <= max; id++) {
store.update('pokeball', {
id: id,
pokemon: pokemonId
});
}
}.observes('selectedPokemonId'),
selectedPokemon: function() {
return this.store.find('pokemon', selectedPokemonId);
}.property('selectedPokemonId'),
...

Is it required to use createRecord before creating a new model instance?

While building my first app with ember and ember-data I've noticed that at one point I started getting the following error when typing in an input field for a newly created model:
Assertion Failed: Cannot delegate set('notes', t) to the 'content' property of object proxy : its 'content' is undefined.
I solved this issue by adding the following code to the route:
App.LessonNewRoute = Ember.Route.extend({
model: function() {
return this.store.createRecord('lesson');
}
});
My understanding is that this error started happening when I created (instead of letting ember generate) the LessonController using ObjectController.
I'm now wondering:
Is it required to use createRecord before creating a new model instance?
Is this the best way of preventing this error from happening?
As far as I understand, Your approach is good.
In order to use a model in a view, you have to have the model instance available somehow. So if you try to use content when nothing has been assigned to it, it will fail. I go around that with a different approach, but creating the record within an action handler.
For some scenarios, specially with small models, I generally create corresponding properties in the controller (sort of a viewModel approach) with an action to handle the save. Then I actually create the record in that action, passing the controller properties arguments of createRecord.
Example (totally conceptual):
...
App.Person = DS.Model.extend({
name: DS.attr('string')
});
...
App.PersonAddController = Em.ObjectController.extend({
personName: null,
actions: {
save: function() {
var theName = this.get('personName');
var person = this.store.createRecord('person', {name: theName});
person.save().then(
...pointers to success and failure handlers go here...
).done(
...transition goes here...
);
},
cancel: function {
this.set('personName', null);
}
}
})
Then in the template, I bind the input to the controller prop rather than a model.
{{input type="text" value=controller.personName}}
With this approach, my Route#model doesn't output a blank model to be added to my store, so I don't have to deal with rollback or anything.

Temporary Non-Persistent Record with Ember-Data 1.0.0-beta

I'm new to Ember and Ember-data and am deciding whether to use Ember-Data or one of the other persistence libraries. In order to evaluate, I'm experimenting with writing a small Rails-backed app.
One of my routes can be considered similar to the Todo MVC app that is frequently used in examples.
In my template, I have a number of input fields that represent attributes within the model. Furthermore, I also have one element in the model that represents a hasMany relationship.
Models:
App.CompanyModel = DS.Model.extend
company: DS.attr()
desc: DS.attr()
contacts: DS.hasMany('company_contact')
App.CompanyContactModel = DS.Model.extend
firstname: DS.attr()
lastname: DS.attr()
...
Within my controller, I want to be able to create a new CompanyModel record (and by virtue, add one or more contacts models to it), but not have it appear within the controller's instance of the CompanyModel until I'm ready to do so.
Currently, when a user wants to add a new record, I have a component that calls an action in my controller as follows:
#set('new_company',
#store.createRecord('company')
)
This actually works fine, except for one thing. My view has to populate the individual attributes within "new_company", which it does, however, the record is immediately added to the controller's model instance and appears in the list of records; I only want the newly created record to be visible in the table once a particular action has taken place.
Instead of instantiating new_company with createRecord, I could do something like this:
#set('new_company',
Ember.Object.create
companyname: ''
desc: ''
contacts: [
firstname: ''
lastname: ''
]
)
And then do a #store.createRecord('company', #get('new_company')), however, given I've already defined my attributes in the model, it doesn't feel very DRY to me.
I'm using Ember 1.5.0 and Ember-Data 1.0.0-beta.7.
It appears I'm not the first person to have this issue (create temporarty non persistent object in Ember-Data), but it appears that Ember-Data has sufficiently changed to make all of these solutions inoperable.
Thanks for your help!
You're real issue is you're using what's considered a live collection. I'm going to assume in your route you've done something like this:
App.FooRoute = Em.Route.extend({
model: function(){
return this.store.find('company');
}
});
find with no parameters says, hey Ember Data, find me all the records that are company. Well Ember Data shoots off a request to your back-end, then returns store.all('company'). all is a live collection that will always have all the records of that type currently in the store. In your case, you are saying I want to avoid any record that is new. There are a couple of ways to handle this.
Create a static list. (You'll need to manually add/remove objects to/from this list).
App.FooRoute = Em.Route.extend({
model: function(){
return this.store.find('company').then(function(companies){
return companies.toArray();
});
}
});
Example: http://emberjs.jsbin.com/OxIDiVU/641/edit
Create a computed property that only shows records that aren't new
App.FooRoute = Em.Route.extend({
model: function(){
return this.store.find('company');
}
});
App.FooController = Em.ArrayController.extend({
savedRecords: function(){
return this.get('model').filterBy('isNew', false);
}.property('model.#each.isNew')
// shorthand this could be written like this
// savedRecords: Ember.computed.filterBy('model', 'isNew', false)
});
Then in your template you would iterate over the computed property
{{#each item in savedRecords}}
{{/each}}
Example: http://emberjs.jsbin.com/OxIDiVU/640/edit

How do you get the id of an association in Ember without loading the association?

I am trying to use the Ember.Select view class, and I am looking for a way to retrieve the id of an association without loading the full model.
I am using Ember Data 1.0.0-beta.7. My property model looks like:
var attr = DS.attr;
App.Property = DS.Model.extend({
neighborhood: DS.belongsTo('neighborhood'),
name: attr()
});
An example of my payload looks like this:
{"property": neighborhood: 5, name: "Foo"}
I just need the id of the neighborhood in order to pass it to the select view value property so that it can be selected. I do not want to load the full neighborhood payload though because I only need the id.
Is there a way to do this without changing my model?
Try using property._data.neighborhood.id. The payload data is stored in the _data hash on the record, but it's not really recommended to use these!
I have a similar use case--I want to check if the record has a parent, but I don't actually want to retrieve the parent when I'm rendering a list. Ember goes crazy and makes 100 requests, which is not exactly desirable!
You can inspect the belongsTo relationship directly in a computed function.
import Ember from 'ember';
var attr = DS.attr;
App.Property = DS.Model.extend({
neighborhood: DS.belongsTo('neighborhood'),
name: attr(),
neighborhoodId: Ember.computed(function(){
return this.belongsTo('neighborhood').id();
}).volatile()
});
Making the computed property volatile means that it is recomputed every time it is called - not only when new data comes in. This way, you don't always get an invalid value before the model is completely loaded.

Saving nested models

I have two models like this:
App.Build = DS.Model.extend({
allegiance: DS.attr('string'),
profession: DS.attr('string'),
skills: DS.hasMany('skill')
});
App.Skill = DS.Model.extend({
name:DS.attr('string'),
value:DS.attr('number')
});
In my app, I have controls to set the allegiance, profession, and values of each skill (there's up to 55).
Then in the actions hash of my application controller, I have an action to save the build model to the server.
save:function(){
var store = this.get('store');
var skills = this.get('controllers.skills').get('model');
console.log(skills);
var build = store.createRecord('build',{
profession:1,
allegiance:1,
skills:skills
});
build.set('skills',skills);
build.save();
console.log('Saved!');
}
But when the build model is sent to the server the skills property is an empty array:
{"build":{"allegiance":"1","profession":"1","skills":[]}}
I'm sure I'm doing something wrong, but I can't figure out what and can't find any good documentation about it. An additional note, all I care about submitting is the skill id and value.
Any help will be greatly appreciated!
UPDATE:
Following Daniel's suggestion, I've edited the save function to use pushObjects to put the skills into the Build model, then save it. It's working better now. The generated post data is like this now:
{"build":{
"allegiance":1,
"profession":1,
"skills":["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50","51","52","53","54","55"]}}
That being a list of the skill ids. None of the other attributes are submitted in the post. I've tried iterating over skills, creating a new object, and just pushing in the id and value, which are the only parts I need, but that gives me an error. Something like, can not use undefined, must be type skill.
This seems like something Ember data should handle natively. Is there something I'm missing to get it to send the other skill attributes in the request?
Thanks!!
If anyone else is interested, I solved the issue by overriding the serlizer with a custom serliazer for the Build model like this:
App.BuildSerializer = DS.RESTSerializer.extend({
serializeHasMany: function(record, json, relationship) {
if(relationship.key === 'skills') {
var skills = record.get('skills');
var block = [];
skills.forEach(function(skill, index) {
var current = {};
current.id = skill.get('id');
current.value = skill.get('value')
block[index] = current;
});
json['skills'] = block;
} else {
return this._super(record,json,relationship);
}
}
});
UPDATE:
There's a much easier way to do this now using the DS.EmbeddedRecordsMixin like this:
App.BuildSerializer = DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin,{
attrs: {
skills: 'records'
}
});
Is the skills model a RecordArray? That's the underlying model Ember data uses. You might try creating the record then using pushObjects after the fact.
var build = store.createRecord('build',{
profession:1,
allegiance:1
});
build.get('skills').pushObjects(skills);
additionally, save returns a promise, so in order to properly handle the successful save versus failure you can handle it like this.
build.save().then(
function(){
console.log('Saved!');
},
function(){
console.log('Failed to save');
});