Ember.js: `{{#each item in array}}` does not rerender when the array is sorted and the change is properly notified

Created on 9 Feb 2015  路  25Comments  路  Source: emberjs/ember.js

Here's my demo code:

App.ApplicationController = Ember.Controller.extend({
    anArray: ['sleeping', 'eating pizza','programming', 'looking at lolcats'],

    actions: {
      sort: function() {

        var anArray = this.get("anArray");

        console.log('anArray in action (before):', anArray.join(', '));

        this.propertyWillChange('anArray');
        anArray.sort();
        this.propertyDidChange('anArray');

        console.log('anArray in action (after):', anArray.join(', '));
      }
    },

    someOtherArray: Ember.computed('anArray.[]', function() {
      var anArray = this.get('anArray');
      console.log('anArray in someOtherArray:', anArray.join(', '));
      return anArray;
    })
});

When i trigger the action, the array gets sorted.

I've created the second property to make sure that the first property does change. From console output you can see that the computed property receives a sorted value and it does recalculate on every button click.

But the sorting is not reflected on the page! =-O

Demo: http://emberjs.jsbin.com/mamiru/4/edit?html,js,output

Most helpful comment

I changed

this.propertyWillChange('anArray');
anArray.sort();
this.propertyDidChange('anArray');

to this.set("anArray", anArray.sort().slice()); and worked. This is the new demo http://emberjs.jsbin.com/kezuqopaci/1/edit.
Note that without slice the template is not updated, I believe that there is a cache layer in the template checking the new property with ===, and since anArray.sort() === anArray the template is not updated.

All 25 comments

Pushing to the array is not reflected on the page as well.

I changed

this.propertyWillChange('anArray');
anArray.sort();
this.propertyDidChange('anArray');

to this.set("anArray", anArray.sort().slice()); and worked. This is the new demo http://emberjs.jsbin.com/kezuqopaci/1/edit.
Note that without slice the template is not updated, I believe that there is a cache layer in the template checking the new property with ===, and since anArray.sort() === anArray the template is not updated.

I know that setting the whole property to a new object forces it to rerender.

The point of this issue ticket is to be able to announce a property change without having to clone the object stored in the property and without resetting the whole property.

PS Thank you for arr.slice(), i was using a bulkier [].concat(arr)

The rerender observer isn't fired because the CP has the same value (with respect to strict equality ===) before and after it recomputes. So even though the array changed in the mean time, Ember doesn't know. @marcioj's suggestion to slice() the array is correct. In the future we will use array diffing to provide an ergonomic solution to this problem.

Hey, i was wrong: using proper .pushObject() instead of .push() does update the view.

If it were just ===, .pushObject() wouldn't redraw, right?

Please explain and/or reopen.

The funny thing is that when i do .sort().pushObject('foo'), the pushed object appears last, but the rest of the array does not update (still unsorted). The resulting view demonstrates a state that the array never posessed.

Example. Initial state: 321. You do sort and get 123. Then you do pushObject and get 1234. But the view demonstrates 3214.

@mmun, you say the problem is due to ===. But === only compares references to objects (unless they are primitives, and arrays aren't primitives). Thus, i would expect .pushObject() not to update the view, but it does! What's the magic behind this?

This behaviour is also correct. There are two ways a collection view will update: complete rerender or a partial rerender.

A complete rerender will happen when the content array reference changes (detected using regular property observers). By returning the same array reference in your CP you are preventing this kind of rerender.

A partial rerender will happen whenever replace is called on the content array (detected using array observers). pushObject de-sugars to replace, as does pushObjects, unshiftObject, etc. In this case, when you call pushObject is it performing a partial rerender of the last position.

Diffing container views will replace these two behaviours with a single, consolidated update strategy that is easier to reason about.

@mmun, so what do i do instead of propertyWillChange/propertyDidChange so that property observers detect a change applied without .slice() or .replace()?

You should use .sort().slice() or .slice().sort() (depends if you want to mutate the original array or not).

@mmun, does this mean that it is impossible to notify observers that an object has changed without replacing it with a different object?

.replace() is somehow able to announce a change without replacing the object, right?

@lolmaus No.

You could try replacing the array with itself but I don't know if that works and I don't see the value.

Thank you for clarifications, @mmun.

Last question: why did you close this? This behavior is counter-intuitive and contradicts with how computed properties behave (they have no problem recalculating on propertyWillChange/propertyDidChange, while {{#each}} ignores those announcements). It looks to me like a legit problem worth addressing.

It's working as expected. Each isn't ignoring announcements. _No announcement is made_ because the value doesn't change. If there's a problem it is the inflexibility of observing changes on an array. It has nothing to do with each.

There is no announcement.

propertyDidChange is the announcement. Computed properties understand it and recalculate properly.

If there's a problem it is the inflexibility of observing changes on an array.

No, it's not. Ember.observer( 'myArray', function(){} ) fires every time i do this.propertyDidChange('myArray'), it doesn't even need .[].

In my JSBin you can see that the computed property logs the changed array every time you click the button.

It has nothing to do with each.

The only place so far that fails to notice the propertyDidChange is {{#each}}.

@lolmaus Computed properties have an optimization to prevent notifying they've changed when you return the same (===) object: https://github.com/emberjs/ember.js/blob/master/packages/ember-metal/lib/computed.js#L474

@ebryn, you've linked to an optimization within the .set() method. I think it has nothing to do with propertyDidChange.

Apologies, I misread the situation. I'm looking into this further as it seems counter-intuitive. cc @krisselden

The problem is that dataSource is bound to content. When dataSource changes it calls set(obj, 'content', value) but the value is the same so the content observers are not notified. See https://github.com/emberjs/ember.js/blob/master/packages/ember-metal/lib/property_set.js#L60-L62.

More generally, any time a binding synchronizes to the same value that it already is the observers on the receiving end will not be called.

The issue isn't unique to {{each}}. It happens across any binding, for example binding a value to a component: http://emberjs.jsbin.com/buxupu/1/edit?html,js,output.

Aha, I suspected it was related to set short-circuiting when ===, just didn't reference the right place :P

The behavior makes sense. The reason pushing and object works is because the arrayChanged, even though the reference to the array is the same. You can add an arrayObserver and check this:

x = [3,2,1]
x.addArrayObserver({arrayWillChange: function () { console.log(arguments)}, arrayDidChange: function () { console.log(arguments);}})
x.pushObject(4);
x.sort()

The problem here is that sort, does mutate the array, but it doesn't notify the arrayObservers. Sort is native, but the same is true for other non-native methods like sortBy. I'm not sure what a good solution is...

@lolmaus if you really wanted to force this without creating a new array, you could call arrayWillChange/arrayDidChange instead of the property counterparts.

        anArray.arrayContentWillChange(0, anArray.length-1, anArray.length-1);
        anArray.sort();
        anArray.arrayContentDidChange(0, anArray.length-1, anArray.length -1)

See: http://emberjs.jsbin.com/jukuvuquqi/2/edit

@MiguelMadero, you da real MVP!

The proper way is to have a standalone way of firing the array change events not removing ===. Dirty checking still relies on === at some point, and there are lots of caches that are === checked, chain nodes, streams, views etc often have local caches that are checked this way. notifyPropertyChange is more 'invalidate and schedule recheck' than it is a force dirty. We will add a obj.* and a standalone way to fire arr.[] so you can do a force dirty that will cause dependent streams to update.

/cc @mmun @ebryn @wycats

Was this page helpful?
0 / 5 - 0 ratings