EMBER JS - Fetch associated model data from back-end only when required - ember.js

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.

Related

How to carry some data between page using model in Ember?

I am trying to carry some data by updating the ( adding ) some properties to existing model. But it's not carried to next page. so i tried like this:
combineArray:Ember.computed(function(){
let trans = Ember.A();
let index = 0;
var that = this;
this.get('model.ccList').map(function(card,num){
let name = card.get('cardName');
let numb = card.get('cardNum');
card.get('cardtransactions').map(function(transaction){
console.log( 'transaction', JSON.stringify(transaction) )
let temp = {};
temp = JSON.parse(JSON.stringify( transaction ));
temp.cardName = name;
temp.cardNum = numb;
temp.selected = false;
temp.handlingFee = that.setHandlingFee( transaction.get('localCurrencyAmount') );
trans.push( Ember.Object.create().setProperties(temp) );
})
});
this.model.set('processedTrans', trans );
console.log( this.model.get('processedTrans').get('firstObject'));
return this.model.get('processedTrans');
}),
Using the above approach I am finding that, some of data's are missing. so what is the correct way to carry some data between page?
Using service not sets for me. because I have no.of pages. I am looking to update a model with required datas.
I would recommend that you begin thinking in terms of route rather than page. Ember Routes are a hierarchy, so you would have parent routes and child routes.
As an example, consider the following in your router configuration:
this.route('post', {
path: '/post/:post_id'
}, function() {
this.route('edit');
});
In this case, there is a parent (post) and a child (post.edit).
In your Route for post, you would have:
export default Route.extend({
model(params) {
return this.store.findRecord('post', params.post_id);
}
});
For the Route for post.edit, you would have:
export default Route.extend({
model(params) {
return this.modelFor('post');
}
});
So here, the child and the parent are using the same model. If you need additional stuff in your child route, you can do that in the model hook (again, this is the child route):
export default Route.extend({
model(params) {
let post= this.modelFor('post');
return post.get('someOtherProperty');
}
});
In this case, the child's model will be the value of the parent's model's someOtherProperty attribute.
Hope this helps you think about your problem in a different way.

Ember : fetch data afterwards

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

Accessing the store from model function in routes

I want to be able to set my model dynamically depending on a record in the store - mainly because I want to control whether or not a form submission should be a POST or PUT.
export default Ember.Route.extend({
model: function() {
let bankAccount = this.store.get('bankAccount').objectAt(0);
if (bankAccount.is_connected) {
bankAccount = this.store.createRecord('bankAccount', { account_label: 'new' });
}
return Ember.RSVP.hash({
appState: this.get('appStates').currentAppState(),
user: this.store.findAll('user').thenGetFirst(),
bankAccount: bankAccount,
});
},
});
The issue is that let bankAccount = this.store.get('bankAccount').objectAt(0); return null when I refresh the page.
However, if I run App.store.get('bankAccount').objectAt(0) in my browser console, it returns the correct record.
What am I doing incorrectly?
The store has no get method do you mean to use findAll to get the accounts?
export default Ember.Route.extend({
model() {
// A better way to do this would be to `findRecord` with an id
return this.store.findAll('bank-account').then(bankAccounts => {
let bankAccount = bankAccounts.get('firstObject');
// Properties are usually camel cased this seems weird
// Can you add your model definition?
if (bankAccount.get('is_connected')) {
bankAccount = this.store.createRecord('bank-account', { account_label: 'new' });
}
});
return Ember.RSVP.hash({
appState: this.get('appStates').currentAppState(),
// A better way to do this would be to `findRecord` with an id
user: this.store.findAll('user').thenGetFirst(),
bankAccount: bankAccount,
});
},
});
On a side note your code needs some major cleanup and rethinking, especially if you are going to be dealing with people's financial data.

Ember-Cli: prepend a findAll() query with a prefix

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!

Ember: create a DS.belongsTo *outside* of a Model

