How do I properly bind controller changes to models? - ember.js

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'),
...

Related

How can I use a model in a component in isolation but maintain a one-way bind for model refreshes?

I currently have component that displays a table of results based on the model passed into it:
\\pagedContent is a computed property of the model
{{table-test model=pagedContent}}
The table updates its contents as various filters are selected via query params. I've bee trying to implement some 'sort on click' behaviour to the table headings with the following code:
import Component from '#ember/component';
import {
computed
} from '#ember/object';
export default Component.extend({
model: null,
init() {
this._super(...arguments);
this.dataSorting = ['total_users']
this.dataSortingDesc = ['total_users:desc']
},
sortedDataDesc: computed.sort('unsortedData', 'dataSortingDesc'),
sortedData: computed.sort('unsortedData', 'dataSorting'),
unsortedData: computed('model', function () {
return this.get('model');
}),
actions: {
columnsort(property) {
if (!this.get('tableSorted')) {
this.set('dataSortingDesc', [`${property}:desc`])
this.set('model', this.get('sortedDataDesc'))
this.set('tableSorted', true)
} else {
this.set('dataSorting', [property])
this.set('displayModel', this.get('sortedData'))
this.set('model', null)
}
},
}
});
The sorting works as expected but I have a problem due to the two way binding of the model. Other components on the template also uses the model and when the data in the table is sorted, it creates all kinds of problems with those components.
I tried to create a seperate 'copy' of the model using a computed property like follows:
\\a new property
displayModel: computed('model', function () {
return this.get('model');
}),
sortedDataDesc: computed.sort('unsortedData', 'dataSortingDesc'),
sortedData: computed.sort('unsortedData', 'dataSorting'),
unsortedData: computed('model', function () {
return this.get('model');
}),
actions: {
columnsort(property) {
if (!this.get('tableSorted')) {
this.set('dataSortingDesc', [`${property}:desc`])
this.set('model', this.get('sortedDataDesc'))
this.set('tableSorted', true)
} else {
this.set('dataSorting', [property])
this.set('displayModel', this.get('sortedData'))
this.set('model', null)
}
},
The table then iterates over displayModel to create itself. This produces a behaviour where the columns sort but then the display 'freezes' once a column heading is clicked and does not update as the underlying model updates. In this case, I can see from my other components that the model continues to update as new filters are applied.
I was also unsuccessful using a oneWay and didUpdateAttrs implementation.
How can I create a copy of the model in the component so that I can sort the table columns without changing the whole model via two-way binding whilst keeping a one way bind so that if the model is updated by the parent template, it will also update in the component?
Edit:
I've created a twiddle here
If you click on the header of the table, you can see that both of the components change their order because I am working on the passed 'model'.
What I am trying to achieve is a workflow in which I can pass the model into the table component so it displays data and I can sort the columns without affecting the second component (also being fed by the model).
The problem is I also need the property populating the table to refresh if something else (a set of filters existing on my parent template) refresh the model through interaction on the parent template.
So a 'sort' affects the property populating the table and nothing else BUT the property populating the table is sensitive to model updates on the parent hosting the component.
The problem here is that you're sharing the array backing the models between components, and then manipulating the array (which Ember is aware of). If you stopped sharing the array (by copying the references into a second array):
import Ember from 'ember';
export default Ember.Route.extend({
model(){
return [{name: "Frank", age: 22}, {name: "Alan", age: 43}, {name: "Bob", age: 56}]
},
setupController(controller, model){
controller.set('model', model);
controller.set('tableModel', model.slice(0));
}
});
And change you're application.hbs like:
{{my-component model=tableModel}}
{{second-component model=model}}
you would only see the change in order happen to the table component. Since both arrays point to the same references, your models themselves are bound to both arrays (ie changing model properties like age affects both model and tableModel since they're actually pointing to the same piece of memory. But the sorting will only affect the tableModel since you've now allocated two arrays
I've expanded upon your gist with my own copy in which I manipulate a referenced model in the models array and it affected both models and tableModels since the underlying elements in the array are the same references.

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

Ember-data createRecord with hasMany relationship without saving

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.

Ember Relationships without Ember Data

I have an Ember App where some Models use Ember Data and some don't. My question relates to creating relationships between these Models and also the best way to structure the Model relationships.
Models
Currently I have the following Models:
Foods
not using Ember Data
makes $.ajax request to external API
extends a Ember.Object (see here and here for examples of the methodology)
Meals
uses Ember Data
has many Portions
Portions
uses Ember Data
hasOne Meal
hasOne Food
In my app I need a Portion to be a unique record which has a weight field. Each Portion should derive it's other values from a associated Food. A Meal should contain many Portions.
Questions
Should Portions be a Model in it's own right our should it be stored in some kind of array-like structure as a field on the Meal (eg: portions)? Consider that a Portion is not reusable and is only able to be associated with a single Meal.
If "Yes" to #1 then what could my Meal Model def look like?
As Food does not use Ember Data what's the best technique for defining a relationship between a Portion and a Food?
Ultimately the User experience should allow someone to
View a Food
Create a Portion of that Food
Associate the Portion with a Meal
View all Portions associated with a Meal
Your help is much appreciated.
Q1: Should Portions be a Model in it's own right our should it be stored in some kind of array-like structure as a field on the Meal (eg: portions)?
I'm not sure you are asking if Portions should be a model or Portion should be a model. But whatever I think the solution is to build Portion as a model and build portions relationship for Meal model. Because you have functionality to create a portion with a food. In my understanding the portion should be created without a meal (although it can link to a meal later).
Q2: If "Yes" to #1 then what could my Meal Model def look like?
The model definition is like this:
App.Portion = DS.Model.extend({
weight: DS.attr(),
meal: DS.belongsTo('meal', {async: true})
});
App.Meal = DS.Model.extend({
portions: DS.hasMany('portion', {async: true})
});
Q3: As Food does not use Ember Data what's the best technique for defining a relationship between a Portion and a Food?
It's better to still use Ember Data to define Food model, just define your custom adapter and serializer, Ember Data handles the rest. The DS.Adapter and DS.Serializer documentations are good place to start. Below is a simple example.
// Just name it "FoodAdapter" and Ember Data knows to use it for "Food".
App.FoodAdapter = DS.Adapter.extend({
find: function(store, type, id) {
// The returned value is passed to "serializer.extract" then "store.push"
return this._ajax({url: '/external/food', type: 'GET'});
},
createRecord: function() {},
updateRecord: function() {},
deleteRecord: function() {},
findAll: function() {},
findQuery: function() {},
_ajax: function(options) {
// Transform jQuery promise to standard promise
return Em.RSVP.cast($.ajax(options));
}
});
App.FoodSerializer = DS.Serializer.extend({
// Assume the json is:
// {
// "food_data": {
// "name": "XXX",
// "price": 100
// }
// }
extract: function(store, type, payload, id, requestType) {
return payload.food_data;
},
serialize: function() {}
});

EmberData: Two models related with hasMany relationships

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.