Returning a Promise with computed properties - ember.js

I need to get hold of a value from another ember-data model in a computed property. All my belongsTo and hasMany relations are async : true.I have access to the value, however, it is displaying as '[Object, Object]' when I know the result actually resolves to the correct value from the debugger. I think it is due to my lack of understanding about promises.
displayName : function() {
return this.get('name') + ' ('+ this.get('currentGroup') + ')'; // currentGroup is displaying as '[Object, Object]' as it is pointing to the Promise I think
}.property('name'),
currentGroup : function() {
var promise = this.get('groups').then(function(groups) { //groups is async : true
//Do a bunch of complicated logic to work out a group based
//on the effective start date
return currentGroup.get('name'); // this resolves correctly in the debugger
});
return promise;
}.property()

currentGroup will return a promise a you wrote, so you should rather do it that way:
this.get('currentGroup').then(function(name) {
// note you have the name you returned in the promise's callback
})
so instead of having displayName property, you could have an observer:
displayName: null,
displayNameUpdate : function() {
this.get('currentGroup').then(function(name) {
var displayedName = this.get('name') + ' ('+ name + ')';
this.set('displayName', displayedName);
}.bind(this));
}.observes('name', 'currentGroup'),

Related

How to test computed property that returns PromiseArray in Ember

I have a computed property that asks server for user data and then the other one that computes number of users. To propagate changes into the template, I'm using DS.PromiseArray wrapper. With this wrapper, I can't find an easy way to test this property.
// model
users: computed('name', function () {
let name = get(this, 'name');
return DS.PromiseArray.create({
promise: this.store.query('user', { name })
});
}),
numberOfUsers: computed('users', function () {
return get(this, 'users.length') || 0;
}),
// ....
test('numberOfUsers returns correct number', function (assert) {
let model = this.subject({
store: EmberObject.create({
query() {
return Promise.resolve([
{ name: 'Thomas' },
{ name: 'Thomas' },
{ name: 'Thomas' },
]);
}
}),
name: 'Thomas',
});
assert.equal(model.get('numberOfUsers'), 3);
});
This test fails with 0 !== 3. Any ideas?
Since model.get('users') is a promise, model.get('numberOfUsers') will not be 3 until the Promise resolves. In your test, you're immediately calling model.get('numberOfUsers'), and that is using the initial value of model.get('users'), which is an unresolved promise.
To get it to work, you could call users and put your assert inside the then of that returned promise:
model.get('users').then((user) => {
assert.equal(model.get('numberOfUsers'), 3);
})
A couple of side notes:
It is conventional in Ember to do your data fetching in the Route's model hook, not in a component like this.
Also, there's no need to manually create a PromiseArray in your application code, because an Ember Data query returns a type of Promise Array already. So you can just return this.store.query('user', { name }); (If you do this, you'll have to change your test query stub to generate a PromiseArray).

Using the result of a promise in Ember.RSVP.hash

I've been pulling out my hair with this for a few hours now so I thought I'd just ask :)
In the model hook of my route, I'm grabbing the account ID from the session store. I'm also returning an Ember hash of layouts using a (presently) hard-coded ID:
model: function() {
var accountId = this.get('session.currentUser').then(function(user) {
return user;
}).then(function(user) {
return user.get('account');
}).then(function(account) {
var accountId = parseInt(account.get('id'));
console.log(accountId); // outputs 2
return accountId;
});
return Ember.RSVP.hash({
layouts: this.store.query('layout', { account_id: 2 })
});
},
/* {{log layouts}} in the template returns the correct list of layouts */
However, when I try and use the value of the first promise in the hash, as follows:
return Ember.RSVP.hash({
layouts: this.store.query('layout', { account_id: accountId })
});
I get the following error:
You must pass a resolver function as the first argument to the promise constructor
TypeError: You must pass a resolver function as the first argument to the promise constructor
I can almost understand this, as perhaps the accountID promise isn't resolved before the hash function is called.
But then I tried:
var _this = this;
var accountId = this.get('session.currentUser').then(function(user) {
return user;
}).then(function(user) {
return user.get('account');
}).then(function(account) {
var accountId = parseInt(account.get('id'));
console.log(accountId); // outputs 2
return accountId;
}).then(function(accountId) {
console.log(accountId); // outputs 2
return Ember.RSVP.hash({
layouts: _this.store.query('layout', { account_id: accountId })
});
});
This does not give any errors, but {{log layouts}} in the template returns 'undefined'.
Can anyone help, please?
Instead of returning the hash at the end, structure your promise the other way around:
var _this = this;
return Ember.RSVP.hash({
layouts: this.get('session.currentUser').then(function(user) {
return user;
}).then(function(user) {
return user.get('account');
}).then(function(account) {
return parseInt(account.get('id'), 10);
}).then(function(accountId) {
return _this.store.query('layout', { account_id: accountId });
})
});