I'm wanting to ideally create a DS.belongsTo / BelongsToRelationship in my own class (which is an Ember.Object, but not a DS.Model), or alternatively recreate the functionality to let me hold a reference to a record in my own class. I don't know if it's possible to use a DS.belongsTo outside of a DS.Model, or if so, how to set it up.
Background:
I have an ember-cli app using ember-data + ember-fire + firebase. One of my models has an attribute which is an object holding "type specific" information for the record. I transform this object into my own class based on the type it describes, and some times that type will have references to other records in the database. In these cases I would like to have a DS.belongsTo property set up in my typeSpecific class that I could link to in the same way as linking to a relationship in a regular model.
Alternative:
After a lot of searching and not finding any information on how to do this I made my own class which got me most of the way there. I've just noticed that although I can change the record it references on the client side and have it update, if I change it on the server-side I don't get updates coming through, so it's back to the drawing board.
If anyone is able to tell me how to make this alternative approach work that would serve the purpose too. The idea with this class is that I pass it a model name and id, and it should create the model reference and then keep model and id in sync if either side changes, and also pass through updates if anything on the model it's connected to gets changed just like a regular relationship would.
export default Ember.Object.extend({
id: null,
table: undefined,
model: undefined,
store: undefined,
init: function() {
this._super();
if(this.id && !this.model) {
this.updateModel();
}
else if(this.model && !this.id) {
this.updateId();
}
},
updateModel: function() {
var self = this;
if( this.get('id') ) {
this.store.find(this.get('table'), this.get('id')).then( function(model) {
self.set('model', model);
});
}
else {
self.set('model', undefined);
}
}.observes('id','table'),
updateId: function() {
if(this.get('model')) {
this.set('id', this.get('model.id'));
}
else {
this.set('id', null);
}
}.observes('model'),
});
Edit: code to manipulate the object above:
//Creating a reference:
this.set('target', ModelPointer.create({store:this.get('store'), table:this.get('targetTable'), id:targetId}));
//or:
this.set('target', ModelPointer.create({store:store, table:'myTable'}));
...
this.set('target.id', '42');
I believe that at the moment if I change either the id or model on the client the other will update automatically, eg:
//either:
this.set('target.id', '43');
//or:
this.store.find('myTable','43').then( function(newModel) {
self.set('target.model', newModel);
});
The problem is that if I log into Firebase and change myTable['42'].name='Fred' then the value showing on my web page which is linked to target.model.name doesn't update to 'Fred'. I suspect that if I set target.model.name to 'Fred' on the client side and save it wouldn't update the value on the server either(?)
The cleanest solution I've come up is to not store the id separately (left to the model itself). I've verified that changes I make in Firebase propagate to the displayed entry just fine.
With this solution setting the referenced model can be done with either its id or simply the model instance itself. See the controller code for examples.
Firstly, for reference, some test data for Firebase:
{
"testModels": {
"1": {
"name": "Model one"
},
"2": {
"name": "The second model"
},
"3": {
"name": "Third is the charm"
}
}
}
Thus its model app/models/test-model.js just needs the name in there.
Here is my belongsTo-like proxy class, I put mine under app/utils/proxy-class.js but it should probably be a Mixin:
import Ember from 'ember';
export default Ember.Object.extend({
remote: null, // reference to the remote DS.Model
store: null, // reference to the actual store
storeModel: null, // name of the model in the store
_watchRemote: function() {
var self = this;
if ( typeof self.get('remote') === 'object' ) {
// do nothing, already an object
if ( ! Ember.isPresent( self.get('store') ) ) {
// but set the store from the model
self.set( 'store', self.get('remote.store') );
}
} else if ( typeof self.get('remote') === 'string' ||
typeof self.get('remote') === 'number' ) {
// it's an id, so fetch the model
self._fetchModel( self.get('remote') );
}
}.observes('remote').on('init'), // on change, and during object init
_fetchModel: function( id ) {
var self = this;
self.store.find( self.get('storeModel'), id ).then(
function( model ) {
self.set( 'remote', model );
}, function ( err ) {
console.error( "couldn't read from the store:", err );
});
},
});
I created this controller, and used the browser console to change the model on the fly to test that model changes are picked up:
import Ember from 'ember';
import proxyClass from '../utils/proxy-class';
export default Ember.Controller.extend({
model: {
remoteFromId: null,
remoteFromModel: null,
},
init: function() {
var self = this;
self.set( 'model.remoteFromId',
proxyClass.create({
remote: 1,
store: self.get('store'),
storeModel: 'test-model',
})
);
self.get('store').find( 'test-model', 2 )
.then( function( model ) {
self.set( 'model.remoteFromModel',
proxyClass.create({
remote: model,
storeModel: 'test-model',
// no store provided here: set from the model
})
);
});
}
});
And the template for the controller:
<p>remoteFromId: {{model.remoteFromId.remote.id}}
{{model.remoteFromId.remote.name}}</p>
<p>remoteFromModel: {{model.remoteFromModel.remote.id}}
{{model.remoteFromModel.remote.name}}</p>