Ответ 1
Изменить
После дальнейшего расследования проблема фактически не исходила от ngFor
. Это был ngModel
с использованием атрибута name
ввода.
В цикле атрибут name
генерируется с использованием индекса массива. Однако при размещении нового элемента в начале массива у нас неожиданно появляется новый элемент с тем же именем.
Вероятно, это создает конфликт с несколькими ngModel
, наблюдающими один и тот же вход внутри.
Такое поведение можно наблюдать при добавлении нескольких входов в начале массива. Все входы, которые были изначально созданы с одним и тем же атрибутом name
, будут принимать значение нового создаваемого входа. Независимо от того, были ли изменены их значения или нет.
Чтобы устранить эту проблему, вам просто нужно дать каждому входу уникальный name
. Либо с помощью уникального id
, как в моем примере ниже
<input [name]="'elem' + item.id" [(ngModel)]="item.value">
Или с помощью генератора уникальных имен/идентификаторов (аналогично тому, что делает Angular Material).
Оригинальный ответ
Проблема, как утверждает penleychan, заключается в отсутствии trackBy
в вашей директиве ngFor
.
Вы можете найти рабочий пример того, что вы ищете здесь
С обновленным кодом из вашего примера
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; trackBy: trackByFn">
<input [name]="'elem' + index" [(ngModel)]="item.value">
</div>
</form>'
})
class TestComponent {
values: {id: number, value: string}[] = [{id: 0, value: 'a'}, {id: 1, value: 'b'}];
trackByFn = (index, item) => item.id;
}
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({id: 2, 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({id: 2, value: 'z'});
fixture.detectChanges();
await fixture.whenStable();
// evaluation
console.log(JSON.stringify(getAllValues())); // Logs '["z","z","b"]'
expect(getAllValues()).toEqual(['z', 'a', 'b']);
});
});
Несмотря на ваш комментарий, это не обходной путь. trackBy
был создан для типа использования (а также для исполнения, но оба связаны).
Вы можете найти код директивы ngForOf
здесь, если хотите посмотреть сами, но вот как это работает.
Директива ngForOf
дифференцирует массив для определения внесенных изменений, однако, если не была передана определенная функция trackBy
, она остается для мягкого сравнения. Что хорошо для простой структуры данных, такой как строки или числа. Но когда вы используете Objects
, он может очень быстро испортиться.
Помимо снижения производительности, отсутствие четкой идентификации для элементов внутри массива может заставить массив перерисовать всю совокупность элементов.
Однако, если директива ngForOf
способна четко определить, какой элемент был изменен, какой элемент был удален, а какой добавлен. Он может оставить все остальные элементы нетронутыми, добавлять или удалять шаблоны из DOM по мере необходимости и обновлять только те, которые должны быть.
Если вы добавите функцию trackBy
и добавите элемент в начале массива, различие может понять, что это именно то, что произошло, и добавить новый шаблон в начало цикла при привязке к нему соответствующего элемента.