I would like to observe all changes on an object properties.
In the following example, i would like to be notified by the personChanged observer if firstname or lastname is changed.
BUT I would like to have something generic applied on all object properties (using Ember.keys() ??)
How to replace 'firstname', 'lastname' with something more generic ?
In my example:
personChanged is called when firstname or lastname is changed:
App.MyObject: Ember.Object.create({
firstname: 'first',
lastname: 'last',
personChanged: Ember.observer(function() {
console.log("person changed");
}, 'firstname', 'lastname')
})
This is possible using an ObjectProxy in two flavours, depending on your requirements. Both approaches differ only in when and how many times is the observer called and both of them rely on Ember.keys.
The HTML for both the solutions is the same.
HTML
<script type="text/x-handlebars" data-template-name="app">
Name: {{App.MyObject.firstname}} {{App.MyObject.lastname}}
<ul>
{{#each App.List}}
<li>{{this}}</li>
{{/each}}
</ul>
</script>
Solution 1
JsFiddle: http://jsfiddle.net/2zxSq/
Javascript
App = Em.Application.create();
App.List = [];
App.MyObject = Em.ObjectProxy.create({
// Your Original object, must be defined before 'init' is called, however.
content: Em.Object.create({
firstname: 'first',
lastname: 'last'
}),
// These following two functions can be abstracted out to a Mixin
init: function () {
var self = this;
Em.keys(this.get('content')).forEach(function (k) {
Em.addObserver(self.get('content'), k, self, 'personChanged')
});
},
// Manually removing the observers is necessary.
willDestroy: function () {
var self = this;
Em.keys(this.get('content')).forEach(function (k) {
Em.removeObserver(self.get('content'), k, self, 'personChanged');
});
},
// The counter is for illustrative purpose only
counter: 0,
// This is the function which is called.
personChanged: function () {
// This function MUST be idempotent.
this.incrementProperty('counter');
App.List.pushObject(this.get('counter'));
console.log('person changed');
}
});
App.ApplicationView = Em.View.extend({
templateName: 'app'
});
// Test driving the implementation.
App.MyObject.set('firstname', 'second');
App.MyObject.set('lastname', 'last-but-one');
App.MyObject.setProperties({
'firstname': 'third',
'lastname' : 'last-but-two'
});
While initialising MyObject, all properties which already exist on the content object are observed, and the function personChanged is called each time any of the property changes. However, since the observers are fired eagerly [1], the function personChanged should be idempotent, which the function in example is not. The next solution fixes this by making the observer lazy.
Solution 2
JsFiddle: http://jsfiddle.net/2zxSq/1/
Javascript
App.MyObject = Em.ObjectProxy.create({
content: Em.Object.create({
firstname: 'first',
lastname: 'last'
}),
init: function () {
var self = this;
Em.keys(this.get('content')).forEach(function (k) {
Em.addObserver(self, k, self, 'personChanged')
});
},
willDestroy: function () {
var self = this;
Em.keys(this.get('content')).forEach(function (k) {
Em.removeObserver(self, k, self, 'personChanged');
});
},
// Changes from here
counter: 0,
_personChanged: function () {
this.incrementProperty('counter');
App.List.pushObject(this.get('counter'));
console.log('person changed');
},
// The Actual function is called via Em.run.once
personChanged: function () {
Em.run.once(this, '_personChanged');
}
});
The only change here is that the actual observer function is now called only at the end of the Ember Run loop, which might be the behaviour you are looking for.
Other notes
These solutions use ObjectProxy instead of defining the observers on the object itself to avoid setting spurious observers (on properties such as init, willDestroy, etc.) or an explicit list of properties to observe.
This solution can be extended to start observing dynamic properties by overriding the setUnknownProperty on the proxy to add an observer every time a key is added to content. The willDestroy will remain the same.
Reference
[1] This might get changed soon thanks to Asyn Observers
What you have there is right using inline observers, but you just have some syntax errors. However it is even simpler to do with the key word observes. So I have tweaked your example a little below.
App.MyObject: Ember.Object.create({
firstname: 'first',
lastname: 'last',
personChanged: function() {
//firstname or lastname changed
}.observes('firstname','lastname')
});
Note: I put quotes around the properties
Source: http://emberjs.com/guides/object-model/observers/
Related
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);
})
})
});
I'm having trouble saving data in this model relationship. My models are as follows:
App.Flow = DS.Model.extend({
title: DS.attr('string'),
content: DS.attr('string'),
isCustom: DS.attr('boolean'),
params: DS.hasMany('parameter', {async: true})
});
App.Parameter = DS.Model.extend({
flow: DS.belongsTo('flow'),
param: DS.attr('string'),
param_tooltip: DS.attr('string'),
param_value: DS.attr('string')
});
As you can see, I want Flows to have multiple Parameters. I have a rudimentary setup using Flow and Parameter fixtures, which behave as expected in the templates. However, when I try to create new ones in the controller, I have trouble setting the flow and parameter values correctly.
var p = this.store.createRecord('parameter', {
param: "foo",
param_tooltip: "world",
param_value: "hello"
});
var f = this.store.createRecord('flow', {
title: 'job',
content: title,
isCustom: true,
params: [p] // doesn't seem to work
});
f.set('params', [p]); // doesn't seem to work
p.set('flow', f); // also doesn't seem to work
// Save the new model
p.save();
f.save();
I've tried a lot of solutions after staring at this and StackOverflow for a while (not just the ones listed). I'm not really sure what to try next. One thing that I noticed in the Ember inspector was that the ids of these created elements were not integers (they were something like the string 'fixture_0'), but I'm not really sure why that would be, whether its related, or how to fix it.
Thanks!
Edit
After dealing with promises ala kingpin's solution, the parameter seems like it is being set in the createFlow function I have when I check with print statements; however, I'm still having trouble accessing the parameters in both my template and in the removeFlow function.
createJob: function() {
// ...
f.get('params').then(function(params) {
params.pushObject(p);
});
f.get('params').then(function(params) {
console.log(params.toArray()[0].get('param')); // prints 'foo'
});
// ...
},
removeJob: function(flow) {
console.log(flow.get('title')); // prints 'job'
flow.get('params').then(function(params) {
var arr = params.toArray();
console.log(arr); // prints '[]'
for (var i=0; i < arr.length; i++) {
console.log('hi'); // never gets printed
arr[i].deleteRecord();
arr[i].save();
}
flow.deleteRecord();
flow.save();
});
},
params being async needs to be waited on before you can set a property or model on it (this is something that is being hardened out before 1.0 finally hits, but it's finicky right now in 1.0 beta). The fixture_ids are applied if you're using the fixture adapter.
var store = this.store;
var p = store.createRecord('parameter', {
param: "foo",
param_tooltip: "world",
param_value: "hello"
});
var f = this.store.createRecord('flow', {
title: 'job',
content: 'title',
isCustom: true,
});
f.get('params').then(function(params){
params.pushObject(p);
});
p.set('flow', f);
http://emberjs.jsbin.com/OxIDiVU/588/edit
Additionally saving using the fixture adapter seems to remove hasMany relationships when their is a manyToOne connection (aka both records have a relationship to each other, one of them being a hasMany). Getting rid of one of the relationships fixes the issue (or using one of the real adapters)
http://emberjs.jsbin.com/OxIDiVU/590/edit
I have a controller handling a list of models. These models are of two different types (e.g. Message and Comment). In order to use an ArrayController I would have to merge both lists into one. Is there a way to do this ?
Class-based polymorphism, as proposed in this thread, would solve my problem, but they are not likely to be implemented soon.
In my current solution, I use an ObjectController reveiving both comments and messages. I then merge them using a computed property:
App.SomeRoute = Ember.Route.extend({
model: function (params) {
return Em.Object.create({
comments: this.store.find('comment'),
messages: this.store.find('message'),
});
},
});
App.SomeIndexController = Ember.ObjectController.extend({
merged: Em.computed.union('messages', 'comments'),
});
It works, but I don't benefit from all the niceties of an ArrayController (like sortProperties for example).
What I would like to do is something like:
App.SomeRoute = Ember.Route.extend({
model: function (params) {
var comments = this.store.find('comment');
var messages = this.store.find('message');
return merge(comments, messages);
},
});
where merge returns something similar to what is returned by this.store.find('model').
I asked a similar question recently, here is how I solved the issue.
App.SomeIndexController = Ember.ObjectController.extend({
sortProperties: ['some field'],
sortAscending: false, // false for descending
merged: function() {
var comments = this.get('comment') || [], // This gets wherever you've stored the comments array
messages = this.get('message') || [];// This gets wherever you've stored the messages array
var stream = Ember.A();
stream.pushObjects(comments.toArray());
stream.pushObjects(messages.toArray());
return Em.ArrayProxy.createWithMixins(Ember.SortableMixin, {
content: stream,
sortProperties: this.sortProperties,
sortAscending: this.sortAscending
});
}.property('messages.#each', 'comments.#each')
});
Hope this works for you as well. Just an FYI, for my example, my controller is actually one that is rendered, so I do not set up the model for it in the route. I simply have properties on my controller, lets say, commments and messages that constantly updated themselves as RecordArrays.
So for your example you may need to observe .property('model.messages.#each', 'model.comments.#each')
Inspired by #bmeyers' answer, and after exploring ember-data's source a little bit, I came up with a solution that is reusable and not too terrible. It is probably not optimal, but it does the work.
App.Store = DS.Store.extend({
findMultiple: function (types) {
var self = this;
var recordsByType = types.map(function (type) {
return self.find(type);
});
return self.mergeArrayPromises(recordsByType);
},
mergeArrayPromises: function (promises) {
var promise = Ember.RSVP.all(promises).then(function(arrays) {
var mergedArray = Ember.A();
arrays.forEach(function (records) {
mergedArray.pushObjects(records.toArray());
});
return mergedArray;
});
return DS.PromiseArray.create({
promise: promise,
});
},
});
App.SomeRoute = Ember.Route.extend({
model: function (params) {
return this.store.findMultiple(['comment', 'message']);
},
});
This might help. I stumbled upon this a while back and your question reminded me
https://gist.github.com/sebastianseilund/6096696
I've created a typeahead view and i'm trying to send an action to the current controller to set a property. Here is my typeahead view
App.Typeahead = Ember.TextField.extend({
dataset_name: undefined, //The string used to identify the dataset. Used by typeahead.js to cache intelligently.
dataset_limit: 5, //The max number of suggestions from the dataset to display for a given query. Defaults to 5.
dataset_template: undefined, //The template used to render suggestions. Can be a string or a precompiled template. If not provided, suggestions will render as their value contained in a <p> element (i.e. <p>value</p>).
dataset_engine: undefined, //The template engine used to compile/render template if it is a string. Any engine can use used as long as it adheres to the expected API. Required if template is a string.
dataset_local: undefined, //An array of datums.
dataset_prefetch: undefined, //Can be a URL to a JSON file containing an array of datums or, if more configurability is needed, a prefetch options object.
dataset_remote: undefined, //Can be a URL to fetch suggestions from when the data provided by local and prefetch is insufficient or, if more configurability is needed, a remote options object.
ctrl_action: undefined,
didInsertElement: function () {
this._super();
var self = this;
Ember.run.schedule('actions', this, function () {
self.$().typeahead({
name: self.get('dataset_name'),
limit: self.get('dataset_limit'),
template: self.get('dataset_template'),
engine: self.get('dataset_engine'),
local: self.get('dataset_local'),
prefetch: self.get('dataset_prefetch'),
remote: self.get('dataset_remote')
}).on('typeahead:selected', function (ev, datum) {
self.selected(datum);
});
});
},
willDestroyElement: function () {
this._super();
this.$().typeahead('destroy');
},
selected: function(datum) {
this.get('controller').send(this.get('ctrl_action'), datum);
}
});
Here's an implementation
App.CompanyTA = App.Typeahead.extend({
dataset_limit: 10,
dataset_engine: Hogan,
dataset_template: '<p><strong>{{value}}</strong> - {{year}}</p>',
dataset_prefetch: '../js/stubs/post_1960.json',
ctrl_action: 'setCompanyDatum',
selected: function (datum) {
this._super(datum);
this.set('value', datum.value);
}
});
and here's my controller
App.PeopleNewController = Ember.ObjectController.extend({
//content: Ember.Object.create(),
firstName: '',
lastName: '',
city: '',
state: '',
ta_datum: undefined,
actions: {
doneEditing: function () {
var firstName = this.get('firstName');
if (!firstName.trim()) { return; }
var lastName = this.get('lastName');
if (!lastName.trim()) { return; }
var city = this.get('city');
if (!city.trim()) { return; }
var state = this.get('state');
if (!state.trim()) { return; }
var test = this.get('ta_datum');
// Create the new person model
var person = this.store.createRecord('person', {
firstName: firstName,
lastName: lastName,
city: city,
state: state
});
// Clear the fields
this.set('firstName', '');
this.set('lastName', '');
this.set('city', '');
this.set('state', '');
// Save the new model
person.save();
},
setCompanyDatum: function(datum) {
this.set('ta_datum', datum);
}
}
});
I'm expecting the setCompanyDatum controller action to be called, but it's not. Everything else is working as expected. The App.Typeahead.selected method is being called with the right action name, but it doesn't actually call the action method.
the controller inside your App.Typeahead points to the instance of the App.Typeahead, not the controller from the route where you are creating the view.
You should just be using sendAction
http://emberjs.jsbin.com/EduDitE/2/edit
{{view App.Typeahead}}
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
},
actions:{
externalAction:function(item){
console.log('helllllo' + item);
}
}
});
App.Typeahead = Ember.TextField.extend({
internalAction: 'externalAction',
didInsertElement: function () {
this.sendAction('internalAction', " I'm a sent action");
this._super();
}
});
My controller has a computed property:
App.IndexController = Ember.ArrayController.extend({
grandTotal: function () {
return this.getEach('total').reduce(function(accum, item) {
return accum + item;
}, 0);
}.property('#each.total'),
});
but I'm having trouble accessing it with my view. Here's my view:
App.SummaryView = Ember.View.extend({
templateName: 'summary',
companiesChanged: function() {
Ember.run.once(this, 'logCompanies');
}.observes('controller.#each'),
logCompanies: function() {
console.log(this.get('controller').get('model').get('length'));
console.log(this.get('controller').get('grandTotal'));
}
});
.get('length') returns correctly, so I know when this is called the models are loaded. But grandTotal is coming back as NaN, even though I know it's coded correctly since it's being rendered in the template. I need to access it within my view for additional reasons.
Any ideas?
Even though the controller's computed property changes with #each.total, the view only cares about the controller's property. Thus, the view was wrongly observing #each model, when it should have just been observing controller.grandTotal:
App.SummaryView = Ember.View.extend({
templateName: 'summary',
companiesChanged: function() {
Ember.run.once(this, 'logCompanies');
}.observes('controller.grandTotal'),
logCompanies: function() {
console.log(this.get('controller').get('model').get('length'));
console.log(this.get('controller').get('grandTotal'));
}
});