New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot observe changes to nested properties on array using @each #541

Closed
robharper opened this Issue Feb 27, 2012 · 75 comments

Comments

Projects
None yet
@robharper
Contributor

robharper commented Feb 27, 2012

Adding an observer on a nested property of objects within a collection does not reliably fire. I've created a test case that could be added to ember-runtime/tests/mixins/array_test.js that illustrates this. Observing a nested property on the EachProxy itself appears to work fine. However, observing the property using @each.nest.isDone does not.

In the following test, count1 is incremented to 1, count2 is not:

test('modifying nested contents of an object in an array should call @each observers', function() {
  var ary = new TestArray([
      Em.Object.create({ nest: Em.Object.create({ isDone: false}) }),
      Em.Object.create({ nest: Em.Object.create({ isDone: false}) }),
      Em.Object.create({ nest: Em.Object.create({ isDone: false}) }),
      Em.Object.create({ nest: Em.Object.create({ isDone: false}) })
    ]);

  var get = Ember.get, set = Ember.set;
  var count1 = 0, count2 = 0;

  // Works
  var each = get(ary, '@each');
  Ember.addObserver(each, 'nest.isDone', function() { count1++; });

  // Doesn't work
  Ember.addObserver(ary, '@each.nest.isDone', function() { count2++; });

  count1 = count2 = 0;
  var item = ary.objectAt(2);
  get(item, 'nest').set('isDone', true);

  equal(count1, 1, '@each.nest.isDone should have notified - observing @each array');
  equal(count2, 1, '@each.nest.isDone should have notified - observing chain containing @each');
});
@wagenet

This comment has been minimized.

Show comment
Hide comment
@wagenet

wagenet Feb 27, 2012

Member

@each is only supported one level deep. It would be nice to allow more, but I don't know how difficult this would be technically. I'll let @wycats or @tomdale chime in on this.

Member

wagenet commented Feb 27, 2012

@each is only supported one level deep. It would be nice to allow more, but I don't know how difficult this would be technically. I'll let @wycats or @tomdale chime in on this.

@wycats

This comment has been minimized.

Show comment
Hide comment
@wycats

wycats Feb 27, 2012

Member

I don't think that's actually true... It uses the normal chain infrastructure and should work arbitrarily many levels deep.

Member

wycats commented Feb 27, 2012

I don't think that's actually true... It uses the normal chain infrastructure and should work arbitrarily many levels deep.

@wagenet

This comment has been minimized.

Show comment
Hide comment
@wagenet

wagenet Feb 27, 2012

Member

@wycats This sounds like a bug then. I've never seen it work more than one level deep.

Member

wagenet commented Feb 27, 2012

@wycats This sounds like a bug then. I've never seen it work more than one level deep.

@tomdale

This comment has been minimized.

Show comment
Hide comment
@tomdale

tomdale Feb 27, 2012

Member

Agreed, this sounds like a bug. I believe this was working at some point. Thank you for the unit test.

Member

tomdale commented Feb 27, 2012

Agreed, this sounds like a bug. I believe this was working at some point. Thank you for the unit test.

@endash

This comment has been minimized.

Show comment
Hide comment
@endash

endash Apr 4, 2012

Contributor

So I've been puzzling through this. It seems to boil down to EachProxy handling setting up the observers internally when you call addObserver with itself as the root. When as part of a chained path, however, the ChainNode plucks the top-level keyname off of the EachProxy, and goes on its merry way, and the EachProxy never gets involved. Basically, there are two levels of arrays at play.

watching '@each.nest.@each.isDone' DOES work, in this instance.

Contributor

endash commented Apr 4, 2012

So I've been puzzling through this. It seems to boil down to EachProxy handling setting up the observers internally when you call addObserver with itself as the root. When as part of a chained path, however, the ChainNode plucks the top-level keyname off of the EachProxy, and goes on its merry way, and the EachProxy never gets involved. Basically, there are two levels of arrays at play.

watching '@each.nest.@each.isDone' DOES work, in this instance.

@endash

This comment has been minimized.

Show comment
Hide comment
@endash

endash Apr 4, 2012

Contributor

Forgot to clarify that when the ChainNode plucks the keyname off the EachProxy, it's getting an EachArray, hence the need for nesting the @ each observers.

Contributor

endash commented Apr 4, 2012

Forgot to clarify that when the ChainNode plucks the keyname off the EachProxy, it's getting an EachArray, hence the need for nesting the @ each observers.

@krisselden

This comment has been minimized.

Show comment
Hide comment
@krisselden

krisselden Jun 20, 2012

Member

The way chains work is each chain node has a corresponding chainWatcher that watches the key on the node's parent's value(), in order for this to work the chain needs to stop at @each and setup the chainWatcher with the remaining path.

I'm considering special casing @ character to just mean this.

Member

krisselden commented Jun 20, 2012

The way chains work is each chain node has a corresponding chainWatcher that watches the key on the node's parent's value(), in order for this to work the chain needs to stop at @each and setup the chainWatcher with the remaining path.

I'm considering special casing @ character to just mean this.

@wagenet

This comment has been minimized.

Show comment
Hide comment
@wagenet

wagenet Jul 9, 2012

Member

@kselden, does your simplify-properties branch touch on this at all?

Member

wagenet commented Jul 9, 2012

@kselden, does your simplify-properties branch touch on this at all?

@wagenet

This comment has been minimized.

Show comment
Hide comment
@wagenet

wagenet Jul 21, 2012

Member

Looks like this is still an issue.

Member

wagenet commented Jul 21, 2012

Looks like this is still an issue.

@krisselden

This comment has been minimized.

Show comment
Hide comment
@krisselden

krisselden Jul 21, 2012

Member

chains do not chain through properties that produce arrays, this requires some changes I stated above, no I haven't done any work on it. It also is problematic @each itself changes when the array does.

Member

krisselden commented Jul 21, 2012

chains do not chain through properties that produce arrays, this requires some changes I stated above, no I haven't done any work on it. It also is problematic @each itself changes when the array does.

@tpitale

This comment has been minimized.

Show comment
Hide comment
@tpitale

tpitale Aug 11, 2012

