4

I am building my first Glimmmer component in Ember and I had something like this in my template:

<p>Some val: {{this.my_obj.some.deeply.nested.path.to.val}}</p>

I then made a corresponding .js component file and thought I would try to remove the long path from the HTML.

My first attempt was to make a getter:

get val() {
  return this.my_obj.some.deeply.nested.path.to.val;
}

With the template now:

<p>Some val: {{this.val}}</p>

That works initially and displays the starting value assigned to the val variable, but it does not update when the underlying val changes.

So I thought I would try marking the getter as tracked, but that made the output disapear:

@tracked val;

get val() {
  return this.my_obj.some.deeply.nested.path.to.val;
}

I then tried this, but it's not valid syntax:

@tracked this.my_obj.some.deeply.nested.path.to.val;

So how should this be handled in a Glimmer component?

Surely the solution is not for the HTML to reference a deep path like this everytime the variable is referenced, however the new Ember docs, nice as they are, leave me none the wiser on this relatively simple/common case.

Ginty
  • 3,483
  • 20
  • 24

3 Answers3

6

It's simple: whatever you change needs to be @tracked. If you have {{this.my_obj.some.deeply.nested.path.to.val}} in your hbs or a getter that returns that does not make a difference.

But if you do this.my_obj.some.deeply.nested.path.to.val = "something" and you want the value to update, you need to ensure that on this.my_obj.some.deeply.nested.path.to val is defined as tracked.

So this will not work:

@tracked data;
constructor() {
  super(...arguments);
  this.data = { foo: 1 };
}
@action test() {
  this.data.foo = 2; // this will not correctly update
}

you need to ensure that foo is @tracked:

class DataWrapper {
  @tracked foo;
}

...

@tracked data;
constructor() {
  super(...arguments);
  this.data = new DataWrapper();
  this.data.foo = 1;
}
@action test() {
  this.data.foo = 2; // this will now correctly update
}

Or you manually invalidate. So this will work:

@tracked data;
constructor() {
  super(...arguments);
  this.data = { foo: 1 };
}
@action test() {
  this.data.foo = 2; // this will not update
  this.data = this.data; // this ensures that everything that depends on `data` will correctly update. So `foo` will correctly update.
}

Also an important thing: @tracked should never be defined on a getter. This will not work. It should directly be defined where you want to change something that should trigger an update. However you can use getters without problems. It will just work, as long as everything that you set with objsomething = value is correctly @tracked. Also (pure) function calls will just work.

wuarmin
  • 3,274
  • 3
  • 18
  • 31
Lux
  • 17,835
  • 5
  • 43
  • 73
  • Thanks for the reply. So, in my case the variable is owned within a 3rd party library, does that mean that I cannot trigger (.js) changes from it unless the library owner marks it as tracked? – Ginty Jan 20 '20 at 12:51
  • yes and no. If you have objects from third party libraries they usually change because you call some methods on that library. Then you should do manual invalidation afterwards (`this.data = this.data`). – Lux Jan 20 '20 at 13:07
  • Thanks for the `this.data = this.data` tip, but that's not ideal - for example I would need to think about when the change occurs, does it occur as soon as the method returns or will it be resolved later? I found that there is another solution and I've added an answer with it. – Ginty Jan 20 '20 at 20:20
  • Yout *solution* will not work. Yes, you need to consider asynchronity when you do `this.data = this.data`. However its not a big problem to do `this.data = this.data` to often. – Lux Jan 21 '20 at 08:16
0

There is an add-on for that. https://www.npmjs.com/package/ember-tracked-nested which works with Ember 3.16+.

Basically, it follows Lux's answer using DataWrapper and cache invalidation. Instead, it's done recursively with a proxy so that it doesn't matter the structure of the object or how nested it is, it will always work.

import { tracked } from '@glimmer/tracking';
import Component from '@glimmer/component';
import { nested } from 'ember-tracked-nested';
import { action } from '@ember/object';

// works with POJO
export default class Foo extends Component {
  @tracked obj = nested({ bar: 2 });

  @action
  changeObj() {
      this.obj.bar = 10;
  }
}

// works when updating nested array
export default class Foo extends Component {
  @tracked obj = nested([{ bar: 2 }, { bar: 4 }]);
  
  @action
  changeObj() {
    this.obj[1].bar = 100;
  }
}

// works with array method
export default class Foo extends Component {
  @tracked obj = nested([{ bar: 2 }, { bar: 4 }]);

  @action
  changeObj() {
    this.obj.push({ bar: 6 });
  }
}

// works with POJO with getter
export default class Foo extends Component {
  @tracked obj = nested({ bar: 2, get foo() { return this.bar } });

  @action
  changeObj() {
    this.obj.bar = 9;
  }
}
Kean Tan
  • 11
  • 1
-1

I found the answer in this cheatsheet - https://ember-learn.github.io/ember-octane-vs-classic-cheat-sheet/#component-properties

There is a @computed decorator which enables the nested attribute to be tracked for changes properly:

import { computed } from '@ember/object';

// Note that no 'this.' is used here
@computed("my_obj.some.deeply.nested.path.to.val")
get val() {
  return this.my_obj.some.deeply.nested.path.to.val;
}
Ginty
  • 3,483
  • 20
  • 24
  • 3
    This will actually not work! `@computed` is 1:1 `computed` from the old pre-octane ember object model. It requires that you make every change by `Ember.set`! So you have to do `Ember.set(this, 'my_obj.some.deeply.nested.path.to.val', 'value')` for it to work. If you change something with `ctx.val = 'value'` it will *not* update. The only exception is if its `@tracked` because of the compatibility layer to `@tracked`. You can manually notify about changes tho with `notifyPropertyChange`, but thats basically the same as `this.data = this.data` with `@tracked`. – Lux Jan 21 '20 at 08:12
  • 1
    This is a fundamental limitation of the Javascript language that could only be overcome with performance degressions. Maybe you want to join the [ember Discord channel](https://emberjs.com/community) where we could discuss this in detail. – Lux Jan 21 '20 at 08:15