I have an array of elements which the user can not only edit, but also add and delete complete array elements. This works nicely, unless I attempt to add a value to the beginning of the array (e.g. using unshift
).
Here is a test demonstrating my problem:
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
@Component({
template: `
<form>
<div *ngFor="let item of values; let index = index">
<input [name]="'elem' + index" [(ngModel)]="item.value">
</div>
</form>`
})
class TestComponent {
values: {value: string}[] = [{value: 'a'}, {value: 'b'}];
}
fdescribe('ngFor/Model', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let element: HTMLDivElement;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [TestComponent]
});
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
fixture.detectChanges();
await fixture.whenStable();
});
function getAllValues() {
return Array.from(element.querySelectorAll('input')).map(elem => elem.value);
}
it('should display all values', async () => {
// evaluation
expect(getAllValues()).toEqual(['a', 'b']);
});
it('should display all values after push', async () => {
// execution
component.values.push({value: 'c'});
fixture.detectChanges();
await fixture.whenStable();
// evaluation
expect(getAllValues()).toEqual(['a', 'b', 'c']);
});
it('should display all values after unshift', async () => {
// execution
component.values.unshift({value: 'z'});
fixture.detectChanges();
await fixture.whenStable();
// evaluation
console.log(JSON.stringify(getAllValues())); // Logs '["z","z","b"]'
expect(getAllValues()).toEqual(['z', 'a', 'b']);
});
});
The first two tests pass just fine. However the third test fails. In the third test I attempt to prepend "z" to my inputs, which is successful, however the second input also shows "z", which it should not.
(Note that hundreds of similar questions exist on the web, but in the other cases people were just not having unique name
-attributes and they are also just appending, not prepending).
Why is this happening and what can I do about it?
Notes on trackBy
So far the answers were just "use trackBy". However the documentation for trackBy states:
By default, the change detector assumes that the object instance identifies the node in the iterable
Since I don't supply an explicit trackBy
-Function, that means that angular is supposed to track by identity, which (in the case above) absolutely correctly identifies each object and is inline with what the documentation expects.
The answer by Morphyish basically states that the feature to track by identity is broken and proposes to use an id
-Property. At first it seemed to be a solution, but then it turned out to be just an error. Using an id-Property exhibits the exact same behavior as my test above.
The answer by penleychan tracks by index, which causes angular to think, that after I unshifted a value angular thinks that actually I pushed a value and all values in the array just happened to have updated. It kind of works around the issue, but it is in violation to the track-By contract and it defeats the purpose of the track-by-function (to reduce churn in the DOM).