Unexpected behavior with ArrayObserver on DS.hasMany Relationship - ember.js

I'm trying to use DS.hasMany.addArrayObserver to monitor when new elements are inserted (either created client side or loaded from the server).
However, the arguments passed to the arrayWillChange and arrayDidChange callbacks show inconsistent behavior between observing changes to a "plain" Ember.A vs a DS.hasMany relationship.
For the Ember.A array, the proper addCount and removeCount are passed, but with the DS.hasMany the removeCount is always equal to the old length of the array and addCount is always equal to the new length of the array.
In other words, it looks like the DS.hasMany "array" is emptied and all new records are added every time. I have two questions:
Is this the expected behavior?
If so, what is the best way to monitor just added/removed elements on a DS.hasMany relationship?
I have set up a demo of the behavior: DEMO
EDIT:
I have also opened an issue on the ember-data github: https://github.com/emberjs/data/issues/2981
EDIT 2:
This looks like it is expected behavior. Ember-data is generating a new array by filtering out deleted items and concating new items. This happens in flushCanonical: https://github.com/emberjs/data/blob/339b79aa3ea82e9be1a8f39db36ec1133d0b65a2/packages/ember-data/lib/system/many-array.js#L67

I have solved my problem, in a somewhat non-intuitive way. Instead of adding an ArrayObserver directly to the model's HasMany relationship, I am using DS.Store.filter() to get the relevant records, and using addArrayObserver to the results.
Relevant code:
var collection = this.store.filter('child', function(record){ return record.get('parent.id') === "1"; });
collection.then(function(records){
records.addArrayObserver({
arrayWillChange: function(obj, start, removeCount, addCount) {
console.log("Filter", start, removeCount, addCount);
},
arrayDidChange: Ember.K
});
});
DEMO

Related

Force a controller to always act as a proxy to a model in Ember

