I’ve looked all over the internet of anyone doing the same without success. It is kind of documented in the API docs but it not very clear what happens when an arrayComputed
depends, in addition to a property of type Array, in other properties that are not enumerable.
Quick example:
I’ve created an object that is a “season balance” that contains all the transaction the user has performed in a season. Since I want to be able to only show the transaction within a specified time window, my balance also has 2 date attributes, timeWindowStart
and timeWindowEnd
, which by default the are initializes to the beginning of the season and the current moment respectively.
The attribute transactions
contains Transaction
objects (id, concept, amount and performedAt fields).
I’ve created an arrayComputed property called visibleTransactions
that only contains the transactions within the time window.
/**
* Computes an array from the transactions in the content that only contains
* the transactions within the time window of the balance.
* @return {Array}
*/
visibleTransactions: Ember.arrayComputed('content', 'timeWindowStart', 'timeWindowEnd', {
addedItem: function(array, item, changeMeta, instanceMeta){
var performedAt = item.get('performedAt'),
timeWindowStart = this.get('timeWindowStart'),
timeWindowEnd = this.get('timeWindowEnd');
if (performedAt >= timeWindowStart && performedAt <= timeWindowEnd){
array.insertAt(Math.min(changeMeta.index, array.length), item);
}
return array;
}
}),
It works great when adding new elements to the transactions. However, when I change any of the bounds of my time window, the whole property needs to be recomputed (array
becomes empty addedItem
is called once per each element in content
). I am sure that this can be improved, but its ok. I can live with that.
The problem is that I also have another arrayComputed property that dependes on visibleTransactions
. This is it:
/**
* Computes a new array from the visible transactions. Each element of the
* array is a BalanceEntry, witch groups all the transactions within the time window
* window by its concept.
* @return {Array}
*/
arrangedContent: Ember.arrayComputed('visibleTransactions', {
initialize: function(array, changeMeta, instanceMeta){
instanceMeta.concepts = [];
},
addedItem: function(array, item, changeMeta, instanceMeta){
var concept = item.get('concept');
var summaryIndex = instanceMeta.concepts.indexOf(concept);
var balanceEntry;
if (summaryIndex == -1){
balanceEntry = BalanceEntry.create({concept: concept, transactions: [item]});
array.push(balanceEntry);
instanceMeta.concepts.push(concept);
} else {
balanceEntry = array[summaryIndex];
balanceEntry.get('transactions').push(item);
}
return array;
},
removedItem: function(array, item, changeMeta, instanceMeta){
var index = instanceMeta.concepts.indexOf(item.get('concept'));
var balanceEntry = array[index];
if (balanceEntry.get('transactions.length') == 1){
array.removeAt(index);
instanceMeta.concepts.removeAt(index);
} else {
balanceEntry.get('transactions').removeObject(item);
}
return array;
}
})
I am doing some stuff with the instance meta for optimization but the idea is simple. Each time a new transaction appears I check if there is already a BalanceEntry
object with that concept. If it exist, I add this transaction to that entry. If it is a new concept, I create a new balanceEntry for group transactions with that new concept.
Again, it works great when adding transactions.
The problem appears when I change the bounds of the time windows:
Since this property depends only in the visible transactions, I expected addedItem
and removedItem
to be called once visibleTransactions
gets modified, BUT this property is updated BEFORE visibleTransactions
is updated.
So, the facts happends in that extrange order:
- Initial status. I have default bounds in my time window and all the transactions are therefore visible.
- I set the new bounds of my time window:
Ember.run(balance, 'setProperties', {timeWindowStart: twoDaysAgo, timeWindowEnd: yesterday});
-
Surprise:
arrangedContent#addedItem(array, item, changeMeta, instanceMeta)
is called. Thearray
property is empty butvisibleTransactions
is still unchanged, so all the items invisibleTransactions
are added 1 by one util all is like it was before. Basically the property is recomputed again from scratch (with identical result since the array on which it depends has not changed yet) -
visibleTransactions
starts to be recomputed. It is empty and is called once for each transaction. In each iteration the transaction might be within the time window or not. When it is, the transaction is added and after thatarrangedContent#addedItem
is called inmediatly. The arrangedContent is empty again and we start adding transactions. This time since it is called each time a transaction “passes” the visibility filter,addedItem
is called only a few times and the result is correct.
I don’t know if i’ve explained the problem clearly, but is quite difficult to provide a gist for that since it is something that is only visible while debugging, because even if it is sub-obtimal, the result is correct.
Maybe I am doing something wrong, but it seems that when an arrayComputed depends has dependencies that are not arrays, other CP that depend on that arrayComputed have an unexpected behavior.
I’ll be glad to pair-program with anybody with a deeper knowledge of arrayComputed than me.
Ideally I would expect visible arrays to get recalculated first, and after that arrangedContent#removedItem
or arrangedContent#addedItem
being called with transactions have left/entered the new time window.