I get some data from my API through model in route.js. This data contains somewhere an id on its own, with no relationships or included stuff to get details. So I have to make another API request to get the object with that id.
I did it with a component (to be able to send the id argument) and it works, but I would like to know if that's the way to go and if so, if I did it right and it cannot be simplified (because it looks complex to me for such a simple task):
I call my component with {{resource-name id=evaluation.manager}}
Component template just contains {{name}}
component.js:
import Ember from 'ember';
export default Ember.Component.extend({
store: Ember.inject.service(),
_getResource(id) {
return this.get('store').findRecord('resource', id);
},
resource: Ember.computed('id', function() {
const id = this.get('id');
const proxy = Ember.ObjectProxy.extend(Ember.PromiseProxyMixin);
return proxy.create({
promise: this._getResource(id)
});
}),
name: Ember.computed('resource.isFulfilled', function() {
if (this.get('resource.isFulfilled')) {
return `${this.get('resource.lastName')} ${this.get('resource.firstName')}`;
}
else {
return "...";
}
}),
didReceiveAttrs() {
const id = this.getAttr('id');
Ember.assert('resource-name must have an "id" attribute!', !Ember.isBlank(id));
}
});
Related
store.findRecord('school', school_id, {
include: [
'students',
'students.records'
].join(',')
Using the above code fetching school, students, students' records data in inital load.
In the initial load, I don't need students.records (only listing students initially)
Need student records only when clicking some button (All Students Performance - to display a performance chart)
Is there any way to fetch associated records separately and link with the existing model
I have sperate api endpoint for fetch students' records
emphasized textyou could use a nested route?
maybe something like:
// router.js
Router.map(function() {
// ...
this.route('school', { path: ':school_id' }, function() {
this.route('performance');
});
// ...
})
// the school route
import Route from '#ember/routing/route';
export default class extends Route {
async model(params) {
const { school_id } = params;
const school = await store.findRecord(
'school', school_id, { include: 'students' }
);
return { school }
}
}
// the performance route
import Route from '#ember/routing/route';
export default class extends Route {
async model() {
// get the id from the parent route
const { school_id } this.paramsFor('school');
const school = await store.findRecord(
'school', school_id, { include: 'students.records' }
);
// depending if the relationship is async or not, students
// will need to be awaited
const students = await school.students;
return { students }
}
}
This is all using the same schools endpoint.
If you want to fetch only the student's records, without hitting the school's endpoint, that depends on your API's capabilities.
if you wanted to fetch a singular student's records you'd probably want to have a sub-route for the student, and do something like this:
// router.js
Router.map(function() {
// ...
this.route('school', { path: ':school_id' }, function() {
this.route('performance');
this.route('students', function() {
this.route('student', { path: ':student_id' });
});
});
// ...
})
and you'd link to that route like this:
{{#link-to 'school.students.student' school_id student_id}}
Student Name
{{/link-to}}
and in your student route, you'd want to construct your model hook like this:
// the student route
import Route from '#ember/routing/route';
export default class extends Route {
async model(params) {
// get the id from the parent route
const { school_id } this.paramsFor('school');
const { student_id } = params;
const student = await store.findRecord(
'student', student_id, { include: 'records' }
);
// depending if the relationship is async or not, students
// will need to be awaited
const records = await student.records;
return { student, records };
}
}
You may want to open another question about your API if you're uncertain how to interact with it. There we could explore how it is structured, like if it's a http://jsonapi.org/ API or a non-standard rest api.
hope this helps.
In one of my routes I need to findAll() of user's Items, but instead of making the standard /items request it has to go to /my/items.
My current solution involves:
// routes/my/item.js
export default Ember.Route.extend({
model() {
this.store.unloadAll('item');
return Ember.$.getJSON('/my/items').then((payload) => {
this.store.pushPayload(payload);
return this.store.peekAll('item');
});
}
});
But unfortunately it's not ideal since it requires to unloadAll() items before making the request as to ensure that the model only returns freshly fetched records while unloading any cached.
A better solution will probably involve creating a custom adapter specifically for this route and overwriting either the findAll() method or urlForFindAll(), but I'm not sure how to properly create and import such custom adapter.
Just for testing I overwrote the default Item adapter and returned findAll('item') in the route's model, and everything worked, the request was prefixed with /my/:
// adapters/item.js
findAll: function(store, type, sinceToken, snapshotRecordArray) {
var query, url;
if (sinceToken) { query = { since: sinceToken }; }
// prefix url with `my` str
url = `my${this.buildURL(type.modelName, null, null, 'findAll')}`;
return this.ajax(url, 'GET', { data: query });
},
// routes/my/item.js
export default Ember.Route.extend({
model() {
return this.store.findAll('item');
}
});
..but that obviously overwrites all findAll() queries for this model, wherein I need to make a custom query only in this route.
This can be solved by using adapterOptions to pass options to the item's adapter using findAll:
1) In the route use adapterOption to pass prefix to the adapter:
return this.store.findAll('item', { adapterOptions: { prefix: 'my' } });
2) In ember-cli overwrite item's default adapter with ember g adapter item.
3) In the adapter overwrite the default findAll to prefix url if such option is passed:
// /app/adapters/item.js
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
findAll: function(store, type, sinceToken, snapshotRecordArray) {
var query, url;
if (sinceToken) { query = { since: sinceToken }; }
let prefix = Ember.get(snapshotRecordArray, 'adapterOptions.prefix');
url = `${prefix || ''}${this.buildURL(type.modelName, null, null, 'findAll')}`;
return this.ajax(url, 'GET', { data: query });
},
});
4) Success, this.store.findAll('item', { adapterOptions: { prefix: 'my' } }); will now make a my/items instead of items!
I have a component which creates record for a specific model like this:
export default Ember.Component.extend({
store: Ember.inject.service(),
addRecord(account) {
this.get('store').createRecord('update', {
authUid: account.get('authUid'),
service: account.get('platform')
});
}
});
I have another component that needs to observe changes done to a particular model (i.e. if records are added or deleted), and show them in that component.
export default Ember.Component.extend({
store: Ember.inject.service(),
observeStoreChanges: /*What should I write so that every time `addRecord`
pushes record in the store, a function is executed in this component*/
});
If you're a fan of the observer pattern:
// store.js
export default DS.Store.extend(Ember.Evented, {
createRecord() {
const record = this._super.apply(this, arguments);
this.trigger('recordCreated', record);
return record;
}
});
// component.js
export default Ember.Component.extend({
observesStoreChanges: function(record) {
}.on('store.recordCreated')
});
I have a custom component that expects data and not a promise, but I am unsure if they way that I am obtaining the data is the right way.
Is this the right way to do it?
component hbs
{{x-dropdown content=salutations valuePath="id" labelPath="description" action="selectSalutation"}}
Doesn't work
controller (this is the way I expect things to work
import Ember from 'ember';
export default Ember.Controller.extend({
bindSalutations: function() {
var self = this;
this.store.find('salutation').then(function(data) {
self.set('salutations', data);
});
}.on('init'),
components/x-dropdown.js
import Ember from 'ember';
export default Ember.Component.extend({
list: function() {
var content = this.get('content');
var valuePath = this.get('valuePath');
var labelPath = this.get('labelPath');
return content.map(function(item) {
return {
key: item[labelPath],
value: item[valuePath],
};
});
}.property('content'),
This works
controller
bindSalutations: function() {
var self = this;
this.store.find('salutation').then(function(data) {
self.set('salutations', data.get('content')); // pass the content instead of just the data
});
}.on('init'),
component
...
list: function() {
var content = this.get('content');
var valuePath = this.get('valuePath');
var labelPath = this.get('labelPath');
return content.map(function(item) {
return {
key: item._data[labelPath], // access through the _data attribute
value: item._data[valuePath],
};
});
}.property('content'),
Ember Data returns a Proxy Promise. This means you can use the promise as if it were a collection or model itself, as long as you aren't dependent on the property being completely populated when you use it. If you really want the promise resolved, you should probably be setting it up in the route.
If you want it on your controller, you can be lazy and do it like so:
Controller
salutations: function() {
this.store.find('salutation');
}.property(),
Component
...
list: function() {
var content = this.get('content'),
valuePath = this.get('valuePath'),
labelPath = this.get('labelPath');
return content.map(function(item) {
return {
key: item.get(labelPath),
value: item.get(valuePath),
};
});
}.property('content.[]'),
Template
{{x-dropdown content=salutations valuePath="id" labelPath="description" action="selectSalutation"}}
The real trick is to watch if the collection is changing. Hence you'll see I changed the property argument to content.[]
I want to have a dynamic segment path in Ember without using the :id attribute
As per the Ember Guides, I'm using the serialize method to achieve this.
Heres my Router:
App.Router.map(function() {
this.resource("orders", function(){
this.resource('order', { path: ':order_sequence'}, function(){
this.route('edit');
})
});
});
And my Route:
var OrderRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('order', params.order_sequence)
},
serialize: function(model) {
return { order_sequence: model.get('sequence') };
}
});
module.exports = OrderRoute;
However, my URL's still behave using the id attribute in the path instead of the sequence attribute..
Any ideas?
Is your browser caching something, because that's correct. Are you passing in the id instead of the sequence/model in any of your transitionTo/transitionToRoute/link-to?
Oh, you aren't talking about the slug in the url, nor the route, you are talking about the id of your model. You need to create a serializer for that particular model and override the primary key
App.OrderSerializer = DS.RESTSerializer.extend({
primaryKey: 'sequence'
});
The Fixture Adapter has a constraint on defining the id, but you can lazily get around it by extending the fixture adapter and overriding a single method
App.OrderAdapter = DS.FixtureAdapter.extend({
fixturesForType: function(type) {
if (type.FIXTURES) {
var fixtures = Ember.A(type.FIXTURES);
return fixtures.map(function(fixture){
// aka we massasge the data a bit here so the fixture adapter won't whine so much
fixture.id = fixture.sequence;
var fixtureIdType = typeof fixture.id;
if(fixtureIdType !== "number" && fixtureIdType !== "string"){
throw new Error(fmt('the id property must be defined as a number or string for fixture %#', [fixture]));
}
fixture.id = fixture.id + '';
return fixture;
});
}
return null;
},
});