How to observe store collection - ember.js

I have code for generate checkboxes list:
accountsCheckboxes: Ember.computed('accountsCheckboxes.#each', function(){
return this.model.accounts.map(row => {
return {
label: row.get('name'),
value: row.get('id')
};
})
}),
but after modify accounts collection, add or remove, this computed property doesnt refresh. I tried find how to do it with events, or how to observe store collection, but without success.
I modyfy this model collection in others controllers.

Its a little confusing what you're trying to do by observing the same property you're defining:
// accountsCheckboxes observes accountsCheckboxes?
accountsCheckboxes: Ember.computed('accountsCheckboxes.#each', ...)
This won't work and will probably result in an infinite chain of lookups.
Did you mean to observe model.accounts instead? If so, this is what you could've done:
accountsCheckboxes: Ember.computed('model.accounts.#each.name', function() {
return this.get('model.accounts').map(row => {
return {
label: row.get('name'),
value: row.get('id')
};
})
});
Note that you must call this.get('model'), not this.model to make sure you always get the proper data.
Alternatively, you might use Ember.computed.map:
accountsCheckboxes: Ember.computed.map('model.accounts.#each.name', function(row) {
return {
label: row.get('name'),
value: row.get('id')
};
});

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).

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

EmberJS, DS.Model, how to know if a particular attribute is dirty?

I can monitor the property hasDirtyAttributes to know if any attribute is dirty.
How can I monitor if a specific attribute is dirty?
Something like:
attributeOneNeedSave: Ember.computed('attributeOne', function() {
return this.get('dirtyAttributes.attributeOne');
})
You can use the changedAttributes method to discover if an attribute has changed. To turn it into a computed property, just call it when that property changes.
isNameDirty: Ember.computed('name', function() {
const changedAttributes = this.changedAttributes();
return !!changedAttributes.name;
})
Also, I'm not 100% sure if Ember Data will remove the property from changedAttributes if it changes back to it's original value. So it might be possible to get something like this:
const changedAttributes = {
name: ['Bob', 'Bob']
};
If that's the case, check for equality as well.
isNameDirty: Ember.computed('name', function() {
const changedAttributes = this.changedAttributes();
if (!changedAttributes.name) {
return false;
}
return (changedAttributes.name[0] !== changedAttributes.name[1]);
})

Why do I need to manually pushObject after a successful hasMany create?

I have a hasMany / belongsTo relationship
App.Appointment = DS.Model.extend({
employee: belongsTo('employee', { async: true})
});
App.Employee = DS.Model.extend({
appointments: hasMany('appointment', { async: true})
});
I have a simple form that lets me create the appointment
var appointment = {
employee: employee (another ember-data model)
}
this.store.createRecord('appointment', appointment).save().then(function(apt) {
self.get('target').transitionTo('day.index');
}
If I do the above my "employees" array never shows the inverse correctly (ie- it doesn't reflect the new appointment when I do something later like employee.get('appointments');
I've been able to "work around" this issue w/ something like the below
this.store.createRecord('appointment', appointment).save().then(function(apt) {
employee.get('appointments').pushObject(apt);
employee.save();
self.get('target').transitionTo('day.index');
}
I don't like this for 2 reasons
I feel like if I have ember-data wired up correctly it should just
"know" that I've added a new appointment w/ the related employee (as
I see that going across the wire)
this forces a "lookup" on my hasMany (so it then fires off a request
asking for that employees apts -often messing up the scope of "how
many" apts I want to show for a given context).
Is my relationship setup correctly here? Or is this a bug in ember-data 1.0 beta 4/5 ?
I'm currently using ember-data 1.0 beta 4 with ember.js 1.3.1
For what it's worth, here is what I'm currently using to do a recursive save on my 'items'. They have children, and they have permissions associated to them. Also worth noting is that these items are recursive (so, the items can have children, which are items that can have children... and so on and so forth). This will handle the case where some of the items are saved or not including all parent re-association and what not. It works for me. This might help you (or it might just confused you completely, I hope not though.)
If you can get something useful out of it, that's great :)
Also worth noting is that I don't do anything with my error catches. This isn't ideal, obviously!
saveAll: function() {
var saveExistingObjects, self;
saveExistingObjects = function(item) {
var promise;
promise = new Ember.RSVP.Promise(function(resolve, reject) {
return item.get('childItems').then(function(childItems) {
var childPromises;
childPromises = childItems.map(function(childItem) {
return saveExistingObjects(childItem);
});
return Ember.RSVP.all(childPromises).then(function(arrayOfSavedChildren) {
var itemPermissions, itemWasNew;
itemWasNew = item.get('isNew');
itemPermissions = item.get('itemPermissions');
return item.save().then(function(savedItem) {
if (itemWasNew) {
arrayOfSavedChildren.forEach(function(childItem) {
childItem.set('parentItem', savedItem);
return childItem.save();
});
itemPermissions.forEach(function(itemPermission) {
itemPermission.set('item', savedItem);
return itemPermission.save();
});
}
savedItem.set('childItems', []);
Ember.RSVP.Promise.cast(savedItem.get('childItems')).then(function(cb) {
return cb.addObjects(arrayOfSavedChildren);
});
return resolve(savedItem);
})["catch"](function(error) {
console.log("Didn't save!");
return reject(error);
});
})["catch"](function(error) {
console.log("Didn't finish saveExistingObjects and returning childPromises");
console.log(error);
return reject(error);
});
})["catch"](function(error) {
console.log("Didn't get childItems");
console.log(error);
return reject(error);
});
});
return promise;
};
self = this;
return saveExistingObjects(self);
}

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