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}}
Related
I spent a chunk of time yesterday trying to include filter (reflecting the JSONAPI spec) in the query params of part of an Ember app. With Ember Data it is easy enough to pass a filter array to an endpoint, the problem I have is reflecting that filter array in the query params for a particular route. Note: other, non array, query params are working fine.
TL;DR I have tried various options without success and have a solution that really feels unsatisfactory and not at all DRY. I figure that many others must have tackled this problem and have surely found a better solution. Read on for details of what I have tried so far.
I started with something like this (I initially assumed it would work having read the Ember docs on query params):
Controller:
import Controller from '#ember/controller';
export default Controller.extend({
queryParams: ['sort', 'filter'],
sort: 'id',
init() {
this._super(...arguments);
this.set('filter', []);
},
});
Route:
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
filter: {
refreshModel: true
},
sort: {
refreshModel: true
}
},
model(params) {
console.log(JSON.stringify(params)); // filter is always []
return this.get('store').query('contact', params);
}
});
Acceptance Test (this was just a proof of concept test before I started on the more complex stuff):
test('visiting /contacts with query params', async function(assert) {
assert.expect(1);
let done = assert.async();
server.createList('contact', 10);
server.get('/contacts', (schema, request) => {
let params = request.queryParams;
assert.deepEqual(
params,
{
sort: '-id',
"filter[firstname]": 'wibble'
},
'Query parameters are passed in as expected'
);
done();
return schema.contacts.all();
});
await visit('/contacts?filter[firstname]=wibble&sort=-id');
});
No matter how I tweaked the above code, params.filter was always [] in the Route model function.
I have searched around for best-practice on what would seem to be a common use case, but have not found anything recent. sarus' solution here from Nov 2015 works, but means that every possible filter key has to be hardcoded in the controller and route, which seems far from ideal to me. Just imagine doing that for 20 possible filter keys! Using sarus' solution, here is code that works for the above acceptance test but as I say imagine having to hardcode 20+ potential filter keys:
Controller:
import Controller from '#ember/controller';
export default Controller.extend({
queryParams: ['sort',
{ firstnameFilter: 'filter[firstname]' }
],
sort: 'id',
firstnameFilter: null,
init() {
this._super(...arguments);
}
});
Route:
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
firstnameFilter: {
refreshModel: true
},
sort: {
refreshModel: true
}
},
model(params) {
if (params.firstnameFilter) {
params.filter = {};
params.filter['firstname'] = params.firstnameFilter;
delete params.firstnameFilter;
}
return this.get('store').query('contact', params);
}
});
I hope there's a better way!
If you don't have the requirement to support dynamic filter fields, #jelhan has provided a really good answer to this question already.
If you do need to support dynamic filter fields read on.
First of all, credit should go to #jelhan who put me on the right track by mentioning the possibility of serializing the application URL with JSON.stringify() and encodeURIComponent() together.
Here's example code with this working...
Controller:
import Controller from '#ember/controller';
export default Controller.extend({
queryParams: ['sort', {
filter: {
type: 'array'
}
}],
sort: 'id',
init() {
this._super(...arguments);
this.set('filter', []);
},
});
Route (no changes required):
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
filter: {
refreshModel: true
},
sort: {
refreshModel: true
}
},
model(params) {
return this.get('store').query('contact', params);
}
});
Acceptance Test:
test('visiting /contacts with query params', async function(assert) {
assert.expect(1);
let done = assert.async();
server.createList('contact', 10);
server.get('/contacts', (schema, request) => {
let params = request.queryParams;
assert.deepEqual(
params,
{
sort: '-id',
"filter[firstname]": 'wibble',
"filter[lastname]": 'wobble'
},
'Query parameters are passed in as expected'
);
done();
return schema.contacts.all();
});
// The filter is represented by a Javascript object
let filter = {"firstname":"wibble", "lastname":"wobble"};
// The object is converted to a JSON string and then URI encoded and added to the application URL
await visit('/contacts?sort=-id&filter=' + encodeURIComponent(JSON.stringify(filter)));
});
Great! This test passes. The filter defined in the application URL is passed through to the Route. The Route's model hook makes a JSONAPI request with the filter correctly defined. Yay!
As you can see, there's nothing clever there. All we need to do is set the filter in the correct format in the application URL and the standard Ember Query Params setup will just work with dynamic filter fields.
But how can I update the filter query param via an action or link and see that reflected in the application URL and also make the correct JSONAPI request via the Route model hook. Turns out that's easy too:
Example Action (in controller):
changeFilter() {
let filter = {
firstname: 'Robert',
lastname: 'Jones',
category: 'gnome'
};
// Simply update the query param `filter`.
// Note: although filter is defined as an array, it needs to be set
// as a Javascript object to work
// this.set('filter', filter); - this seems to work but I guess I should use transitionToRoute
this.transitionToRoute('contacts', {queryParams: {filter: filter}});
}
For a link (say you want to apply a special filter), you'll need a controller property to hold the filter, we'll call it otherFilter and can then reference that in the link-to:
Example Controller property (defined in init):
init() {
this._super(...arguments);
this.set('filter', []);
this.set('otherFilter', {occupation:'Baker', category: 'elf'});
}
Example link-to:
{{#link-to 'contacts' (query-params filter=otherFilter)}}Change the filters{{/link-to}}
There you have it!
There is no reason to represent filter values in applications URL the same way as they must be for backend call to be JSON API complaint. Therefore I would not use that format for application URLs.
If you don't have the requirement to support dynamic filter fields, I would hard code all of them to have nice URLs like /contacts?firstname=wibble&sort=-id.
Your code would look like this, if you like to support filtering for firstname and lastname:
// Controller
import Controller from '#ember/controller';
export default Controller.extend({
queryParams: ['sort', 'page', 'firstname', 'lastname'],
sort: 'id',
});
// Route
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
firstname: {
refreshModel: true
},
lastname: {
refreshModel: true
}
sort: {
refreshModel: true
}
},
model({ firstname, lastname, sort, page }) {
console.log(JSON.stringify(params)); // filter is always []
return this.get('store').query('contact', {
filter: {
firstname,
lastname
},
sort,
page
});
}
});
If you have to support dynamic filter fields, I would represent the filter object in application URL. For serialization you could use JSON.stringify() and encodeURIComponent() together. The URL would then look like /contacts?filter=%7B%22firstname%22%3A%22wibble%22%7D&sort=-id.
I have a model in a route that has data i'd like to chart using a chart plugin (ember-google-charts or ember-charts specifics do not matter to my issue)
in routes/server.js:
import Ember from 'ember';
export default Ember.Route.extend({
model(params) {
return Ember.RSVP.hash({
stats: this.store.query('stat', {filter: {serverId: params.server_id}})
server: this.store.findRecord('server', params.server_id),
})
},
setupController(controller, models) {
controller.setProperties(models);
}
}
});
My issue where/how to make stats into a proper array (using timechecked and players attributes) in order to pass the data into a charting plugin in the template.
I've tried something similar to:
stats: this.store.query('stat', {filter: {serverId: params.server_id}}).then(items => {
//modify items here
})
but i have a hard time figuring out how to manipulate the data and build a new array without throwing errors using a forEach (which exists on the ember Array class)
1.You can use forEach to iterate and return result you created. refer this answer for work with ember model
stats: this.store.query('stat', { filter: { serverId: params.server_id } }).then(items => {
//modify items here
let result =[];
items.forEach(function(element) {
let value1 = element.get('propertyName');
result.pushObject(value1);
}, this);
return result;
})
2. If you just want plain array then you can use toArray method.
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);
}
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.
power-select]1. The options i pass to power select comes from a service. The option array has duplicate values which i try to remove in my controller.
I have made a twiddle put since it ember-twiddle does not support addonss (ember-power-select) till now. I have modified it little bit
Ember twiddle
The comment code in twiddle is what I have on desktop. This code works in tweedle but does not work on my computer.
Issue: dropDown value is not visible on template
import Ember from 'ember';
const {
get,
set,
computed
} = Ember;
export default Ember.Service.extend({
locations: computed(function(){
var self = this;
return get(this,'store')
.findAll('store')
.then(stores =>{
let locs = stores.map(store => {
return get(store,'adminAreaLevel2') +" "+ get(store,'adminAreaLevel1')
});
set(self,'locations',locs);
});
});
And the code in controller is
//Controller
import Ember from 'ember';
const {
inject: { service },
computed: { uniq }
}=Ember;
export default Ember.Controller.extend({
dropData: service('my-service'),
dropLocation: uniq('dropData.locations')
});
The power-select-code that I cannot include in twiddle
{{#power-select
options=dropLocation
selected=selectedLoc
matcher=locMatcher
onchange=(action "selectLoc") as |loc|}}
{{first-caps loc}}
{{/power-select}}
{{/bs-form-group}}
UPDATES
Hi #Bek thanks for solution some one suggest me same in ember slack community. the problem with this solution is dropLocation: uniq('dropData.locations') does not get updated as uniq does not work with promise array properly. I have come up with ugly solution but it work.
dropLocation: Ember.computed('dropData.locations',function(){
return this.get('dropData.locations')
.then(data => data.uniq());
})
Looking for good solution
you cannot observe change on promise itself you can use PromiseObject (or PromiseArray) to wrap your promise so that it is observable.
var promise = Ember.RSVP.resolve({ name: 'mario' });
var object = Ds.PromiseObject({ promise });
now in your template you can use it like:
{{object.name}} // mario
make sure that your promise returns object not primitive (or array for PromiseArray).
in your case it should look like this:
locations: computed(function(){
var promise = get(this,'store')
.findAll('store')
.then(stores =>{
let locs = stores.map(store => {
return get(store,'adminAreaLevel2') +" "+ get(store,'adminAreaLevel1')
});
return locs;
});
return DS.PromiseArray.create({ promise });
});