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.
The end result looks like this - having 1s in two inputs instead of one, the focus moves down an input as well.
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
- fix(core): modify default trackBy to return index if not object change default behaviour to allow for duplicate primitives in ngFor iterable Fixes: #45647 — committed to ChrisHamilton91/angular by deleted user 2 years ago
- test(core): fix old default_iterable_differ tests update tests to use objects instead of primitives, reason is iterables no longer attempt to track add / remove / move of primitives Related to: #456... — committed to ChrisHamilton91/angular by deleted user 2 years ago
- test(core): fix old acceptance and animation tests update tests to use objects instead of primitives, reason is iterables no longer attempt to track add / remove / move of primitives Related to: #45... — committed to ChrisHamilton91/angular by deleted user 2 years ago
- test(common): fix old ngClass test update tests to use objects instead of primitives, reason is iterables no longer attempt to track add / remove / move of primitives Related to: #45647 — committed to ChrisHamilton91/angular by deleted user 2 years ago
- refactor(core): formatting default_iterable_differ just a clang-format run Related to: #45647 — committed to ChrisHamilton91/angular by deleted user 2 years ago
- test(forms): fix old reactive_integration tests update tests to use objects instead of primitives, reason is iterables no longer attempt to track add / remove / move of primitives Related to: #45647 — committed to ChrisHamilton91/angular by deleted user 2 years ago
- fix(core): modify default trackBy to return index if not object change default behaviour to allow for duplicate primitives in ngFor iterable Fixes: #45647 — committed to ChrisHamilton91/angular by deleted user 2 years ago
- test(core): update old iterable tests to use only objects use objects instead of primitives as iterables no longer attempt to track add / remove / move of primitives Related to: #45647 — committed to ChrisHamilton91/angular by deleted user 2 years ago
- test(forms): update old tests to use only objects update tests to use objects instead of primitives, reason is iterables no longer attempt to track add / remove / move of primitives Related to: #456... — committed to ChrisHamilton91/angular by deleted user 2 years ago
- fix(common): maintain track by value for ngClass iterable maintain same behaviour, default track by function no longer tracks primitives by value Caused by: #45647 — committed to ChrisHamilton91/angular by deleted user 2 years ago
- fix(core): rewrite default_iterable_differ correctly handle duplicate values, find a minimal set of changes on check(), reduce number of operations performed during forEachOperation() Fixes #45647 — committed to ChrisHamilton91/angular by deleted user 2 years ago
- test(core): add default_iterable_differ tests Add tests to ensure the problems with the old default_iterable_differ remain solved. The old implementation produced unnecessary or incorrect operations ... — committed to ChrisHamilton91/angular by deleted user 2 years ago
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 😄