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

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!

Related

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

Promise result in Ember Data computed property

I'm trying to make a call to an external API and use the results as a computed property in my Ember Data model. The result is fetched fine, but the computed property returns before the Promise resolves, resulting in undefined. Is this a use case for an Observer?
export default DS.Model.extend({
lat: DS.attr(),
lng: DS.attr(),
address: Ember.computed('lat', 'lng', function() {
var url = `http://foo.com/json?param=${this.get('lat')},${this.get('lng')}`;
var addr;
var request = new Ember.RSVP.Promise(function(resolve, reject) {
Ember.$.ajax(url, {
success: function(response) {
resolve(response);
},
error: function(reason) {
reject(reason);
}
});
});
request.then(function(response) {
addr = response.results[0].formatted_address;
}, function(error) {
console.log(error);
})
return addr;
})
});
Use DS.PromiseObject. I use the following technique all the time:
import DS from 'ember-data';
export default DS.Model.extend({
...
address: Ember.computed('lat', 'lng', function() {
var request = new Ember.RSVP.Promise(function(resolve, reject) {
...
});
return DS.PromiseObject.create({ promise: request });
}),
});
Use the resolved value in your templates as {{address.content}}, which will automatically update when the proxied Promise resolves.
If you want to do more here I'd recommend checking out what other people in the community are doing: https://emberobserver.com/?query=promise
It's not too hard to build a simple Component that accepts a DS.PromiseObject and show a loading spinner while the Promise is still pending, then shows the actual value (or yields to a block) once the Promise resolves.
I have an Ember.Service in the app I work on that's composed almost entirely of Computed Properties that return Promises wrapped in DS.PromiseObjects. It works surprisingly seamlessly.
I've used the self.set('computed_property', value); technique in a large Ember application for about three months and I can tell you it have a very big problem: the computed property will only work once.
When you set the computed property value, the function that generated the result is lost, therefore when your related model properties change the computed property will not refresh.
Using promises inside computed properties in Ember is a hassle, the best technique I found is:
prop: Ember.computed('related', {
// `get` receives `key` as a parameter but I never use it.
get() {
var self = this;
// We don't want to return old values.
this.set('prop', undefined);
promise.then(function (value) {
// This will raise the `set` method.
self.set('prop', value);
});
// We're returning `prop_data`, not just `prop`.
return this.get('prop_data');
},
set(key, value) {
this.set('prop_data', value);
return value;
}
}),
Pros:
It work on templates, so you can do {{object.prop}} in a template and it will resolve properly.
It does update when the related properties change.
Cons:
When you do in Javascript object.get('prop'); and the promise is resolving, it will return you inmediately undefined, however if you're observing the computed property, the observer will fire again when the promise resolves and the final value is set.
Maybe you're wondering why I didn't returned the promise in the get; if you do that and use it in a template, it will render an object string representation ([object Object] or something like that).
I want to work in a proper computed property implementation that works well in templates, return a promise in Javascript and gets updated automatically, probably using something like DS.PromiseObject or Ember.PromiseProxyMixin, but unfortunately I didn't find time for it.
If the big con is not a problem for your use case use the "get/set" technique, if not try to implement a better method, but seriously do not just use self.set('prop', value);, it will give your a lot of problems in the long-term, it's not worth it.
PS.: The real, final solution for this problem, however, is: never use promises in computed properties if you can avoid it.
PS.: By the way, this technique isn't really mine but of my ex co-worker #reset-reboot.
Create a component (address-display.js):
import Ember from 'ember';
export default Ember.Component.extend({
init() {
var url = `http://foo.com/json?param=${this.get('lat')},${this.get('lng')}`;
Ember.$.ajax(url, {
success: function(response) {
this.set('value', response.results[0].formatted_address);
},
error: function(reason) {
console.log(reason);
}
});
}
});
Template (components/address-display.hbs):
{{value}}
Then use the component in your template:
{{address-display lat=model.lat lng=model.lng}}
The below works by resolving inside the property and setting the result.
Explained here:
http://discuss.emberjs.com/t/promises-and-computed-properties/3333/10
export default DS.Model.extend({
lat: DS.attr(),
lng: DS.attr(),
address: Ember.computed('lat', 'lng', function() {
var url = `http://foo.com/json?param=${this.get('lat')},${this.get('lng')}`;
var self = this;
var request = new Ember.RSVP.Promise(function(resolve, reject) {
Ember.$.ajax(url, {
success: function(response) {
resolve(response);
},
error: function(reason) {
reject(reason);
}
});
}).then(function(response) {
self.set('address', response.results[0].formatted_address);
})
})
});

