Home > OS >  Angular.js: TrackBy doesn't prevent full *ngFor-generated list refresh
Angular.js: TrackBy doesn't prevent full *ngFor-generated list refresh

Time:02-14

I have code, which prints words of a simple sentence in an ul-list, so that you can remove any of the items and then insert it back:

  arr = ['hi', 'i', 'am', 'an' ,'item', 'too'].map(function (word) {
    return {value: word}
  });
  trackByFn (item : any) {
    return item.value;
  }
  
  removed   : any = null;
  removed_i : any = null;
  removeItem (i : number) {
    this.removed_i = i;
    this.removed = this.arr.splice(i, 1);
  }
  insert () {
    this.arr.splice(this.removed_i, 0, this.removed);
  }
<ul>
    <li *ngFor="let item of arr; let i = index; trackBy: trackByFn">
      {{item.value}}
      <button *ngIf="!removed" (click)="removeItem(i)">Remove</button>
    </li>
</ul>
<button *ngIf="removed" (click)="insert()">Insert</button>

It resulted in 2 issues:

  1. I tried to do it with trackBy feature. My trackByFn returns item.value as an "id" but still when list is re-rendered after invoking insert(...), there are always several li-s updated (checked via Browser Console)!
  2. Moreover, the inserted item does not contain {{item.value}}, which is interpolated into it!

Please, can somebody explains what's going on?

UPDATE

I have changed trackBy-function and added a button with full_refresh() reaction.

<button (click)="full_refresh()">Random refresh</button>

full_refresh () {
    var prevSource = this.source, prevLength = prevSource.length;
    var source = this.source = new Array(prevLength);
    for (let i = 0; i < prevLength; i  )
      source[i] = {value: prevSource[i].value   prevSource[i].value};
  }

  trackBy (i : number, word : any) {
    return i; // UPDATE 3: corrected according to Siddhant's note
  }

So, now I have changed the trackBy-function (now it returns index with purpose) and added a full refresh button, each of which replaces both old array and array objects with entirely new ones. So, new array objects have nothing to do with the old ones, so == should return false. Then Angular is supposed to check trackBy-function, right? And trackBy-function returns array index, so each new object should be recognized as "identical" to the corresponding old object in array, right? But still I get complete refresh of the elements! And also, an error pops up in console:

Can't bind to 'ngForNgTrackBy' since it isn't a known property of 'li'.

UPDATE 2:

Just found out that function trackBy isn't called at all. When I add console.log(...) into it nothing gets logged when I commit full_refresh, which changes source, which is then printed as a list via ngFor.

Moreover, when I change function name like this (please, see ngTrackBy: trackByByeByeBye in the end of the ngFor statement), app gets compiled but in the console there appears the error: Can't bind to 'ngForNgTrackBy' since it isn't a known property of 'li'.

<ul>
  <li *ngFor="let word of source; let i = index; trackBy: trackByByeByeBye">
  <!--there should be trackBy <<actual property>> : trackBy <<corresponding method>>-->
    {{word.value}}
    <button (click)="remove(i)">Remove</button>
  </li>
</ul>

ANSWER

Please, see the last comment to Saddhant's reply! It is there! :) Above you see the code, which's already been corrected! Enjoy your day! :)

CodePudding user response:

The reason {{item.value}} doesn't print the value after insert is because the value inserted is an array and not an object containing value property.

// splice returns an array of deleted elements
this.removed = this.arr.splice(i, 1);
// this.removed would be [{value: 'deletedValue'}]

// You can use this.removed[0] when inserting
this.arr1.splice(this.removed_i, 0, this.removed[0]);
// In case if this.removed contains multiple elements, you can use spread operator as ...this.removed

In your example, using trackBy doesn't really help in any way and is not required as you are mutating the same array and playing around with same object references. Since object references within the array remain same, Angular automatically would be able to handle unnecessary re-creation of DOM nodes.

Also trackByFn written in your case won't work as expected, because the 1st argument passed to trackByFn is the index.

The proper way to write the function would be:

trackByFn (index: number, item : any) {
  return item.value;
}

Edit: For the new issue added to the post

Since you are now creating a new array with new references, using trackBy will make sense in this scenario.

To your point of why the elements are getting refreshed? You are using word.value within trackByFn and since you have changed the value within the new object, Angular will consider it as a new record and re-create the element.

// The 'value' which is being used in trackByFn is now changed
{value: prevSource[i].value   prevSource[i].value}
  • Related