Emberjs access a nested property

This question is the following of this one emberjs display an object return from an ajax call
To resume a bit, I have a dynamic list generated with some button for each item of that list. I catch the event of any button with this class :
App.EnquiriesView = Ember.View.extend({
didInsertElement: function() {
var that = this;
this.$().on('click', '.view-btn', function(){
var id = $(this).attr('data-value');
that.get('controller').send('clickBtn', id);
});
}
});
And it goes to my controller here :
App.EnquiriesController = Ember.ObjectController.extend({
actions: {
clickBtn: function( id ) {
console.log('DEBUG: ClickBtn OK id = ' + id);
//console.log(App.Enquiries.findOne(id));
this.transitionToRoute('enquiry', /*App.Enquiries.findOne(id)*/id);
}
}
});
The router related :
App.EnquiryRoute = Ember.Route.extend({
model: function( param ) {
console.log('Enquiry id = ' + param.enquiry_id);
return App.Enquiries.findOne(param.enquiry_id);
}
});
and my map :
App.Router.map(function() {
this.resource('login', { path: '/' });
this.resource('home');
this.resource('enquiries', function (){
this.route('create');
});
this.resource('enquiry', { path: 'enquiry/:enquiry_id' }, function(){
this.route('update');
});
});
So far for now when the user click on the button its redirect correctly to the enquiry with the good URL (e.g : /#/enquiry/1)
But the problem is coming from my update class now. I've just create a button with the action helper to display the update form :
App.EnquiryController = Ember.ObjectController.extend({
actions: {
update: function() {
console.log('DEBUG: in EnquiryController update');
console.log(this.get('model'));
this.transitionToRoute('enquiry.update');
}
}
});
So when you click on the update button you are redirected to this kind of URL : /#/enquiry/undefined/update instead of /#/enquiry/1/update ...
I don't know how this can happen and how I can loose my id during the process...
Thanks for your help.
[edit] If you need to know what is my findOne function :
findOne: function(id) {
return $.ajax({
url: host + 'mdf/enquiry/' + id,
type: 'GET',
accepts: 'application/json',
success: function (data) {
console.log('DEBUG: GET Enquiry ' + id + ' OK');
},
error: function() {
console.log('DEBUG: GET Enquiry ' + id + ' Failed');
}
});
}
Its fetching the data from the server for every item after you've click on the related button in the list.
Here is the object I've got back :
Object {readyState: 1, getResponseHeader: function, getAllResponseHeaders: function, setRequestHeader: function, overrideMimeType: function…}
abort: function ( statusText ) {
always: function () {
complete: function () {
done: function () {
error: function () {
fail: function () {
getAllResponseHeaders: function () {
getResponseHeader: function ( key ) {
overrideMimeType: function ( type ) {
pipe: function ( /* fnDone, fnFail, fnProgress */ ) {
progress: function () {
promise: function ( obj ) {
readyState: 4
responseJSON: Object
responseText: "{"ok":true,"enquiry":{"id":1,"domainid":"domain","userid":"userid","status":null,"type":"new","customerName":"Marco","customerEmail":"Marco#email.com",...}"
setRequestHeader: function ( name, value ) {
state: function () {
status: 200
statusCode: function ( map ) {
statusText: "OK"
success: function () {
then: function ( /* fnDone, fnFail, fnProgress */ ) {
__proto__: Object
There's a number of issues going on here. A couple of things you're doing are very "non-ember" idiomatic, and I wouldn't be surprised if further issues pop up as a result. But I'll focus on the question asked, and if you want more advice on adjusting further segments I'm happy to provide it.
In short, you have the following code:
this.resource('enquiry', { path: 'enquiry/:enquiry_id' });
in your map, but an enquiry object that looks something like:
{"ok":true,
"enquiry":{
"id":1,
"domainid":"motorpark",
"userid":"motorpark/mpuser"‌​
...
}
}
And these don't match. Your map defines that it is serialized by a field enquiry_id *which does not exist on your model. To fix this you can do one of these solutions:
Solution 1: Adjust your model
If you want to keep your map as is, you'll need to adjust your model to have an enquiry_id field, such as:
{"ok":true,
"enquiry_id":1,
"enquiry":{
"id":1,
"domainid":"motorpark",
"userid":"motorpark/mpuser"‌​
...
}
}
Solution 2: Adjust your Map (recommended)
It's easier to just change your map though. To do this, replace your enquiry resource on the map with:
this.resource('enquiry', {path: 'enquiry/:enquiry.id'});
The . tells ember that the desired element is the id field within the enquiry object.
You'll also need to modify how you access the param value. Because you're naming an element of the param as enquiry.id you need to specify this as a variable name and not a path when retrieving the value. On your route, change all instances of:
param.enquiry_id
To:
param['enquiry.id']

How to make a computed property that depends on a global class attribute?

I wanna create a property that depends on a global attribute:
App.Test= Em.Object.extend();
App.Test.reopenClass({ all: Em.A() });
App.Other = Em.object.extend({
stuff: function() {
return "calculated stuff from this.get('foo') and App.Test.all";
}.property('foo', 'App.Test.all.#each.bar')
});
As a workarround I could create a observer and always set a dummy property with a new random value to trigger the property change, but is there a better way to do this?
I need this for some caching. I've a really crazy, and single threaded backend. So I write my own Model classes. So I try to reimplement a bit of the logic in the client for a better caching.
Ive an Item class (App.Item) and another class where each instance has a calculated reduced list of Items.
App.Model = Em.Object.extend({
});
App.Model.reopenClass({
all: Em.A(),
load: function(hash) {
return this.get('all').pushObject(this.create(hash));
}
});
App.Item = App.Model.extend({
});
App.List = App.Model.extend({
loadedInitItems: false,
items: function() {
if(!this.get('loadedInitItems')) { this.set('loadedInitItems', true); Backend.call('thelist', function(item) { App.Item.load(this); }); }
return App.Item.all.filter(function(item) {
// heavy filter stuff, depends on a lot of propertys on the current list instance
});
}.property('someprops', 'App.Item.all.#each.foo')
});
Backend.call represents some AJAX stuff
the point is, that now any item could change so that the filter will return something diffrent. And there are other places om the application, where the user can add Items. I dont want to call the backend again, because its very slow! And I know that the backend will not modify the list! So I wanna cache it.
This is just a reduced example of my use case, but I think've described the point. In reallity I have this dozend of times, with over 25000 objects.
have you tried adding 'Binding' to your property and then the value you want to bind to ?, something like this:
App.PostsController = Em.ArrayController.extend({
nameOfYourVariableBinding: "App.SomeObject.propertyYouWantToBindTo"
})
It looks like the problem is the double uppercase letter. So App.test ist working, but not App.Foo.test.
But I was able to find a Solution with the ArrayProxy.
Its about this:
App.Model = Em.Object.extend({
});
App.Model.reopenClass({
all: Em.A(),
load: function(hash) {
return this.get('all').pushObject(this.create(hash));
}
});
App.Item = App.Model.extend({
});
App.List = App.Model.extend({
loadedInitItems: false,
items: function() {
var self = this;
if(!this.get('loadedInitItems')) {
this.set('loadedInitItems', true);
Backend.call('thelist', function(item) {
App.Item.load(this);
});
}
return Em.ArrayProxy.extend({
content: App.Item.all,
arrangedContent: function() {
return this.get('content').filter(function(item) {
// heavy filter stuff, depends on a lot of propertys on the current list instance
// use self.get('someprops')
})
}.property('content.#each.foo')
});
}.property('someprops')
items: function() {
if(!this.get('loadedInitItems')) { this.set('loadedInitItems', true); Backend.call('thelist', function(item) { App.Item.load(this); }); }
return App.Item.all.filter(function(item) {
// heavy filter stuff, depends on a lot of propertys on the current list instance
});
}.property('someprops', 'App.Item.all.#each.foo')
});

temporary suspension of ember observers

With Ember.js, I have the following scenario:
My Class has a property that require some manipulation at instanciation.
To achieve that, I'm calling a function that does the work from init. I also want to observe this propery change, so that if consumer will set new value at run time I will run my manipulation logic over the new value.
The problem is, that as part of the init flow, I'm setting myself the new value to the property after manipulation, and this invokes the trigger (as expected). I do not want this code to run twice.
Consider the following code. 'here' will be printed twice to the console.
var MyObj = Ember.Object.extend({
prop: null,
init: function init() {
this._super.apply(this, arguments);
this._applyProp();
},
_applyProp: function prop() {
console.log('here');
var prop = this.get('prop');
if (prop === 'Dan') {
prop = 'Hi' + prop;
}
this.set('prop', prop);
}.observes('prop')
});
MyObj.create({prop: 'Dan'});
Any advice will be really appreciated.
Having an observer that sets the property it is observing seems like a bad idea. Your particular example could be achieved using a computed property getter/setter:
var MyObj = Ember.Object.extend({
prop: function(key, value) {
if (value !== undefined) {
if (value == 'Dan') {
return 'Hi ' + value;
}
return value;
}
}.property(),
});
MyObj.create({prop: 'Dan'});
Would this be sufficient to cover your use cases?