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

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

Related

Access property inside Ember component

Need your help folks. How can I access property inside the component. Something like this:
export default Ember.Component.extend({
cMsg: Ember.computed('msg', function() {
return `${this.get('msg')} , ${this.get('msg')}`;
}),
selectedDomain: { msgPrefix: 'cMsg???' },
});
Here is the twiddle: https://ember-twiddle.com/9acda203a89dbd3892059170ab665d08?openFiles=components.hello-there.js%2C
Most of the time we miss the usage of custom helper and computed property. In this case you can write computed property,
selectedDomain: Ember.computed('cMsg', function() {
return { msgPrefix: this.get('cMsg') }
})

How to observe store collection

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

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

How to initialize a Controller's computed property to model in Ember?

I have a computed property myArray defined on an Ember controller that returns an array. The array should be initialized to the contents of model and then recompute by filtering model depending on a user-input query.
myArray: function() {
// return a value that filters model with query
}.property('model', 'query')
The problem is that I can't figure out how to do both at the same time. The below does not work to initialize myArray to model; I'm guessing because model is loaded asynchronously and init() runs before it's finished.
// doesn't work
init: function() {
this._super();
this.set('myArray', this.get('model'));
}
So I thought that setupController() would be the place to set it, but I found that setting myArray there caused the filter update not to work, maybe because I was overwriting the definition.
// route definition...
setupController: function(controller, model) {
controller.set('model', model);
controller.set('myArray', model); // breaks updating
}
How can I accomplish my goal?
You want a computed property depending on model - you don't have to initialize it, it just has to return the right thing.
// untested, just to show the idea
myArray: function() {
var query = this.get('query');
var model = this.get('model');
if (query)
return doSomethingWith(model, query);
else
return model;
}.property('model', 'query')
Since it is a property, the initializing and updating will take place for itself.
That said, Twitter lore is that the usage of .property shall be discouraged, better use Ember.computed.
// untested, just to show the idea
myArray: Ember.computed('model', 'query', function() {
var query = this.get('query');
var model = this.get('model');
if (query)
return doSomethingWith(model, query);
else
return model;
})
It's just another way to write it, and may be mor future-proof in the long run.
try:
myArray: function() {
this.set('myArray', this.get('model');
}.property('model', 'query')
EDIT: I don't know why I went with the roundabout way of doing things. I guess I just wanted to illustrate that computed properties act as setters too. This will also work the same way:
myArray: function() {
return this.get('model').filter(function(item) {
return (item.property_you_want_to_filter_by === true);
});
}.property('model.#each.property_you_want_to_filter_by', 'query')
The Ember shorthand will also work:
myArray: Ember.computed.filter('model', function(item) {
return (item.property_you_want_to_filter_by === true);
});
You need to take advantage of the fact that computed properties are getters and setters.
myArray: function(key, value) {
// This is the setter
if (arguments.length > 1) {
this.set('_myArray', value);
}
// This is the getter
// Do your filtering with `query` here
return this.get('_myArray').filter(function(item) {
return (item.selected === true);
});
}.property('_myArray', 'query')
Since you only set it when you get a new model, you can just store the value in a private property on the controller (in this case _myArray). Then for the getter, you can use the value stored in that property combined with your query to return the value you want. In my example above, I've filtered out every non-selected item.

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