Sounds like a simple enough thing to do yet is causing me all sorts of grief.
I have a simple server model which has a few nested objects,
export default DS.Model.extend({
type: DS.attr('string'),
attributes: DS.attr(),
tasks: DS.attr()
});
I can create a new record in the route using
export default Ember.Route.extend({
model() {
return this.store.createRecord('server');
},
actions: {
create(server) {
server.save().then(() => this.transitionTo('servers'));
}
}
});
and in the related .hbs I'm setting a few properties of attributes and tasks using value=model.attributes.name from a form for example.
This all works fine. I however want to add a few more properties from the route during create such as default values.
Using server.set('attributes.size', 'large'); doesn't work as Ember doesn't know about size yet as it's a new record.
I can use setProperties but this seems to wipe out every other value
server.setProperties({
attributes: {
size: "large"
},
tasks: {
create: true
}
});
size is now correctly set, however name is now null because I didn't specify it in the setProperties...
What's the proper way to go about this? Surely I don't need to map out all the properties in setProperties? That seems wasteful and very error prone.
Something I've thought is should attributes just be its own model and have a relationship with Server? Even though this is always a 1-to-1 and 1-to-1 relationship?
I would recommend using ember-data-model-fragments addon as a solution in this case.
https://github.com/lytics/ember-data-model-fragments
Other option using a separate model for attributes and setting up a 1-to-1 relation. Both would be belongsTo, however it is depend on your database and API also, so you have to align your backend system to match with this new structure.
Related
installed versions
ember-cli 2.14.2
ember-data 2.14.10
A little perspective:
I have a service called menu that performs store queries inside computed properties. One of these store queries is behaving rather weird. It fetches all records under the model name product-segment from a fully functional JSON API. This model has a n-n relationship to a model called product, referenced through hasMany DS objects:
models/product-segment.js
export default DS.Model.extend({
products: DS.hasMany('product'),
// ...
});
models/product.js
export default DS.Model.extend({
productSegments: DS.hasMany('product-segment'),
// ...
})
The problem:
Now, when I fetch these product-segment models, I instruct the API to { include: 'products' }, and the API does as is requested. The response includes 15 related product models for a particular product-segment, which is correct.
(let's call this particular product-segment segment x, it's the subject for all my debugging info below)
However, accessing the relationship collection on segment x from any context, at any time, only returns me 12 models, so 3 are missing. I witnessed similar issues with other product-segment models, so I don't think it's an issue with one specific model.
More perspective
I initially thought I was dealing with a race condition of some kind, and to be sure I created a computed property - test - to find out, and I dumped {{menu.test}} into my view to tickle the computed prop.
Here's the bare minimum info inside services/menu.js
export default Service.extend({
store: inject(),
activeProductSegment: null,
// As a note: this does not trigger an infinite loop
productSegments: computed('store.product.[]', 'store.product-segment.[]', function() {
return get(this, 'store').findAll('product-segment', { include: 'products' });
}),
test: computed('activeProductSegment', function() {
let segment = get(this, 'activeProductSegment');
if (segment) {
console.log(segment.hasMany('products').ids());
console.log(get(segment, 'products').mapBy('id'));
}
}),
});
The property activeProductSegment is being set to different product-segment model instances through an action of a component , which looks like this:
export default Component.extend({
menu: inject(), // menu service is injected here...
actions: {
setProductSegment(segment) {
get(this, 'menu').set('activeProductSegment', segment);
}
}
});
The action itself works as expected, and is actually quite unrelated to my problem. activeProductSegment is never updated in any other way. The view passes this action product-segment model objects:
{{#each menu.productSegments as |segment|}}
<li {{action 'setProductSegment' segment}}>{{segment.name}}</li>
{{/each}}
Trouble starts here
I set menu.activeProductSegment to segment x by clicking its associated <li> element.
When I now try to get all related product models of segment x, only 12 of 15 models are present within the returned collection. To be sure that the JSON response was really fine (i.e. the type definitions etc. are right) I checked the amount of product IDs that were registered at segment x. I logged the following line (the context of the logs below is in the Ember.Service snippet above):
console.log(segment.hasMany('products').ids());
That returned me an array with 15 correct IDs, so segment x has all id's as it is supposed to have. All product models of those id's have been included in the response, so I suppose there should be no issue with async data of some kind. Still, the following line gave me back an array of 12 id's:
console.log(get(segment, 'products').mapBy('id'));
I tried putting the 2 logs into a 2-second setTimeout, but the result stayed identical:
I'm starting to think this is a bug, since I noticed that the first time that an id was not accompanied by a model, is when for the first time the next ID is lower than the preceding ID.
Update on the above I tried a different order in the response, and note the second and the third id's: "7", "6". Guess this is not the problem:
Unless I misunderstand, the models should be live, so any relationship is supposed to update as data becomes available. I think it is very unlikely this has anything to do with malformed data.
What could be the cause for the missing models in the hasMany relationship collection, despite the fact that all necessary ids are properly registered at the hasMany relationship object, and we're not required to await arrival of any async/network data at this point? And what might be a suitable solution to the problem?
I know it does not appear to be related to async issues, but I would still try defining the hasMany as not async:
products: DS.hasMany('product', {async: true}),
I've two different models, with hasMany/belongsTo relationships between them. Ordering the request regular positions (like in this case name) by Emberfire it's easy. But I cannot figure it out how to do it with relationships
On the route's template we have Model1 in the route and loop through different positions on model 1. Inside, we loop through linked model2 positions, but they should be ordered by day
Model 1
export default DS.Model.extend({
name : DS.attr('string'),
model2 : DS.hasMany('model2', {async: true})
});
Model 2
export default DS.Model.extend({
day : DS.attr('number'),
model1 : DS.belongsTo('model1', { async: true })
});
When you access the async property you are merely accessing the entities associated with that relationship, not performing a search (so there is no way, at least without rolling your own solution, to filter these entities on the fetch).
The easiest way to handle this would be to have a computed property on your controller / class which orders the child models by day.
Another option (if you want Emberfire to handle the ordering for you) is to just not rely on lazy loading, and rather query for child models directly and order by the day field in that query.
-- Edit for example --
You'd want to watch changes to your selected model's model2 property as follows:
sortedList: Ember.computed('selectedModel.model2.[]', function() {
return this.get('selectedModel.model2').sortBy('day');
}
I need to access the application specific data in my components as well as routes. I also need to set the application specific data from normal JS.
I have currently created an object with global namespaces (App.globalSetting) and then created the variables as properties on this object.
I am then able to set and get the variable using App.globalSetting.set() and App.globalSetting.get().
Is the above method a good practice or is there a better way to do the same.
Also the data to be stored is critical. So please suggest a best way to accomplish this task.
You may want to take a look at Services: http://guides.emberjs.com/v2.0.0/services/.
From the guides:
"An Ember.Service is a long-lived Ember object that can be injected as needed."
Example service:
settings-service.js
export default Ember.Service.extend({
exampleSetting: true,
update(value) {
this.set('exampleSetting', value);
}
});
How to access this service from a Component (or Route):
export default Ember.Component.extend({
settings: Ember.inject.service('settings-service'),
actions: {
doSomething(val) {
this.get('settings').update(val);
}
}
});
I am trying to define a computed property that consists of a filtered hasMany relationship. When I loop over the items of the PromiseManyArray, I get undefined when trying to access the attribute I want to filter on. On later calls to this computed property, everything works fine.
This is a simplified version of my controller code:
export default Ember.Controller.extend({
availableModules: function () {
let thisModule = this.get('model')
console.log(thisModule.get('library.modules')) // This logs <DS.PromiseManyArray:ember604>
// loop over siblings
return thisModule.get('library.modules').filter(mod => {
// mod.classification is undefined
return mod.get('classification') !== 'basis'
})
}.property('model')
})
For the Module model we can assume that it has a classification attribute, and it belongs to a Library object, and the Library model hasMany modules.
I have tried something like this, and it logs properly the attribute classification, but I don't know how to return anything so that the template can render it.
availableModules: function () {
let thisModule = this.get('model')
thisModule.get('library.modules').then(mods => {
mods.forEach(mod => {
console.log(mod.get('classification'))
})
})
}.property('model')
So the problem seems to be that inside of the PromiseManyArray.filter method, the attributes of the found objects are not yet resolved... How can I create a promise that will return all filtered objects once those have been resolved? I don't know how to get my head around this. Thanks.
Inspired by Bloomfield's comment, and with help of this thread in the ember forum, I have found an acceptable solution. Basically it consists of resolving all the relationships in the route, so that when the controller is called, you don't have to deal with promises.
Solution:
In the model hook of the route, return a hash of promises of all the needed information
Define a custom setupController, and inside of it, store the model and the extra data in the controller
The route code looks like this:
export default Ember.Route.extend({
model(params) {
let module = this.store.findRecord('module', params.mod_id)
return Ember.RSVP.hash({
module: module,
siblingModules: module.then(mod => mod.get('library.modules')), // promise based on previous promise
})
},
setupController(controller, hash) {
controller.set('model', hash.module)
controller.set('siblingModules', hash.siblingModules)
},
})
Note: for the route to still work properly, the {{#link-to 'route' model}} have to explicitly use an attribute, like the id: {{#link-to 'route' model.id}}
Extra info
Bloomfield's approach consisted of using the afterModel hook to load the extra data in an attribute of the Route object, and then in the setupController, set the extra data in the Controller. Something like this:
export default Ember.Route.extend({
model(params) {
return this.store.findRecord('module', params.mod_id)
},
afterModel(model) {
return model.get('library.modules').then(modules => {
this.set('siblingModules', modules)
})
},
siblingModules: null, // provisional store
setupController(controller, model) {
controller.set('model', model)
controller.set('siblingModules', this.get('siblingModules'))
},
})
But this feels like a hack. You have to return a promise in afterModel, but you can't access the result. Instead the result has to be accessed via .thenand then stored in theRoute` object... which is not a nice flow of information. This has however the advantage that you don't have to specify any attribute for the links in the template.
There are more options like using PromiseProxyArray, but that's too complicated for a newcomer like me.
For anyone running into PromiseManyArray issues in modern times, make sure you have async: false explicitly set on any hasMany relationships directly serialized by the API. Modern versions of Ember will behave unexpectedly if you don't, such as computed properties not working when you use pushObject.
I got nested JSON data from the server like this:
{
name: "Alice",
profile: {
something: "abc"
}
}
and I have the following model:
App.User = Ember.Object.extend({
name: null,
profile: Ember.Object.extend({
something: null
})
})
If I simply do App.User.create(attrs) or user.setProperties(attrs), the profile object gets overwritten by plain JS object, so currently I'm doing this:
var profileAttr = attrs.profile;
delete attrs.profile
user.setProperties(attrs); // or user = App.User.create(attrs);
user.get('profile').setProperties(profileAttrs);
It works, but I've got it in a few places and in the real code I've got more than one nested object, so I was wondering if it's ok to override User#create and User#setProperties methods to do it automatically. Maybe there's some better way?
Based on your comment, you want the automatic merging behaviour you get with models (the sort of thing you get with .extend()). In that case, you could try registering a custom transformer, something like:
App.ObjectTransform = DS.Transform.extend({
deserialize: function(json){
return Ember.Object.create(json);
}
});
App.User = DS.Model.extend({
profile: DS.attr('object')
});
See: https://github.com/emberjs/data/blob/master/TRANSITION.md#json-transforms
If you are doing your server requests without an adapter you can use the model class method load() with either an array of json objects or a single object. This will refresh any known records already cached and stash away the JSON for future primary key based lookups. You can also call load() on a model instance with a JSON hash as well but it will only update that single model instance.
Its unclear why you are not using an adapter, you can extend one of the Ember Model adapters and override the the record loading there, eg. extend from the RESTAdapter and do any required transform on the JSON if required by overriding _loadRecordFromData
You can also override your models load function to transform data received if required as well. The Ember Model source is fairly easy to read so its not hard to extend to your requirements.