How to represent complex objects in Ember models? - ember.js

This SO question explains how to store an array in an Ember model, but how could it be done with a custom object, which also isn't supported natively supported, according to the Guides.
This is an example of the object that I am building
obj[0] = {"timeInMinutes": ">120", "occurrences": 24 };
obj[1] = {"timeInMinutes": "90-120","occurrences": 69 };
obj[2] = {"timeInMinutes": "60-90", "occurrences": 53 };
obj[3] = {"timeInMinutes": "30-60", "occurrences": 35 };
obj[4] = {"timeInMinutes": "0-30", "occurrences": 24 };
Update.
Using the information provided in this answer, I was able to create an array attribute on my model along with several other values, but I also want to be able to create DS.attr('object') attribute to be used like this. To create an object type, do I need to use a DS.Transform.extend({ as was done with the array in the linked to SO answer?
App.Index = DS.Model.extend({
names: DS.attr('array'),
country: DS.attr('string'),
statistics: DS.attr('object')

If you want the property to be a primitive object and not an Ember Object then you can do the following:
ObjectTransform = DS.Transform.extend({
deserialize: function(serialized) { return serialized; },
serialize: function(deserialized) { return deserialized; }
});
This is assuming that you don't need to change the object at all after it comes in over the wire.

If you’re using Ember Data, you’ll have to structure your models in an Ember Data-compatible way. You could have a parent Object (surely has a better name, but you didn’t explain your domain at all) that hasMany TimespanOccurrences or the like.

Related

How can I re-use the same model template for different requests in Ember?

Imagine I have 20 charts on the index page of my application. I can request the data points for each chart in JSON:API form from the API:
export default Route.extend({
model() {
return {
chart01: this.store.findAll('chart-timetable-01'),
chart02: this.store.findAll('chart-timetable-02'),
// ... etc
All routes from the API return a collection of documents that each represent a data point. Every document has the same attributes: name and value.
Now even though they are all exactly the same, I need to have 20 models:
models/chart-timetable-01.js
models/chart-timetable-02.js
# ... etc
Now that I want to introduce a second chart type with an actual different model, I want to simplify this. Can I use one model for all charts that are functionally the same, so that I don't have to duplicate the model for every new chart?
So in stead of chart-timetable-{01..20}.js I can have one simple chart.js model?
I can override the type with a serializer:
import DS from 'ember-data'
export default DS.JSONAPISerializer.extend({
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (primaryModelClass.modelName.match(/^chart-timetable-/g)) {
payload.data.forEach((doc, idx) => doc.type = 'chart')
}
return this._super(...arguments)
}
})
But Ember still wants models named in the findAll to exist. How can I override this?
I'm not sure if I get it correctly but maybe this will help.
Ember provides some helpful called rsvp. You can use it to store all data into return object
import { hash } from 'rsvp';
And then just place whatever You want:
return hash({
chart01: this.store.findAll('chart-timetable-01'),
chart02: this.store.findAll('chart-timetable-02'),
});
And now your model looks like:
chart01{...},
chart02{...}
Hope this will help!

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

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() {}
});

Ember.js RESTAdapter attributes types

RESTAdapter has built in attribute types of string, number, boolean, and date. There are relations to link another models to represent some complex data.
To represent array I need to use transformation or change API from something like this:
["ember.js", "angular.js", "embergular.js"]
to:
[
{
"id": 1,
"ember.js"
},
{
"id": 2,
"angular.js"
},
{
"id": 3,
"embergular.js"
}
]
Which is a little bit overkill... Why there is no built in types like array and object?
IMO the main reason why there aren't such attribute type like array or object is mainly per design.
To represent array I need to use transformation or change API from something like this:
but to represent an array without the needs of building a relation with models you could define a custom transform (what you already mentioned) which don't touches your data. For example to use an array as a model attribute you could do something like this:
DS.RESTAdapter.registerTransform('rawData', {
deserialize: function(serialized) {
return serialized;
},
serialize: function(deserialized) {
return deserialized;
}
});
Then define it in your model like this:
App.MyModel = DS.Model.extend({
myArray: DS.attr('rawData')
});
This way the attribute myArray will be just what your backend returned, an array or object etc.
Hope it helps.

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.