Contributor

I've hacked around this, for anyone that is facing this issue (especially when using ember-data associations), by adding a property on one side of the association that checks the property in the other end of the association (in my case, isLoaded) and then using that after the @each. As I said, this is a hack. But until 1.1, it works for me.

Contributor

tpitale commented Aug 11, 2012

I've hacked around this, for anyone that is facing this issue (especially when using ember-data associations), by adding a property on one side of the association that checks the property in the other end of the association (in my case, isLoaded) and then using that after the @each. As I said, this is a hack. But until 1.1, it works for me.

@drogus

This comment has been minimized.

Show comment
Hide comment
@drogus

drogus Aug 24, 2012

Contributor

@tpitale: could you show an example of such hack? I'm not sure what you mean by adding a property on other side of association.

@kselden @wagenet: I encountered something similar recently, but frankly I'm not sure if this is the same bug, could you take a look at this fiddle: http://jsfiddle.net/uErrd/4/ ?

Contributor

drogus commented Aug 24, 2012

@tpitale: could you show an example of such hack? I'm not sure what you mean by adding a property on other side of association.

@kselden @wagenet: I encountered something similar recently, but frankly I'm not sure if this is the same bug, could you take a look at this fiddle: http://jsfiddle.net/uErrd/4/ ?

@tpitale

This comment has been minimized.

Show comment
Hide comment
@tpitale

tpitale Aug 24, 2012

Contributor

@drogus given the original example, there would be a nestIsDone property for the path nest.isDone. So you could observe @each.nestIsDone.

Contributor

tpitale commented Aug 24, 2012

@drogus given the original example, there would be a nestIsDone property for the path nest.isDone. So you could observe @each.nestIsDone.

@mspisars

This comment has been minimized.

Show comment
Hide comment
@mspisars

mspisars Oct 22, 2012

Contributor

would really love to see this in 1.0 not 1.1 👍

Contributor

mspisars commented Oct 22, 2012

would really love to see this in 1.0 not 1.1 👍

@wagenet

This comment has been minimized.

Show comment
Hide comment
@wagenet

wagenet Oct 22, 2012

Member

@mspisars Me too, but I don't want to block 1.0 on things that are difficult to fix and non-essential.

Member

wagenet commented Oct 22, 2012

@mspisars Me too, but I don't want to block 1.0 on things that are difficult to fix and non-essential.

@darthdeus

This comment has been minimized.

Show comment
Hide comment
@darthdeus

darthdeus Feb 14, 2013

Member

last activity 4 months ago ... 👍

Member

darthdeus commented Feb 14, 2013

last activity 4 months ago ... 👍

@wagenet

This comment has been minimized.

Show comment
Hide comment
@wagenet

wagenet Feb 19, 2013

Member

@mspisars, @darthdeus I would love to see it in 1.0 too. The issue is not about desire, it's about ability.

Member

wagenet commented Feb 19, 2013

@mspisars, @darthdeus I would love to see it in 1.0 too. The issue is not about desire, it's about ability.

@phammer

This comment has been minimized.

Show comment
Hide comment
@phammer

phammer Feb 20, 2013

👍 for 1.0: would be really useful to have this working.

phammer commented Feb 20, 2013

👍 for 1.0: would be really useful to have this working.

@pdwinkel

This comment has been minimized.

Show comment
Hide comment
@pdwinkel

pdwinkel Mar 2, 2013

👍 for 1.0

pdwinkel commented Mar 2, 2013

👍 for 1.0

@wagenet

This comment has been minimized.

Show comment
Hide comment
@wagenet

wagenet Mar 5, 2013

Member

Public Service Announcement: +1s do not provide development resources where they are lacking.

We've already agreed that we'd love to have this feature but it's very much non-trivial to support. It's certainly not something we'll block 1.0 on.

Member

wagenet commented Mar 5, 2013

Public Service Announcement: +1s do not provide development resources where they are lacking.

We've already agreed that we'd love to have this feature but it's very much non-trivial to support. It's certainly not something we'll block 1.0 on.

@endash

This comment has been minimized.

Show comment
Hide comment
@endash

endash Mar 5, 2013

Contributor

Agreed with @wagenet these are non-trivial code paths that touch mission critical parts of ember.

Contributor

endash commented Mar 5, 2013

Agreed with @wagenet these are non-trivial code paths that touch mission critical parts of ember.

@HWest

This comment has been minimized.

Show comment
Hide comment
@HWest

HWest May 15, 2013

Any updates on this? We have been trying to use this for a web app that's about to be used by a major Hotel chain and none of the workarounds seem to work. The only workable solution at this point is to redesign this part of the interface to make it simpler which is a shame.
Any information would be greatly appreciated and thanks for all your great work.

HWest commented May 15, 2013

Any updates on this? We have been trying to use this for a web app that's about to be used by a major Hotel chain and none of the workarounds seem to work. The only workable solution at this point is to redesign this part of the interface to make it simpler which is a shame.
Any information would be greatly appreciated and thanks for all your great work.

@joliss

This comment has been minimized.

Show comment
Hide comment
@joliss

joliss May 15, 2013

Contributor

It's a surprisingly hard problem. Part of the issue is that even if you implemented it or worked around it, updating a dependent array (which seems to be be the most common use case) would take O(n*2) time, which is a big problem even for fairly small datasets. To get it to be O(n), we'd need smarter array primitives to compose dependent arrays. There has been *some work on this by Peter and others, and people are definitely interested in pushing it forward, but I wouldn't hold my breath.

Related: #1288, #2711.

For now I'd recommend generating the dependent arrays on the server, basically as "computed relationships".

Contributor

joliss commented May 15, 2013

It's a surprisingly hard problem. Part of the issue is that even if you implemented it or worked around it, updating a dependent array (which seems to be be the most common use case) would take O(n*2) time, which is a big problem even for fairly small datasets. To get it to be O(n), we'd need smarter array primitives to compose dependent arrays. There has been *some work on this by Peter and others, and people are definitely interested in pushing it forward, but I wouldn't hold my breath.

Related: #1288, #2711.

For now I'd recommend generating the dependent arrays on the server, basically as "computed relationships".

