Just getting started with Ember.js, so after a workign myself through the various tutorials online for a couple of weeks(…), I really can't puzzle out the following question.
I want to display 4 models on 1 route. How can I do that, while avoiding making 4 server calls?
More inforamtion:
I want to display records of type "person", "quote", "product" and "case" on my index page.
In my index route, (routes/index.js) I can load them using:
import Ember from "ember";
export default Ember.Route.extend({
model(){
return Ember.RSVP.hash({
persons : this.get('store').findAll('person'),
quotes : this.get('store').findAll('quote'),
cases : this.get('store').findAll('case'),
products: this.get('store').findAll('product')
});
}
});
(And in my adapter, adapters/application.js, I have: )
import DS from "ember-data";
export default DS.JSONAPIAdapter.extend({
host : 'http://localhost:8080/dummy.php',
pathForType: function (type) {
return "?type=" + type;
}
});
This works very nicely :), but ember.js makes 4 requests:
However, I could easily provide a JSON file that provides records of all 4 types.
So, how can I tell ember.js:
"Here's a nice large JSON file, full of records. Now, use only records
of the 'person'-type for the person model, and idem dito for 'case',
'quote' and 'product'
?
Nothing wrong in loading model per request. If models are related then you should consider defining relationship between them. again for loading any async data it will make network request.
If you want to load data in single request for different model type, then you can try the below, this is not ember-data way. so I will not encourage this.
import Ember from "ember";
const {RSVP} = Ember;
export default Ember.Route.extend({
model() {
return RSVP
.resolve(Ember.$.getJSON('http://localhost:8080/dummy.php'))
.then((result) => {
this.get('store').pushPayload(result);
return {
persons : this.get('store').peekAll('person'),
quotes : this.get('store').peekAll('quote'),
cases : this.get('store').peekAll('case'),
products: this.get('store').peekAll('product')
};
});
}
});
Well, you probably could implement this in your adapter. This could give you some idea what you could do:
export default DS.JSONAPIAdapter.extend({
init() {
this._super(...arguments);
this.set('toLoad', {});
},
loadDebounces() {
const toLoad = this.get('toLoad');
this.set('toLoad', {});
const keys = Object.keys(toLoad);
const data = magicLoadForAllAllKeys(); // just do something you like here. Sent the keys as POST, or GET array, websockets, smoke signals..
Object.keys(data).forEach(key => {
toLoad[key].resolve(data[key]);
});
},
findAll (store, type, sinceToken, snapshotRecordArray) {
return new Ember.RSVP.Promise((resolve, reject) => {
this.set(`toLoad.${type}`, { resolve, reject });
Ember.run.debounce(this, this.loadDebounces, 1);
});
},
});
You basically can just debounce multiple requests and process them as one. However this is not RESTFull nor JSONAPI compliant. Just to mention this.
Related
My ember version:
DEBUG: -------------------------------
Ember : 2.10.2
Ember Data : 2.11.0
jQuery : 2.2.4
Ember Simple Auth : 1.1.0
Model Fragments : 2.3.2
DEBUG: -------------------------------
And my route code:
import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
import RSVP from 'rsvp';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model() {
console.log(1);
return RSVP.hash({
...,
user: this.store.findRecord('user', this.get('session.data.authenticated.id'))
});
},
afterModel(model, transition) {
return this.store.findRecord('company', model.user.get('companyId')).then(company => {
console.log(2);
this.set('company', company);
});
},
setupController(controller, model) {
console.log(3);
controller.set('user', model.user);
controller.set('company', this.get('company'));
}
});
Look at console.log code, I think the correct order should be 1->2->3. But sometimes it turns out to be 1->3->2.
But my company id must come from user api. So what is way I set it in route? Thanks.
I am writing just another solution, From RSVP.hash api docs
Returns a promise that is fulfilled when all the given promises have been fulfilled, or rejected if any of them become rejected. The returned promise is fulfilled with a hash that has the same key names as the promises object argument. If any of the values in the object are not promises, they will simply be copied over to the fulfilled object.
So you can write your requirement like the below code,
model() {
var promises = {
user: this.store.findRecord('user', this.get('session.data.authenticated.id'))
};
return Ember.RSVP.hash(promises).then(hash => {
//then method will be called once all given promises are fulfilled, or rejected if any of them become rejected.
return this.store.findRecord('company', hash.user.get('companyId')).then(company => {
hash.company = company; // Here i am setting company property inside model itself, so you dont need to set it in route and carry over to controller
return hash;
});
})
}
Note:I am curious to know if you can reproduce 1->3->2 behavior in ember-twiddle.
Actually the right way to do this is to put all your model fetching in your model hook:
model() {
return RSVP.hash({
...,
user: this.store.findRecord('user', this.get('session.data.authenticated.id'))
}).then(hash => {
hash.company = this.store.findRecord('company', hash.user.get('companyId'));
return RSVP.hash(hash);
})
},
setupController(controller, model) {
controller.set('user', model.user);
controller.set('company', model.company);
}
Since the ember-guides explains how to load mutliple models on a route like that
export default Ember.Route.extend({
model() {
return Ember.RSVP.hash({
songs: this.get('store').findAll('song'),
albums: this.get('store').findAll('album')
});
}
});
Im wondering how to load only the related model-entries from a second one, like loading ALL songs but only the albums which are indexed in the songs if we assume that the song model containing this
...
albums: hasMany('album'),
...
How can I do that?
Assuming your adapter and JSON API backend support it, you can simply say:
export default Ember.Route.extend({
model() {
return Ember.RSVP.hash({
songs: this.get('store').findAll('song', { include: 'albums' }),
});
}
});
Typically, this will generate a GET to /songs?include=albums, which tells the JSON API backend to include the related album resources, according to http://jsonapi.org/format/#fetching-includes.
On the Ember side of things, this feature is documented at http://emberjs.com/blog/2016/05/03/ember-data-2-5-released.html#toc_code-ds-finder-include-code.
If the above isn't an option, then there's no way to load everything in one request without building a custom endpoint and using store.pushPayload.
Here is one way to do it
export default Ember.Route.extend({
model() {
var promise = new Ember.RSVP.Promise(function(resolve,reject){
this.store.findAll('song').then(function(songs){
var albumPromises = songs.map(fuction(s){return s.get('album')});
Em.RSVP.all(albumPromises).then(function(){
resolve(songs);
});
});
});
return promise;
}
});
So Basically you are waiting till everything is resolved.
Hope it helps!
I am using Ember 1.13.9 an Ember-data 1.13.11 and struggling to have Ember Data do what I would like. As an example, I have a model called "goal" and a
goals: Ember.on('init', Ember.computed(function() {
const {store} = this.getProperties('store');
return store.findAll('goal');
})),
When this runs it does query the database and put the appropriate records into the store BUT getting them out of the store is my problem. I would have thought that once the Promise resolved that I'd be able to iterate over the array of results. Using the inspector I can see that at clients.goals.content.content (where clients is the name of the server I see this from the inspector:
First of all this is pretty deep into the structure. I was hoping Ember's "get" would allow me to simply say something like data.get('content.0.id') but this just comes back as undefined. Second of all the crazy structure continues in that each of these listed objects are InternalModel objects which only have the following structure to them:
Note that:
there are two InternalModels, that is the right number (matches store results)
the id property is available here
there is an internal property called _data which has the other attributes of the record
Ok so in a completely hacky way I could pull out what I need but surely I shouldn't be writing code like:
_goals: Ember.on('init', function() {
const {store} = this.getProperties('store');
store.findAll('goal').then(data => {
let result = [];
data.forEach(item => {
let record = item.get('data'); // this gets what's in _data apparently
record.id = item.get('id');
result.push(record);
}
this.set('goals', result);
}),
Yuck. What am I missing?
If you need to convert Ember model to plain object you can use Model.serialize or Model.toJSON methods.
Update:
If you need to not just extract the data from models but to access fetched models via computed property, there are several ways to implement it.
1) Synchronous property (collection):
Controller:
import Ember from 'ember'
export default Ember.Controller.extend({
goals: [],
someProperty: Ember.computed('goals.#each', function () {
var goals = this.get('goals');
goals.forEach(goal => {
console.log( goal.get('someProperty') );
});
})
});
Route:
import Ember from 'ember'
export default Ember.Route.extend({
setupController: function (controller, model) {
this._super(controller, model);
this.store.findAll('goal').then(goals => {
controller.set('goals', goals);
});
}
});
Template:
{{#each goals as |goal|}}
{{log goal}}
{{/each}}
2) Asynchronous property (promise):
Controller:
import Ember from 'ember'
export default Ember.Controller.extend({
goals: Ember.computed(function () {
var storeGoals = this.store.peekAll('goal') || [];
if (storeGoals.length) {
return RSVP.resolve(storeGoals);
} else {
return this.store.findAll('goal')
}
}),
someProperty: Ember.computed('goals.#each', function () {
var goals = this.get('goals').then(resolvedGoals => {
resolvedGoals.forEach(goal => {
console.log( goal.get('someProperty') );
});
});
})
});
Template:
{{#each goals as |goal|}}
{{log goal}}
{{/each}}
Cross-posting from discuss.ember. I am using Ember 2.0.1 with Ember-data 2.0 and default the default RESTSerializer generated by ember-cli. I know this question has been asked to many places before (which none have real answers) but no solutions have been working for me yet.
I have this model hook for a user model :
export default Ember.Route.extend({
model() {
return this.store.findAll('user');
}
});
Router is the following :
Router.map(function() {
this.route('users', { path: '/' }, function() {
this.route('user', { path: '/:user_id' }, function(){
this.route('conversations', { path: '/'}, function(){
this.route('conversation', { path: '/:conversation_id' });
});
});
});
});
For example, going to /conversations/4 transitions to users.user.conversations. My relations are defined in my models. In the user model I have a DS.hasMany('conversation') conversations attribute set with { embedded: 'always' }. Returned JSON looks like this :
{"conversations":[
{
"id":183,
"status":"opened",
"readStatus":"read",
"timeAgoElement":"2015-08-20T16:58:20.000-04:00",
"createdAt":"June 16th, 2015 20:00",
"user":
{
"id":4
}
}
]}
The problem I get is that Ember-data is able to add my data to the store but I get this error :
Passing classes to store methods has been removed. Please pass a dasherized string instead of undefined
I have read these posts : #272 and #261
Is it a problem with the JSON response?
Thank you. I have been using ember-data for quite a bit of time and never encountered this error before switching to ember 2.0.1 and ember-data 2.0.0
EDIT : I am now sure it is related to the embedded conversations because in ember inspector, if I try to see the conversations of a user (and the conversations are loaded into the store), it returns me a promiseArray which isn't resolved.
Try not to push objects to store directly. Possible use-case of .push() :
For example, imagine we want to preload some data into the store when
the application boots for the first time.
Otherwise createRecord and accessing attributes of parent model will load objects to the store automatically.
In your case UserController from backend should return JSON:
{"users" : [ {"id":1,"conversations":[183,184]} ]}
Ember route for conversation may look like:
export default Ember.Route.extend({
model: function(params) {
return this.store.find('conversation', params.conversation_id);
}
}
User model:
export default DS.Model.extend({
conversations: DS.hasMany('conversation', {async: true})
});
You don't have to always completely reload model or add child record to store. For example you can add new conversation to user model:
this.store.createRecord('conversation', {user: model})
.save()
.then(function(conversation) {
model.get('conversations').addObject(conversation);
});
P.S. Try to follow Ember conventions instead of fighting against framework. It will save you a lot of efforts and nervous.
Your conversation route has URL /:user_id/:conversation_id. If you want it to be /:user_id/conversations/:conversation_id, you should change this.route('conversations', { path: '/'}, function(){ to this.route('conversations', function(){ or this.route('conversations', { path: '/conversations'}, function(){
I'm trying to set up a shopping bag model in Ember. The shopping bag will be created on load of the page and saved in LocalStorage using the LocalStorage adapter. At any given time, there should only be one instance of a bag saved, as a user only needs to add products to one shopping bag. My question is this: it seems that I'm being hack-y with my methods of getting and setting data on my bag as Ember data caters to models with more than one instance. Is there a better way to structure/define my bag model that is better suited for one-instance models? Here's my model:
import DS from 'ember-data';
export default DS.Model.extend({
products: DS.hasMany('product', {async: true}),
productCount: function() {
return this.get('products.length');
}.property('products.length')
});
When I want to get the productCount in my template, the only way I can seem to get it to print is use an {{#each}} statement with {{productCount}} nested inside. As there is only one bag, this seems inefficient. In other parts of my code, I need to get the current instance of the bag and act on it. To get this to work, I'm finding all bags, then getting the firstObject, which also seems hack-y:
import Ember from "ember";
export default Ember.ArrayController.extend({
actions: {
addToBag: function(model) {
this.store.find('bag').then(function(bags) {
var bag = bags.get('firstObject');
bag.get('products').then(function(products) {
products.pushObject(model);
bag.save();
});
});
}
}
});
My application route uses the bag as its model, and sets up the controller:
import Ember from "ember";
var ApplicationRoute = Ember.Route.extend({
activate: function() {
var store = this.store;
store.find('bag').then(function(bags) {
var existing_bag = bags.get('firstObject');
// If there isn't already a bag instantiated, make one and save it
if(typeof existing_bag === 'undefined') {
var new_bag = store.createRecord('bag');
new_bag.save();
}
});
},
model: function() {
return this.store.find('bag');
},
setupController: function(controller,model) {
controller.set('content', model);
}
});
Any ideas here to make this more efficient? I don't want this to fester into code that is messy. Thanks so much in advance!
If that is your BagController above, it should be an ObjectController instead.
The reason why you're having to get the first object is because find fetches all items of that model type in your store. You may only have one, but find doesn't know that unless you provide an id and if this bag hasn't been stored in your database, it may not have one yet.
Instead of fetching the bag model from the store, I would link you Bag and Products controllers with needs, then simply access the model property of that controller. You can even set up an alias to be able to access it quickly.
For example:
import Ember from "ember";
export default Ember.ArrayController.extend({
needs: 'bag',
bag: Ember.computed.alias("controllers.bag.model").
actions: {
addToBag: function(model) {
this.get('bag').get('products').pushObject(model);
}
}
});
The ApplicationRoute is only fired once, when you're app first boots up so you don't need to check if there's already a bag model present. The only one that will be there is the one you create. You should do this in the model hook. You don't need to set content as the model. It'll be hooked up like that by default.
import Ember from "ember";
export default Ember.Route.extend({
model:function() {
return this.store.createRecord('bag');
}
});
If you may want the ability to have multiple shopping bags going forward you could try using the 'singleton' approach Discourse follows: https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/mixins/singleton.js
Basically it adds 'current' property that you can use as your single instance throughout your code.
However, if you won't have a need to have multiple instances this may not be the best choice.