I'm looping through a content of an ArrayController whose content is set to a RecordArray. Each record is DS.Model, say Client
{{# each item in controller}}
{{item.balance}}
{{/each}}
balance is a property of the Client model and a call to item.balance will fetch the property from the model directly. I want to apply some formatting to balance to display in a money format. The easy way to do this is to add a computed property, balanceMoney, to the Client object and do the formatting there:
App.Client = DS.Model({
balance: DS.attr('balance'),
balanceMoney: function() {
// format the balance property
return Money.format(this.get('balance');
}.property('balance')
});
This serves well the purpose, the right place for balanceMoney computed property though, is the client controller rather than the client model. I was under the impression that Ember lookup properties in the controller first and then tries to retrieve them in the model if nothing has been found. None of this happen here though, a call to item.balanceMoney will just be ignored and will never reach the controller.
Is it possible to configure somehow a controller to act always as a proxy to the model in all circumstances.
UPDATE - Using the latest version from emberjs master repository you can configure the array controller to resolve records' methods through a controller proxy by overriding the lookupItemController method in the ArrayController. The method should return the name of the controller without the 'controller' suffix i.e. client instead of clientController. Merely setting the itemControllerClass property in the array controller doesn't seem to work for the moment.
lookupItemController: function( object ) {
return 'client';
},
This was recently added to master: https://github.com/emberjs/ember.js/commit/2a75cacc30c8d02acc83094b47ae8a6900c0975b
As of this writing it is not in any released versions. It will mostly likely be part of 1.0.0.pre.3.
If you're only after formatting, another possibility is to make a handlebars helper. You could implement your own {{formatMoney item.balance}} helper, for instance.
For something more general, I made this one to wrap an sprintf implementation (pick one of several out there):
Ember.Handlebars.registerHelper('sprintf', function (/*arbitrary number of arguments*/) {
var options = arguments[arguments.length - 1],
fmtStr = arguments[0],
params = Array.prototype.slice.call(arguments, 1, -1);
for (var i = 0; i < params.length; i++) {
params[i] = this.get(params[i]);
}
return vsprintf(fmtStr, params);
});
And then you can do {{sprintf "$%.2f" item.balance}}.
However, the solution #luke-melia gave will be far more flexible--for example letting you calculate a balance in the controller, as opposed to simply formatting a single value.
EDIT:
A caveat I should have mentioned because it's not obvious: the above solution does not create a bound handlebars helper, so changes to the underlying model value won't be reflected. There's supposed to be a registerBoundHelper already committed to Ember.js which would fix this, but that too is not released yet.

How would I modify this ember.js function to return an Enumerable or Array

I have just begun learning ember.js, I have followed some tutorials and created a working example here:
App.Track.reopenClass({
find: function() {
var tracks = [];
$.ajax({
url: 'http://ws.spotify.com/lookup/1/.jsonuri=spotify:album:6J6nlVu4JMveJz0YM9zDgL&extras=track',
dataType: 'json',
context: this,
success: function(data, textStatus, jqXHR) {
$.each(data.album.tracks, function(index, value) {
track_id = value.href.replace("spotify:track:", "");
tracks.addObject(App.Track.create(value));
// I would rather do something like:
// tracks[track_id] = App.Track.create(value)
});
}
})
return tracks;
}
});
This function hits an API and loops through the returned data to populate the tracks object (tracks.addObject(App.Track.create(value));) and return it.
Rather than getting an ordinary object back from this function, I would like to get an Enumerable / Array so I can manipulate it with filterProperty or pull out tracks by id (There is a track_id which I would like to use as the array index).
All of my attempts to use an array have broken ember's magical ability to update the view when the ajax call populates the tracks.
Can anyone modify http://jsfiddle.net/ZEzwn/ to return an Enumerable (preferably an Array) but still update the view automatically?
As your method already returns an Array (because you have Ember prototype extension enabled), doing:
var tracks = [];
is equivalent to
var tracks = Ember.A();
On ajax request success, you're just populating the array, so you could use Ember.Array methods like filterProperty.
Just one thing about using id as array key, you really SHOULD NOT, as Ryan Bigg says in its blog:
However, if the variant’s id is [something a little higher, like] 1,013,589,413, then you start to run into problems.
In that case, JavaScript would create a one billion, thirteen million, five hundred and eighty-nine thousand, four hundred and fourteen element array. All to store one value in, right at the end.
Ok this is now working, as louiscoquio pointed out, tracks IS an enumerable object and I can do stuff like
tracks.filterProperty('href', 'spotify:track:7x7F7xBqXqr0L9wqJ3tuQW')
tracks.getEach('name')
tracks.get('firstObject')

Adding item to filtered result from ember-data

I have a DS.Store which uses the DS.RESTAdapter and a ChatMessage object defined as such:
App.ChatMessage = DS.Model.extend({
contents: DS.attr('string'),
roomId: DS.attr('string')
});
Note that a chat message exists in a room (not shown for simplicity), so in my chat messages controller (which extends Ember.ArrayController) I only want to load messages for the room the user is currently in:
loadMessages: function(){
var room_id = App.getPath("current_room.id");
this.set("content", App.store.find(App.ChatMessage, {room_id: room_id});
}
This sets the content to a DS.AdapterPopulatedModelArray and my view happily displays all the returned chat messages in an {{#each}} block.
Now it comes to adding a new message, I have the following in the same controller:
postMessage: function(contents) {
var room_id = App.getPath("current_room.id");
App.store.createRecord(App.ChatMessage, {
contents: contents,
room_id: room_id
});
App.store.commit();
}
This initiates an ajax request to save the message on the server, all good so far, but it doesn't update the view. This pretty much makes sense as it's a filtered result and if I remove the room_id filter on App.store.find then it updates as expected.
Trying this.pushObject(message) with the message record returned from App.store.createRecord raises an error.
How do I manually add the item to the results? There doesn't seem to be a way as far as I can tell as both DS.AdapterPopulatedModelArray and DS.FilteredModelArray are immutable.
so couple of thoughts:
(reference: https://github.com/emberjs/data/issues/190)
how to listen for new records in the datastore
a normal Model.find()/findQuery() will return you an AdapterPopulatedModelArray, but that array will stand on its own... it wont know that anything new has been loaded into the database
a Model.find() with no params (or store.findAll()) will return you ALL records a FilteredModelArray, and ember-data will "register" it into a list, and any new records loaded into the database will be added to this array.
calling Model.filter(func) will give you back a FilteredModelArray, which is also registered with the store... and any new records in the store will cause ember-data to "updateModelArrays", meaning it will call your filter function with the new record, and if you return true, then it will stick it into your existing array.
SO WHAT I ENDED UP DOING: was immediately after creating the store, I call store.findAll(), which gives me back an array of all models for a type... and I attach that to the store... then anywhere else in the code, I can addArrayObservers to those lists.. something like:
App.MyModel = DS.Model.extend()
App.store = DS.Store.create()
App.store.allMyModels = App.store.findAll(App.MyModel)
//some other place in the app... a list controller perhaps
App.store.allMyModels.addArrayObserver({
arrayWillChange: function(arr, start, removeCount, addCount) {}
arrayDidChange: function(arr, start, removeCount, addCount) {}
})
how to push a model into one of those "immutable" arrays:
First to note: all Ember-Data Model instances (records) have a clientId property... which is a unique integer that identifies the model in the datastore cache whether or not it has a real server-id yet (example: right after doing a Model.createRecord).
so the AdapterPopulatedModelArray itself has a "content" property... which is an array of these clientId's... and when you iterate over the AdapterPopulatedModelArray, the iterator loops over these clientId's and hands you back the full model instances (records) that map to each clientId.
SO WHAT I HAVE DONE
(this doesn't mean it's "right"!) is to watch those findAll arrays, and push new clientId's into the content property of the AdapterPopulatedModelArray... SOMETHING LIKE:
arrayDidChange:function(arr, start, removeCount, addCount){
if (addCount == 0) {return;} //only care about adds right now... not removes...
arr.slice(start, start+addCount).forEach(function(item) {
//push clientId of this item into AdapterPopulatedModelArray content list
self.getPath('list.content').pushObject(item.get('clientId'));
});
}
what I can say is: "its working for me" :) will it break on the next ember-data update? totally possible
For those still struggling with this, you can get yourself a dynamic DS.FilteredArray instead of a static DS.AdapterPopulatedRecordArray by using the store.filter method. It takes 3 parameters: type, query and finally a filter callback.
loadMessages: function() {
var self = this,
room_id = App.getPath('current_room.id');
this.store.filter(App.ChatMessage, {room_id: room_id}, function (msg) {
return msg.get('roomId') === room_id;
})
// set content only after promise has resolved
.then(function (messages) {
self.set('content', messages);
});
}
You could also do this in the model hook without the extra clutter, because the model hook will accept a promise directly:
model: function() {
var self = this,
room_id = App.getPath("current_room.id");
return this.store.filter(App.ChatMessage, {room_id: room_id}, function (msg) {
return msg.get('roomId') === room_id;
});
}
My reading of the source (DS.Store.find) shows that what you'd actually be receiving in this instance is an AdapterPopulatedModelArray. A FilteredModelArray would auto-update as you create records. There are passing tests for this behaviour.
As of ember.data 1.13 store.filter was marked for removal, see the following ember blog post.
The feature was made available as a mixin. The GitHub page contains the following note
We recommend that you refactor away from using this addon. Below is a short guide for the three filter use scenarios and how to best refactor each.
Why? Simply put, it's far more performant (and not a memory leak) for you to manage filtering yourself via a specialized computed property tailored specifically for your needs

Computed property being observed doesn't fire if changed twice in a row

I have an Ember.Object that I'm updating with a property like below, but if I change primaryDemo twice in a row, it doesn't fire, yet if I change primaryDemo, then Rate, it does change. I'm puzzled as to why this is and how I can fix it.
dependantChanged: function() {
console.log('Firing change');
this.get('_update')(this);
}.observes('primaryDemo', 'Rate', 'Totals'),
UPDATE: So the first answer and fiddle got me thinking as to what the problem was, and it's due to changing a property on an object and not the object itself. I think ember does a hash check to see if there is a difference. In my case I'm already using underscorejs, so I just change the property, then use _.clone(demo) before doing the set. I'd rather not do that, so will wait to see if there is a more elegant solution before closing this.
You don't need to set primaryDemo again. In the example that does nothing. You need to force tell Ember to notify your observer. See this fiddle...
var demo = { Imps: 1, Demo: { Id: 2 } }
var obj = Ember.Object.create({
dependantChanged: function() {
console.log('Firing change');
}.observes('primaryDemo', 'Rate', 'Totals'),
});
obj.set('primaryDemo', demo);
demo.Imps = 2;
obj.set('primaryDemo', demo);
// Notify observers on obj#primaryDemo
Ember.notifyObservers(obj, 'primaryDemo');
​
Can you give more details? I created a simple JSFiddle http://jsfiddle.net/JjbXb/ from your description but changing the same property in a row, as you say, works.
Are you sure the value of primaryDemo is different in your 2 consecutive calls?

Proper design of REST-powered list in Ember.js

I'm having difficulty wrapping my head around the following:
There's a view that displays the list of items
I take the list of items from the backend via RESTful interface in JSON using ember-data and hand-crafted adapter
In my view I do something like this:
{{#collection contentBinding="App.recentAdditionsController"}}
...
{{/collection}}
App.recentAdditionsController is defined like this:
App.recentAdditionsController = Em.ArrayController.create({
refresh: function(query) {
var items = App.store.findAll(App.Item);
this.set('content', items);
}
});
And... this doesn't work. The reason being App.store.findAll() returning ModelArray which is much like ArrayController itself.
I saw people doing something like this:
App.recentAdditions = App.store.findAll(App.Item);
I could imagine doing it like that, but how would I refresh the list at will (checking if there's anything new).
Hope all is clear more or less.
I've verified that you can use a ModelArray inside an ArrayController. Here's a jsFiddle example: http://jsfiddle.net/ebryn/VkKX2/
"Now the question is how to make the list update itself if there are new objects in the backend?"
Use App.Model.filter to keep your recordArray in sync. Add the query hash when the filter is invoked to ensure than an initial query was made.
model: ->
App.Model.filter {page: 1}, (data) ->
data
edit: Just saw how old the question was, but leaving it here in case it helps someone.