简体   繁体   中英

How to track a nested attribute in a Glimmer component?

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.

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.

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;
  }
}

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;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM