[Solved] @tracked array and each helper problem

Hi everyone!

I have a interesting problem with @tracked array and each helper.

in controller

@tracked
belgeList = [
  {
    adi: "A Belgesi",
    id: "dosya1",
    yuklenen: ""
  },
  {
    adi: "B Belgesi",
    id: "dosya2",
    yuklenen: ""
  },
  {
    adi: "C Belgesi",
    id: "dosya3",
    yuklenen: ""
  }
];

@action
dosyaYuklemeyeHazirla(index, yuklenenDosya) {
  let belgeListesi = [...this.belgeList];
  this.belgeList = [];

  belgeListesi[index]["yuklenen"] = yuklenenDosya.name;

  // update new values
  this.belgeList = [...belgeListesi];
}

in hbs file

{{#if this.belgeList}}
 {{#each this.belgeList as |belge index|}}
    <tr>
      <td>{{belge.adi}}</td>
      <td>
        {{#if belge.yuklenen}}
          {{belge.yuklenen}}
        {{else}}
          <span class="text-danger">Henüz yükleme gerçekleştirilmemiştir.</span>
         {{/if}}
       </td>
       <td class="text-right">
         <FileUpload @name={{belge.id}} @onfileadd={{action this.dosyaYuklemeyeHazirla index}}>
           <a class="btn btn-success btn-sm">Yükle</a>
         </FileUpload>
         <button class="btn btn-danger btn-sm" type="button" disabled={{if belge.yuklenen false "disabled"}}>Sil</button>
       </td>
     </tr>
 {{/each}}
{{/if}}

My object’s yuklenen prop wont update on my hbs. Any advice?

Hi @themesama, I think the problem is that @tracked (at least in its current implementation) works with simple types but you’re trying to use an array of objects, while you’ve marked the array as tracked it doesn’t automatically “reach in” and track each property of each object in the array.

IIRC there are a couple ways around this, you could make your own extended object type, or you can use Ember.set (instead of regular ES assignment) to set the value which updates properly behind the scenes, or you can use the tracked-built-ins addon to provide tracked versions of the complex data types like Object, Array, etc.

1 Like

Doing the assignment like this:

Is a correct way to tell glimmer that you’ve made changes to the array. In fact, with tracked properties (unlike set), you don’t even need to create a new object that is !==. Mutating and reassigning the old array also works, like:

let belgeListesi = this.belgeList;
belgeListesi.push(newThing);
this.belgeList = belgeListesi;

A way to think about it is this: every time you do this.belgeList = anything, that will cause the #each that is consuming this.belgeList to re-render, and there are no === comparisons involved at that point.

However, #each itself tries to be efficient when it rerenders, and it has its own strategies for determining which elements in the array really should be rerendered. By default, it uses ===, but you can customize this behavior via the key argument to #each. In your example, the members of the list remain === before and after the update, so #each will not destroy and recreate its body for any of the array elements.

That means re-rendering will proceed down to the individual fields, like {{belge.yuklenen}}. Since these are not tracked, they won’t know to re-render either.

So there are two different places you could solve this problem. One is to rerender the entire body of the #each for the element that has changed by giving it a new identity:

@action
dosyaYuklemeyeHazirla(index, yuklenenDosya) {
  let belgeListesi = this.belgeList;

  // create a new object with all the same fields as the old object, plus the new `yuklenen`
  belgeListesi[index] = { ...belgeListesi[index], yuklenen: yuklenenDosya.name };

  // and assign to the array so that the `each` will rerender and see our new object
  this.belgeList = belgeListesi;
}

The alternative is to give the individual elements tracked properties:

class MyItem {
  @tracked yuklenen;
  constructor(args) {
    Object.assign(this, args);
  }
}

// ...

@tracked belgeList = [
  new MyItem({
    adi: "A Belgesi",
    id: "dosya1",
    yuklenen: ""
  }
]

@action
dosyaYuklemeyeHazirla(index, yuklenenDosya) {
  this.belgeList[index]["yuklenen"] = yuklenenDosya.name;
}

If there is expensive stuff being rendered in the body of the each, this second solution will be faster because it will only rerender the yuklenen field and nothing else. The tradeoff is that you need to introduce another class for the individual items so you can set up their tracking.

4 Likes

Thank you for the detailed explanations and examples.

Regards, Themesama