@tim-evans

This comment has been minimized.

Show comment
Hide comment
@tim-evans

tim-evans May 16, 2013

Contributor

Here's another fiddle demonstrating this issue: http://jsfiddle.net/3bGN4/268.

Contributor

tim-evans commented May 16, 2013

Here's another fiddle demonstrating this issue: http://jsfiddle.net/3bGN4/268.

@meelash

This comment has been minimized.

Show comment
Hide comment
@meelash

meelash Jun 14, 2013

Example workaround, demonstrating nested @each:

https://gist.github.com/meelash/5785328

meelash commented Jun 14, 2013

Example workaround, demonstrating nested @each:

https://gist.github.com/meelash/5785328

@dandehavilland

This comment has been minimized.

Show comment
Hide comment
@dandehavilland

dandehavilland Aug 17, 2013

If this is on the backburner, it would be helpful to note this behaviour in the API docs (maybe I missed it?) as it took me a while to find this ticket.

dandehavilland commented Aug 17, 2013

If this is on the backburner, it would be helpful to note this behaviour in the API docs (maybe I missed it?) as it took me a while to find this ticket.

@luxferresum

This comment has been minimized.

Show comment
Hide comment
@luxferresum

luxferresum Aug 20, 2013

@dandehavilland Exactly! It should definitly be noted here: 'http://emberjs.com/guides/object-model/computed-properties-and-aggregate-data/'. "@each.goo.bar" is still not working, i've needed days to find this out.

luxferresum commented Aug 20, 2013

@dandehavilland Exactly! It should definitly be noted here: 'http://emberjs.com/guides/object-model/computed-properties-and-aggregate-data/'. "@each.goo.bar" is still not working, i've needed days to find this out.

@joliss

This comment has been minimized.

Show comment
Hide comment
@joliss

joliss Aug 27, 2013

Contributor

Good idea. emberjs/website#653

Contributor

joliss commented Aug 27, 2013

Good idea. emberjs/website#653

@kingpin2k

This comment has been minimized.

Show comment
Hide comment
@kingpin2k

kingpin2k Aug 27, 2013

Contributor

But technically you can view nested properties, just not nested collections of properties, right?

works - flagpole and flag are singular

    schools.@each.flagpole.@each.flag

but should be

    schools.@each.flagpole.flag

* doesn't work - flags is a collection*

    schools.@each.flags.@each.flag 
Contributor

kingpin2k commented Aug 27, 2013

But technically you can view nested properties, just not nested collections of properties, right?

works - flagpole and flag are singular

    schools.@each.flagpole.@each.flag

but should be

    schools.@each.flagpole.flag

* doesn't work - flags is a collection*

    schools.@each.flags.@each.flag 
@joliss

This comment has been minimized.

Show comment
Hide comment
@joliss

joliss Aug 28, 2013

Contributor

@kingpin2k No, I don't believe any of those work. They won't update correctly.