Ember Data rewrite URL

I have the following setup:
App.Router.map(function() {
this.route('tab', { 'path' : 'tab/:which' });
});
App.ApplicationStore = DS.Store.extend({});
App.ApplicationAdapter = DS.RESTAdapter.extend({
host: '../api'
});
App.TabAdapter = DS.RESTAdapter.extend({
find: function(store, type, id) {
alert("I doesn't get invoked");
return this._super(store, type, id);
}
});
App.TabRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('emails', {tab: "inbox"});
}
});
And when visiting the route #/tab/inbox i wanna rewrite the URL for the endpoint from
http://localhost/ba/api/emails?tab=inboxinto
http://localhost/ba/api/emails/inbox. Therefore i'm overriding the find()-method on the TabAdapter, but when this.store.find('emails', {tab: "inbox"}); runs, it doesn't enter my overridden method(and my test-alert doesn't get invoked).
Why does my overridden find()-method not get invoked?
you're overriding the wrong find method. You're finding by query, not id and should be overriding that method
findQuery: function(store, type, query) {
// Do your thing here
return this.ajax(this.buildURL(type.typeKey), 'GET', { data: query });
}
And you're using a TabAdapter which would be specific to models of type tab not of type email(s). You should be creating a Email(s)Adapter. The general convention is for a model to be singular btw.
See also: How do you create a custom adapter for ember.js?

Fetching a single record with Ember data - non-conventional URL format

My server API has a route, '/user', which fetches a SINGLE user record based on the client's authentication cookie, and no ID.
How do I make this call in Ember Data?
override pathForType and buildUrl on your user adapter:
App.UserAdapter= DS.RESTAdapter.extend({
pathForType: function(type) {
var decamelized = Ember.String.decamelize(type);
//return Ember.String.pluralize(decamelized);
return decamelized;
},
buildURL: function(type, id) {
var url = [],
host = get(this, 'host'),
prefix = this.urlPrefix();
if (type) { url.push(this.pathForType(type)); }
if (id!==-1) { url.push(id); } // This will exclude the id for this request
if (prefix) { url.unshift(prefix); }
url = url.join('/');
if (!host && url) { url = '/' + url; }
return url;
},
});
then use
this.store.find('user', -1);
http://emberjs.jsbin.com/OxIDiVU/147/edit
I don't know exactly how you want to fetch the user, but either way, you'll want to create your own adapter. You'll likely want to override one or more of the find methods (find, findAll, findMany, findQuery), depending on your situation. Check to see if the type is user first. If it is, implement your custom logic. If not, just call the super method on whichever adapter you extend.
If you give me a little more information on how you want to access user models, I could probably help more.
EDIT: It seems like you might only need to override the buildURL function. You might have to return more than just /user, such as the host and prefix, but this should give you the gist. Check the source of the REST adapter to see how they do it by default.
App.ApplicationAdapter = DS.RESTAdapter.extend({
buildUrl: function(type, id) {
if (type.typeKey === 'user' && id === 1) {
return '/user';
} else {
return this._super.buildUrl.apply(this, arguments);
}
}
});

Custom Dynamic Segment (NOT ID) - EmberJS

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