9

I'm trying out Octane, and for some reason, if I show an array in a template and I add a new object to it, the UI doesn't update. What am I doing wrong?

Here is my template:

<label for="new-field-name">Field Name</label>
<Input id="new-field-name" @value={{this.newFieldName}} type="text" />
<button {{on "click" this.addField}}>Add field</button>

{{#each this.fields as |field|}}
    <p>{{field.name}}</p>
{{/each}}

And the component:

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

export default class ConfigControlsComponent extends Component {
    @tracked fields = []
    @tracked newFieldName = ''

    @action addField() {
        this.fields.push({
            name: this.newFieldName
        })
        console.log(this.fields)
    }
}

The console.log shows the array with the new object added to it, and the fields array is tracked, but nothing changes when I click the button.

handlebears
  • 2,168
  • 8
  • 18

2 Answers2

11

When you use tracked with arrays, you need to "reset" the array so that Ember notices that there has been a change. Try doing this.fields = this.fields after pushing a new object into the array.

Edit: some linters will guard against self-assignment. So, instead, we could pull from immutability patterns, and set using a new array, as shown below.

export default class ConfigControlsComponent extends Component {
  @tracked fields = []
  @tracked newFieldName = ''

  @action addField() { 
    // add this line
    this.fields = [...this.fields, {
      name: this.newFieldName
    }]; 
  }
}

If you are trying to use tracked with an object instead of an array, you have two options:

First, you could create a class where all the properties on the object are tracked:

import { tracked } from '@glimmer/tracking';

class Address {
  @tracked street;
  @tracked city;
}

class Person {
  address = new Address();

  get fullAddress() {
    let { street, city } = this.address;

    return `${street}, ${city}`;
  }
}

Or, second, you could use the same "reset" approach as the array example above.

NullVoxPopuli
  • 61,906
  • 73
  • 206
  • 352
handlebears
  • 2,168
  • 8
  • 18
  • 4
    You could do `this.fields = [...this.fields, { name: this.newFieldName }]` as well. – locks Aug 13 '19 at 01:20
  • 4
    `pushObject` instead `push` should work fine. This notifies ember to re-render. – Aswath Apr 23 '20 at 09:28
  • 1
    IMHO @Aswath's answer looks right. The OP should be using `pushObject` method since it is compatible with KVO compliant classes. Additionally, it would be desirable to use `EmberArray` class explicitly, through `A([])` – Thiago Petrone Jul 19 '21 at 13:29
2

Arrays are not automatically tracked if one of their element change. You cannot do:

this.fields.push({
        name: this.newFieldName
    })

But they get tracked if you change the whole array, using the spread operator for example:

this.fields = [...this.fields, {name: this.newFieldName}]

Or eventually you can use EmberArray, see https://guides.emberjs.com/release/in-depth-topics/autotracking-in-depth/#toc_arrays

pinkra
  • 21
  • 3