[Edit: maybe I'm wrong]

Contributor

joliss commented Aug 28, 2013

@kingpin2k No, I don't believe any of those work. They won't update correctly.

[Edit: maybe I'm wrong]

@kingpin2k

This comment has been minimized.

Show comment
Hide comment
@kingpin2k

kingpin2k Aug 28, 2013

Contributor

@joliss here's a jsbin showing it. http://jsbin.com/IXalezO/11/edit

Contributor

kingpin2k commented Aug 28, 2013

@joliss here's a jsbin showing it. http://jsbin.com/IXalezO/11/edit

@wagenet

This comment has been minimized.

Show comment
Hide comment
@wagenet

wagenet Oct 12, 2013

Member

@kselden is this something you're likely to work on any time soon?

Member

wagenet commented Oct 12, 2013

@kselden is this something you're likely to work on any time soon?

@wagenet

This comment has been minimized.

Show comment
Hide comment
@wagenet

wagenet Nov 9, 2013

Member

This is documented as not working. It would be great to add, but it's obviously difficult / not high enough a priority. If someone wants to work on it, we'd be happy to review a PR.

Member

wagenet commented Nov 9, 2013

This is documented as not working. It would be great to add, but it's obviously difficult / not high enough a priority. If someone wants to work on it, we'd be happy to review a PR.

@serhiisol

This comment has been minimized.

Show comment
Hide comment
@serhiisol

serhiisol Sep 11, 2014

You can just add new computed property, which will contains only very nested object, which you're trying to observe. And @each will works correctly
Use next method

  allTickets: function () {
    var sections = this.get('sections'),
      each = Ember.$.each,
      tickets = [];

    each(sections, function (i, section) {
      each(section.rows, function (j, row) {
        each(row.tickets, function (k, ticket) {
          tickets.addObject(ticket);
        })
      });
    });

    return tickets;
  }.property(),

  checkedCount: function () {
    ///custom logic
    return something;
  }.property('allTickets.@each.is_checked')

instead of

  checkedCount: function () {
    ///custom logic
    return something
  }.property('sections.@each.rows.@each.tickets.@each.is_checked')

serhiisol commented Sep 11, 2014

You can just add new computed property, which will contains only very nested object, which you're trying to observe. And @each will works correctly
Use next method

  allTickets: function () {
    var sections = this.get('sections'),
      each = Ember.$.each,
      tickets = [];

    each(sections, function (i, section) {
      each(section.rows, function (j, row) {
        each(row.tickets, function (k, ticket) {
          tickets.addObject(ticket);
        })
      });
    });

    return tickets;
  }.property(),

  checkedCount: function () {
    ///custom logic
    return something;
  }.property('allTickets.@each.is_checked')

instead of

  checkedCount: function () {
    ///custom logic
    return something
  }.property('sections.@each.rows.@each.tickets.@each.is_checked')
@Microfed

This comment has been minimized.

Show comment
Hide comment
@Microfed

Microfed commented Oct 9, 2014

👍

@panayi

This comment has been minimized.

Show comment
Hide comment
@panayi

panayi Oct 23, 2014

This is so easy to miss in the guides. I believe it should issue a warning when a nested @each is found.

panayi commented Oct 23, 2014

This is so easy to miss in the guides. I believe it should issue a warning when a nested @each is found.

@rwwagner90

This comment has been minimized.

Show comment
Hide comment
@rwwagner90

rwwagner90 May 13, 2015

Member

This is still an issue many years later, correct? Is there a supported workaround of some sort?

Member

rwwagner90 commented May 13, 2015

This is still an issue many years later, correct? Is there a supported workaround of some sort?

@kingpin2k

This comment has been minimized.

Show comment
Hide comment
@kingpin2k

kingpin2k May 14, 2015

Contributor

Yes, you could always just build up computed properties at each level and watch the computed property instead of each property on each collection.

Contributor

kingpin2k commented May 14, 2015

Yes, you could always just build up computed properties at each level and watch the computed property instead of each property on each collection.

@rwwagner90

This comment has been minimized.

Show comment
Hide comment
@rwwagner90

rwwagner90 May 14, 2015

Member

@kingpin2k I tried something like that, and it didn't work. Had to use .observes because the property never evaluated itself. There should really be some sort of built in Ember stuff to handle this, in my opinion.

Member

rwwagner90 commented May 14, 2015

@kingpin2k I tried something like that, and it didn't work. Had to use .observes because the property never evaluated itself. There should really be some sort of built in Ember stuff to handle this, in my opinion.

@kingpin2k

This comment has been minimized.

Show comment
Hide comment
@kingpin2k

kingpin2k May 15, 2015

Contributor

Here's an example: http://emberjs.jsbin.com/faximekico/2/edit You essentially need to aggregate the values at the mid level.

Contributor

kingpin2k commented May 15, 2015

Here's an example: http://emberjs.jsbin.com/faximekico/2/edit You essentially need to aggregate the values at the mid level.

@rwwagner90

This comment has been minimized.

Show comment
Hide comment
@rwwagner90

rwwagner90 May 15, 2015

Member

I understand how to do it, the properties just don't fire, so observes had to be used.

Member

rwwagner90 commented May 15, 2015

I understand how to do it, the properties just don't fire, so observes had to be used.

@kingpin2k

This comment has been minimized.

Show comment
Hide comment
@kingpin2k

kingpin2k May 15, 2015

Contributor

Can you show me an example, properties are lazily loaded, so if you aren't actually attempting to get the properties, they won't "fire".

Contributor

kingpin2k commented May 15, 2015

Can you show me an example, properties are lazily loaded, so if you aren't actually attempting to get the properties, they won't "fire".

@rwwagner90

This comment has been minimized.

Show comment
Hide comment
@rwwagner90

rwwagner90 May 16, 2015

Member

I never did an explicit .get to retrieve the property in question, I did have another computed property that was supposed to listen to it though. It seems that only an explicit get or use in a template makes them fire though, and not having another property observe them.

Member

rwwagner90 commented May 16, 2015

I never did an explicit .get to retrieve the property in question, I did have another computed property that was supposed to listen to it though. It seems that only an explicit get or use in a template makes them fire though, and not having another property observe them.

@kingpin2k

This comment has been minimized.

Show comment
Hide comment
@kingpin2k

kingpin2k May 17, 2015

Contributor

Correct, properties are only calculated on demand, just watching a property doesn't mean the property should be calculated. Rightfully so, if no one wants to use the property, there is no need to calculate the property.

Contributor

kingpin2k commented May 17, 2015

Correct, properties are only calculated on demand, just watching a property doesn't mean the property should be calculated. Rightfully so, if no one wants to use the property, there is no need to calculate the property.

@rwwagner90

This comment has been minimized.

Show comment
Hide comment
@rwwagner90

rwwagner90 May 17, 2015

Member

@kingpin2k so what is recommend in this case? We're not supposed to use observes anymore.

Member

rwwagner90 commented May 17, 2015

@kingpin2k so what is recommend in this case? We're not supposed to use observes anymore.

@kingpin2k

This comment has been minimized.

Show comment
Hide comment
@kingpin2k

kingpin2k May 17, 2015

Contributor

My example shows how to accomplish the nested each dependencies without an observer. I don't recall anyone saying observers shouldn't be used anymore, but I don't religiously pay attention.

Contributor

kingpin2k commented May 17, 2015

My example shows how to accomplish the nested each dependencies without an observer. I don't recall anyone saying observers shouldn't be used anymore, but I don't religiously pay attention.

@rwwagner90

This comment has been minimized.

Show comment
Hide comment
@rwwagner90

rwwagner90 May 17, 2015

Member

@kingpin2k your example uses a property, .property('model.books.@each.titles')

That gives me the same problem where it won't fire, as it is a property. I have another property that needs to observe that property.

Member

rwwagner90 commented May 17, 2015

@kingpin2k your example uses a property, .property('model.books.@each.titles')

That gives me the same problem where it won't fire, as it is a property. I have another property that needs to observe that property.

@alexspeller

This comment has been minimized.

Show comment
Hide comment
@alexspeller

alexspeller May 17, 2015

Contributor

@rwwagner90 the approach definitely works, I suspect there is another issue in your code causing this problem. Try posting a jsbin showing the problem to figure out what it is

Contributor

alexspeller commented May 17, 2015

@rwwagner90 the approach definitely works, I suspect there is another issue in your code causing this problem. Try posting a jsbin showing the problem to figure out what it is

@kingpin2k

This comment has been minimized.

Show comment
Hide comment
@kingpin2k

kingpin2k May 17, 2015

Contributor

I might understand what you are getting at, could you modify my example to clarify though?

Contributor

kingpin2k commented May 17, 2015

I might understand what you are getting at, could you modify my example to clarify though?

@alexspeller

This comment has been minimized.

Show comment
Hide comment
@alexspeller

alexspeller May 17, 2015

Contributor

@rwwagner90 regarding observers, my take on it is that using observers in cases when you're not interacting with non-ember code is a big code smell. So using observers to e.g. trigger a d3 graph rendering when your app data changes is totally legit, but using observers to set one ember property when another changes is almost always more easily accomplished with a different approach

Contributor

alexspeller commented May 17, 2015

@rwwagner90 regarding observers, my take on it is that using observers in cases when you're not interacting with non-ember code is a big code smell. So using observers to e.g. trigger a d3 graph rendering when your app data changes is totally legit, but using observers to set one ember property when another changes is almost always more easily accomplished with a different approach

@rwwagner90

This comment has been minimized.

Show comment
Hide comment
@rwwagner90

rwwagner90 May 17, 2015

Member

@alexspeller @kingpin2k I have two properties, and one must change when the other changes. This is not possible though because properties don't evaluate, so the second property never updates.

Member

rwwagner90 commented May 17, 2015

@alexspeller @kingpin2k I have two properties, and one must change when the other changes. This is not possible though because properties don't evaluate, so the second property never updates.

@alexspeller

This comment has been minimized.

Show comment
Hide comment
@alexspeller

alexspeller May 17, 2015

Contributor

@rwwagner90 having one property that changes when another changes is a core feature of computed properties, so I'm not sure what you're describing - if you show the code you're using it's likely that someone can help.

Contributor

alexspeller commented May 17, 2015

@rwwagner90 having one property that changes when another changes is a core feature of computed properties, so I'm not sure what you're describing - if you show the code you're using it's likely that someone can help.

@rwwagner90

This comment has been minimized.

Show comment
Hide comment
@rwwagner90

rwwagner90 May 17, 2015

Member

@alexspeller I converted it all to using observes, but let me attempt to put it back and show you.

let extendedObject = Ember.Object.extend({
          valueLength: function() {
            return this.get('value').length;
          }.property('value.@each')
        });

Then I have a property that should update when valueLength does:

}.property('model.data.@each', 'model.data.@each.valueLength'),

However, this never fires, so I was forced to change my valueLength property to an observer that sets valueLength instead like so:

let extendedObject = Ember.Object.extend({
          valueLengthUpdater: function() {
            this.set('valueLength', this.get('value').length);
          }.observes('value.@each')
        });
Member

rwwagner90 commented May 17, 2015

@alexspeller I converted it all to using observes, but let me attempt to put it back and show you.

let extendedObject = Ember.Object.extend({
          valueLength: function() {
            return this.get('value').length;
          }.property('value.@each')
        });

Then I have a property that should update when valueLength does:

}.property('model.data.@each', 'model.data.@each.valueLength'),

