angular: NgForOf incorrect behaviour with array of strings and inputs

Which @angular/* package(s) are the source of the bug?

common

Is this a regression?

No

Description

The ngForOf directive has strange behaviour when rendering inputs linked to an array of strings. At first glance it looks like it makes your cursor jump around, but putting breakpoints on lines 9598 (refreshView(tView, lView, templateFn, context);) and 9602 (rendererFactory.end();) of @angular/core/fesm2015/core.mjs, you can see that Angular is actually generating a new input, causing the list to grow and other inputs to move, and then deleting another input. This does not seem to be the case when using objects.

You might say I should add a trackBy function to track by index, but the thing is, this behaviour is solved by passing in ANY trackBy function, even a void one. Surely if a void trackBy function fixes this issue, then it should just work by default, otherwise, you should be able to just add a flag to the directive to make this example work.

Here’s how to reproduce:

<div *ngFor="let val of array; let i = index">
  <input [(ngModel)]="array[i]" />
</div>
export class OneComponent {
  array = '000000'.split('');
}

Then try to type into the inputs.

For whatever reason this works fine:

<div *ngFor="let val of array; let i = index; trackBy: trackByFn">
  <input [(ngModel)]="array[i]" />
</div>
export class OneComponent {
  array = '000000'.split('');
  trackByFn() {}
}

Here’s a picture of the intermediate extra input while debugging - I typed 1 into index 2, it moved down to index 3, and then there were two index 5s.

ngForBug

The end result looks like this - having 1s in two inputs instead of one, the focus moves down an input as well.

ngForBug2

As far as I can tell, with an array of objects this weird intermediate input doesn’t appear, so what gives? Why does passing ANY trackBy function produce correct behaviour?


Update: you can actually reproduce this error with objects:

<div *ngFor="let value of objs; let i = index">
  <input [ngModel]="objs[i].v" (ngModelChange)="setObj(i, $event)" />
</div>
export class OneComponent {
  obj = {v: '0'};
  objs = [this.obj, this.obj, this.obj, this.obj];

  setObj(i: number, value: string) {
    this.objs[i] = {v: value};
  }
}

The bug is reproducible as long as the trackBy function returns the same value for any two entries in the array. So weird behaviour can occur with any duplicate values.

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/angular-ivy-wjkczw?file=src/app/app.component.ts

Please provide the exception or error you saw

No response

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 13.3.2
Node: 16.14.0
Package Manager: npm 8.6.0
OS: win32 x64

Angular: 13.3.2
... animations, cdk, cli, common, compiler, compiler-cli, core
... forms, material, material-moment-adapter, platform-browser
... platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1303.2
@angular-devkit/build-angular   13.3.2
@angular-devkit/core            13.3.2
@angular-devkit/schematics      13.3.2
@schematics/angular             13.3.2
rxjs                            7.5.5
typescript                      4.6.3

Anything else?

No response

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 1
  • Comments: 15 (6 by maintainers)

Commits related to this issue

Most upvoted comments

I still can’t wrap my head around, why iterating over primitives will not work as expected. I can imagine a lot of simple cases, where a basic “todo list” behaviour is desired. Providing a trackBy function doesn’t seem to solve this issue for primitives. I guess highlighting this behaviour in the documentation for NgFor would be a good idea.

Here is my minimal reproduction example: https://stackblitz.com/edit/stackblitz-bug-ngfor-reference

In my case I solved the issue with this approach: https://stackblitz.com/edit/stackblitz-bug-ngfor-reference-nzpvfn?file=src%2Fmain.ts

@mlc-mlapis Have you looked at the stackblitz?

What I’m saying is: the default trackBy function found here:

https://github.com/angular/angular/blob/master/packages/core/src/change_detection/differs/default_iterable_differ.ts#L27

does not uniquely identify primitives, causing bugs.

I’m not talking about supplying a custom function, I’m talking about default behaviour, so new users don’t go “well that doesn’t work” and then head over to React 😄