However, this never fires, so I was forced to change my valueLength property to an observer that sets valueLength instead like so:

let extendedObject = Ember.Object.extend({
          valueLengthUpdater: function() {
            this.set('valueLength', this.get('value').length);
          }.observes('value.@each')
        });
@krisselden

This comment has been minimized.

Show comment
Hide comment
@krisselden

krisselden May 17, 2015

Member

@rwwagner90 your dependency keys don't match what you are getting, your dependency key should be model.data.length

Member

krisselden commented May 17, 2015

@rwwagner90 your dependency keys don't match what you are getting, your dependency key should be model.data.length

@krisselden

This comment has been minimized.

Show comment
Hide comment
@krisselden

krisselden May 17, 2015

Member

Also, model.data.@each.valueLength is ok, by you should then actually get('valueLength') when computing a value. In a CP, generally speaking, if you are doing ending a dependency with just '@each' you aren't going to have a good time, and dependencies need to be consumed as part of the CP calculation. You cannot assume things about stuff changing together (stuff is invalidated synchronously and if there are observers that consume the property it can lead to race conditions), dependencies should align with what you actually consume as part of making the calculated value.

Generally, observers are low level, and hard to get right, you shouldn't actually do work in them (it can lead to race conditions, overly frequent recalculations, etc) they should only schedule work once per event frame.

Member

krisselden commented May 17, 2015

Also, model.data.@each.valueLength is ok, by you should then actually get('valueLength') when computing a value. In a CP, generally speaking, if you are doing ending a dependency with just '@each' you aren't going to have a good time, and dependencies need to be consumed as part of the CP calculation. You cannot assume things about stuff changing together (stuff is invalidated synchronously and if there are observers that consume the property it can lead to race conditions), dependencies should align with what you actually consume as part of making the calculated value.

Generally, observers are low level, and hard to get right, you shouldn't actually do work in them (it can lead to race conditions, overly frequent recalculations, etc) they should only schedule work once per event frame.

@kingpin2k

This comment has been minimized.

Show comment
Hide comment
@kingpin2k

kingpin2k May 17, 2015

Contributor

Ditto @krisselden. Also, I'm assuming your model on some controller has a property data which is a collection of extendedObject and in the controller you have the computed property, and your extendedObject has a length property on it (which sounds like it should be an array, and not an object, but ignoring that:

Controller
something: function(){
   // inside of here you should actually fetch `valueLength` off of each item, or else it isn't really a dependecy
  // assuming data returns an array
  var lengths = this.get('model.data').getEach('valueLength');
}.property('model.data.length', 'model.data.@each.valueLength'),
Contributor

kingpin2k commented May 17, 2015

Ditto @krisselden. Also, I'm assuming your model on some controller has a property data which is a collection of extendedObject and in the controller you have the computed property, and your extendedObject has a length property on it (which sounds like it should be an array, and not an object, but ignoring that:

Controller
something: function(){
   // inside of here you should actually fetch `valueLength` off of each item, or else it isn't really a dependecy
  // assuming data returns an array
  var lengths = this.get('model.data').getEach('valueLength');
}.property('model.data.length', 'model.data.@each.valueLength'),
@alexspeller

This comment has been minimized.

Show comment
Hide comment
@alexspeller

alexspeller May 17, 2015

Contributor

Edit:

You can replace valueLength property with valueLength: Ember.computed.alias('value.length').

Your dependent key then becomes:

}.property('model.data.@each.valueLength')

See @krisselden's explanation below on why what I wrote previously won't work.

@rwwagner90 valueLength is not necessary there - you can just use the value.length property. The property's dependent key can be simplified to:

}.property('model.data.@each.value.length')

@each shouldn't be used without a further property on the end. If you just want to observe when items are added or removed use somearray.[] instead. If you observe foo.@each.something then you don't need to also observer foo.[]

Contributor

alexspeller commented May 17, 2015

Edit:

You can replace valueLength property with valueLength: Ember.computed.alias('value.length').

Your dependent key then becomes:

}.property('model.data.@each.valueLength')

See @krisselden's explanation below on why what I wrote previously won't work.

@rwwagner90 valueLength is not necessary there - you can just use the value.length property. The property's dependent key can be simplified to:

}.property('model.data.@each.value.length')

@each shouldn't be used without a further property on the end. If you just want to observe when items are added or removed use somearray.[] instead. If you observe foo.@each.something then you don't need to also observer foo.[]

@krisselden

This comment has been minimized.

Show comment
Hide comment
@krisselden

krisselden May 17, 2015

Member

@alexspeller Actually you can't

Member

krisselden commented May 17, 2015

@alexspeller Actually you can't

@krisselden

This comment has been minimized.

Show comment
Hide comment
@krisselden

krisselden May 17, 2015

Member

@alexspeller get('@each.value') unfortunately returns an array of the values instead of returning an EachProxy which kills further chaining. I don't know why that was done that way but I don't know if I can change it given semver, and to be honest, I'm nervous about enabling this.

Member

krisselden commented May 17, 2015

@alexspeller get('@each.value') unfortunately returns an array of the values instead of returning an EachProxy which kills further chaining. I don't know why that was done that way but I don't know if I can change it given semver, and to be honest, I'm nervous about enabling this.

@kingpin2k

This comment has been minimized.

Show comment
Hide comment
@kingpin2k

kingpin2k May 17, 2015

Contributor

The squeaky wheel gets the oil, until it's affecting a larger portion of the ember community it won't get prioritized up, there is plenty of other low hanging fruit that helps 90% of the community.

Contributor

kingpin2k commented May 17, 2015

The squeaky wheel gets the oil, until it's affecting a larger portion of the ember community it won't get prioritized up, there is plenty of other low hanging fruit that helps 90% of the community.

@rwwagner90

This comment has been minimized.

Show comment
Hide comment
@rwwagner90

rwwagner90 May 17, 2015

Member

@alexspeller Ember.computed.alias worked. Thanks! 👍

Member

rwwagner90 commented May 17, 2015

@alexspeller Ember.computed.alias worked. Thanks! 👍

@rwwagner90

This comment has been minimized.

Show comment
Hide comment
@rwwagner90

rwwagner90 May 17, 2015

Member

@krisselden Chaining @each would be awesome, but no one has tackled it in the past few years, so it doesn't seem likely that it ever will be.

Member

rwwagner90 commented May 17, 2015

@krisselden Chaining @each would be awesome, but no one has tackled it in the past few years, so it doesn't seem likely that it ever will be.

@vothai-son

This comment has been minimized.

Show comment
Hide comment
@vothai-son

vothai-son Oct 3, 2016

I hope this issue is still on cause I don't know where to post this issue.
I am using Ember 2.1.0+45f524a3

I got the observer FIRED with this:

var o = Ember.Object.create({
    a: [{ b: 1 }, {b: 2} ]
})

Ember.addObserver(o, 'a.@each.b', o, function () {
    console.log("FIRE")
});

Ember.set(o.a[0], 'b', 3)

But with this, nothing happens

var o = Ember.Object.create({
    A: [{ b: 1 }, {b: 2} ]
})

Ember.addObserver(o, 'A.@each.b', o, function () {
    console.log("FIRE")
});

Ember.set(o.A[0], 'b', 3)

(The difference is the case of property name "a" and "A")

So, anyone recognizes this issue, please help me explain. Thanks.

vothai-son commented Oct 3, 2016

I hope this issue is still on cause I don't know where to post this issue.
I am using Ember 2.1.0+45f524a3

I got the observer FIRED with this:

var o = Ember.Object.create({
    a: [{ b: 1 }, {b: 2} ]
})

Ember.addObserver(o, 'a.@each.b', o, function () {
    console.log("FIRE")
});

Ember.set(o.a[0], 'b', 3)

But with this, nothing happens

var o = Ember.Object.create({
    A: [{ b: 1 }, {b: 2} ]
})

Ember.addObserver(o, 'A.@each.b', o, function () {
    console.log("FIRE")
});

Ember.set(o.A[0], 'b', 3)

(The difference is the case of property name "a" and "A")

So, anyone recognizes this issue, please help me explain. Thanks.

@alexspeller

This comment has been minimized.

Show comment
Hide comment
@vothai-son

This comment has been minimized.

Show comment
Hide comment
@vothai-son

vothai-son Oct 3, 2016

Thanks. But I have it triggering with the lowercase "a".

var o = Ember.Object.create({
    a: [{ b: 1 }, {b: 2} ]
})

Ember.addObserver(o, 'a.@each.b', o, function () {
    console.log("FIRE")
});

Ember.set(o.a[0], 'b', 3)

And no computed property here.

vothai-son commented Oct 3, 2016

Thanks. But I have it triggering with the lowercase "a".

var o = Ember.Object.create({
    a: [{ b: 1 }, {b: 2} ]
})

Ember.addObserver(o, 'a.@each.b', o, function () {
    console.log("FIRE")
});

Ember.set(o.a[0], 'b', 3)

And no computed property here.

@alexspeller

This comment has been minimized.

Show comment
Hide comment
@alexspeller

alexspeller Oct 3, 2016

Contributor

Oh, the upper case a is not supported. Use property names that start with lower case letters.

Contributor

alexspeller commented Oct 3, 2016

Oh, the upper case a is not supported. Use property names that start with lower case letters.

@vothai-son

This comment has been minimized.

Show comment
Hide comment
@vothai-son

vothai-son Oct 3, 2016

Thanks,
This happens with an uppercase name and ".@each."
It's fine with simple property. Like this:

var o = Ember.Object.create({
    A: 1
})

Ember.addObserver(o, 'A', o, function () {
    console.log("FIRE")
});

Ember.set(o, 'A', 3)

I can't find documentation for that anywhere.
I think I will endup with a bunch of lowercase aliases for those uppercase named properties.

vothai-son commented Oct 3, 2016

Thanks,
This happens with an uppercase name and ".@each."
It's fine with simple property. Like this:

var o = Ember.Object.create({
    A: 1
})

Ember.addObserver(o, 'A', o, function () {
    console.log("FIRE")
});

Ember.set(o, 'A', 3)

I can't find documentation for that anywhere.
I think I will endup with a bunch of lowercase aliases for those uppercase named properties.

@rwjblue

This comment has been minimized.

Show comment
Hide comment
@rwjblue

rwjblue Oct 3, 2016

Member

Historically (pre-2.0), capitalized paths indicated a global (so in your case it would be observing window.A). This issue is talking about nested paths with @each, which your comment is not discussing.

Most (hopefully all) of the special casing of capitalized paths should be removed. We should test against 2.8.1. If it still isnt working on 2.8, please open a new issue with a twiddle/jsbin reproduction.

Member

rwjblue commented Oct 3, 2016

Historically (pre-2.0), capitalized paths indicated a global (so in your case it would be observing window.A). This issue is talking about nested paths with @each, which your comment is not discussing.

Most (hopefully all) of the special casing of capitalized paths should be removed. We should test against 2.8.1. If it still isnt working on 2.8, please open a new issue with a twiddle/jsbin reproduction.

@timothyerwin

This comment has been minimized.

Show comment
Hide comment
@timothyerwin

timothyerwin Jan 8, 2018

This is extremely basically functionality which is sadly missing. It would be helpful to focus on getting the basics right before going off adding all these bells and whistles to the framework.

timothyerwin commented Jan 8, 2018

This is extremely basically functionality which is sadly missing. It would be helpful to focus on getting the basics right before going off adding all these bells and whistles to the framework.

@luxferresum

This comment has been minimized.

Show comment
Hide comment
@luxferresum

luxferresum Jan 15, 2018

@timothyerwin actually it's pretty easy to work around this. Also I think right now is a bad moment to change this. Things like '@Tracked' im glimmer show, that we need to re-think the entire CP-thing in the long term. Also don't get me wrong, I would love to have this sometimes!

luxferresum commented Jan 15, 2018

@timothyerwin actually it's pretty easy to work around this. Also I think right now is a bad moment to change this. Things like '@Tracked' im glimmer show, that we need to re-think the entire CP-thing in the long term. Also don't get me wrong, I would love to have this sometimes!

@lukemelia

This comment has been minimized.

Show comment
Hide comment
@lukemelia

lukemelia Jan 16, 2018

Member

This is the Ember issue I encounter most frequently lately, often because it is hidden behind something else, like a filter or sort CP macro where it's easy to forget you effectively creating a dependent key that is an @each with nested properties.

Member

lukemelia commented Jan 16, 2018

This is the Ember issue I encounter most frequently lately, often because it is hidden behind something else, like a filter or sort CP macro where it's easy to forget you effectively creating a dependent key that is an @each with nested properties.

@jacobq

This comment has been minimized.

Show comment
Hide comment
@jacobq

jacobq Mar 2, 2018

Contributor

@luxferresum Would you mind posting a link to more information about how to work around this? Admittedly, I repeatedly get stumped by this, and I know I've been given some hints via community slack help channel, but I can't remember them nor can I seem to find a good post from searching the web. I think perhaps the trick was to break out each level as its own computed property somehow.

So if I have an array like traces: [{ x: [...], y: [...]}, ...], and I want to trigger an external charting library to update based on the (not allowed) key traces.@each.{x,y}.[] would I flatten it like this?

// Not only is this ugly, but it doesn't work since `_oneBigMess` won't get
// called again when something in one of its nested arrays changes,
// e.g. Changing traces[0].x[0] does not trigger 'traces.[]'
_oneBigMess: computed('traces.[]', function() {
  return this.get('traces').reduce(function(result, item, i, a) {
    return {
      x: result.x.concat(item.x),
      y: result.x.concat(item.y)
    };
  }, { x: [], y: []})
}),
_somethingChanged: observer('_oneBigMess.x.[]', '_oneBigMess.y.[]', function() {
  // update chart
})

I'm sure the above code is idiotic to someone more experienced and familiar with the tools available, but I am grasping at straws here.

but none seem to have a work-around for the situation I described 😢

Update: Someone please correct me if I'm wrong, but after fighting with this for a while tonight, it seems that the problem here isn't merely about providing more syntactic sugar but really about a limitation of the current system. If I understand correctly (based on the old comment above and some search results) this functionality isn't provided yet because there is not an efficient / clean way to do it. The advice that I was given was basically to not do this but rather change the design of my code so that I didn't need to. This is probably good advice but sometimes may not be feasible (e.g. insufficient time or requirements outside of one's control). For two-levels, one (pretty horrible & inefficient) work-around is to generate a long (maximum data size your code needs to handle correctly) list of keys, each associated with a single element of the array. For example:

_watchChanges: observer('data.[]', 'data.@each.{name,type}',
  ...(new Array(100).fill(0).map((z,i) => `data.${i}.x.[]`)),
  ...(new Array(100).fill(0).map((z,i) => `data.${i}.y.[]`)), function() {
  // something in one of these arrays has changed --> go do something
})

Another possibility (though I haven't tried it, and it's still not ideal) would be to iterate over the top-level array and use addArrayObserver to watch each nested array (and then removeArrayObserver later to avoid leaking).

Contributor

jacobq commented Mar 2, 2018

@luxferresum Would you mind posting a link to more information about how to work around this? Admittedly, I repeatedly get stumped by this, and I know I've been given some hints via community slack help channel, but I can't remember them nor can I seem to find a good post from searching the web. I think perhaps the trick was to break out each level as its own computed property somehow.

So if I have an array like traces: [{ x: [...], y: [...]}, ...], and I want to trigger an external charting library to update based on the (not allowed) key traces.@each.{x,y}.[] would I flatten it like this?

// Not only is this ugly, but it doesn't work since `_oneBigMess` won't get
// called again when something in one of its nested arrays changes,
// e.g. Changing traces[0].x[0] does not trigger 'traces.[]'
_oneBigMess: computed('traces.[]', function() {
  return this.get('traces').reduce(function(result, item, i, a) {
    return {
      x: result.x.concat(item.x),
      y: result.x.concat(item.y)
    };
  }, { x: [], y: []})
}),
_somethingChanged: observer('_oneBigMess.x.[]', '_oneBigMess.y.[]', function() {
  // update chart
})

I'm sure the above code is idiotic to someone more experienced and familiar with the tools available, but I am grasping at straws here.

but none seem to have a work-around for the situation I described 😢

Update: Someone please correct me if I'm wrong, but after fighting with this for a while tonight, it seems that the problem here isn't merely about providing more syntactic sugar but really about a limitation of the current system. If I understand correctly (based on the old comment above and some search results) this functionality isn't provided yet because there is not an efficient / clean way to do it. The advice that I was given was basically to not do this but rather change the design of my code so that I didn't need to. This is probably good advice but sometimes may not be feasible (e.g. insufficient time or requirements outside of one's control). For two-levels, one (pretty horrible & inefficient) work-around is to generate a long (maximum data size your code needs to handle correctly) list of keys, each associated with a single element of the array. For example:

_watchChanges: observer('data.[]', 'data.@each.{name,type}',
  ...(new Array(100).fill(0).map((z,i) => `data.${i}.x.[]`)),
  ...(new Array(100).fill(0).map((z,i) => `data.${i}.y.[]`)), function() {
  // something in one of these arrays has changed --> go do something
})

Another possibility (though I haven't tried it, and it's still not ideal) would be to iterate over the top-level array and use addArrayObserver to watch each nested array (and then removeArrayObserver later to avoid leaking).

@luxferresum

This comment has been minimized.

Show comment
Hide comment
@luxferresum

luxferresum Mar 4, 2018

@jacobq

but rather change the design of my code

true. however there are much easer ways to handle this than you suggest.
let me show it to your by your original example, with an array traces: [{ x: [...], y: [...]}, ...], where you basically want to get a list of all x and all y.

the primary trick/workaround here is to convert each trace into an ember object, and then have a CP on the Trace with a dependency key of x.[] and y.[]. This CP will fire whenever something is added/removed in the x/y arrays. Next you can provide a simple CP that has traces.@each.myProp as a dependency key, where traces is the list of ember Trace objects, and myProp is the CP I just talked about.

Or with code:

const Trace = Ember.Object.extend({
  wrapped: Ember.computed('x.[]', 'y.[]', {
    get() {
      return {
        x: this.get('x'),
        y: this.get('y'),
      }
    }
  }),
});

export default Ember.Component.extend({
  traces: null,
  tracesAsEmberObjects('traces.[]', {
    get() {
      this.get('traces').map(t => Trace.create(t));
    }
  }),
  oneBigMess: Ember.computed('tracesAsEmberObjects.@each.wrapped', {
    get() {
      return this.get('tracesAsEmberObjects').reduce((result, item, i, a) => {
        return {
          x: result.x.concat(item.x),
          y: result.x.concat(item.y)
        };
      }, {x:[], y:[]});
    }
  }),
});

actually it doesnt really matter what wrapped returns, as long it has the right dependency keys, you .get() it, and have it as a .@each.wrapped dependency key in the outer CP. However I prefer it to return something useful.

luxferresum commented Mar 4, 2018

@jacobq

but rather change the design of my code

true. however there are much easer ways to handle this than you suggest.
let me show it to your by your original example, with an array traces: [{ x: [...], y: [...]}, ...], where you basically want to get a list of all x and all y.

the primary trick/workaround here is to convert each trace into an ember object, and then have a CP on the Trace with a dependency key of x.[] and y.[]. This CP will fire whenever something is added/removed in the x/y arrays. Next you can provide a simple CP that has traces.@each.myProp as a dependency key, where traces is the list of ember Trace objects, and myProp is the CP I just talked about.

Or with code:

const Trace = Ember.Object.extend({
  wrapped: Ember.computed('x.[]', 'y.[]', {
    get() {
      return {
        x: this.get('x'),
        y: this.get('y'),
      }
    }
  }),
});

export default Ember.Component.extend({
  traces: null,
  tracesAsEmberObjects('traces.[]', {
    get() {
      this.get('traces').map(t => Trace.create(t));
    }
  }),
  oneBigMess: Ember.computed('tracesAsEmberObjects.@each.wrapped', {
    get() {
      return this.get('tracesAsEmberObjects').reduce((result, item, i, a) => {
        return {
          x: result.x.concat(item.x),
          y: result.x.concat(item.y)
        };
      }, {x:[], y:[]});
    }
  }),
});

actually it doesnt really matter what wrapped returns, as long it has the right dependency keys, you .get() it, and have it as a .@each.wrapped dependency key in the outer CP. However I prefer it to return something useful.

jacobq added a commit to EmberMN/ember-cli-plotly that referenced this issue Mar 6, 2018

@jacobq

This comment has been minimized.

Show comment
Hide comment
@jacobq

jacobq Mar 6, 2018

Contributor

@luxferresum Thanks for taking the time to reply with a potential solution. However, it seems to have the same problem. Although creating a new EmberObject with a computed property allows one to react to changes of the arrays held by that object, the process of creating that object seems to create a copy of the original data, so changing the original data does not trigger the CP/observer.

For example, in the ember-cli-plotly addon I'm trying to make I have an integration test that changes the data to be plotted, and it works when I use a kludgy work-around where the main component generates a tagless child component for each trace, which then fire an action when data in their set changes.

I will try your method a little more in case I'm just doing something wrong (very possible), but the way it's acting now makes me think that it's not as simple as you suggest.

Contributor

jacobq commented Mar 6, 2018

@luxferresum Thanks for taking the time to reply with a potential solution. However, it seems to have the same problem. Although creating a new EmberObject with a computed property allows one to react to changes of the arrays held by that object, the process of creating that object seems to create a copy of the original data, so changing the original data does not trigger the CP/observer.

For example, in the ember-cli-plotly addon I'm trying to make I have an integration test that changes the data to be plotted, and it works when I use a kludgy work-around where the main component generates a tagless child component for each trace, which then fire an action when data in their set changes.

I will try your method a little more in case I'm just doing something wrong (very possible), but the way it's acting now makes me think that it's not as simple as you suggest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment