ngFor + ngModel:如何将值移至要迭代的数组?

yan*_*kee 14 javascript angular angular-forms

我有一个元素数组,用户不仅可以编辑,还可以添加和删除完整的数组元素。除非我尝试在数组的开头添加一个值(例如使用unshift),否则此方法效果很好。

这是证明我的问题的测试:

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']);
    });
});
Run Code Online (Sandbox Code Playgroud)

前两个测试通过得很好。但是,第三次测试失败。在第三个测试中,我尝试在输入之前加上“ z”,这是成功的,但是第二个输入也显示“ z”,但不应输入。

(请注意,网络上存在成百上千个类似的问题,但在其他情况下,人们只是没有唯一的name属性,他们也只是在追加而不是在前面)。

为什么会发生这种情况,我该怎么办?

注意事项 trackBy

到目前为止,答案只是“使用trackBy”。但是trackBy的文档指出:

默认情况下,更改检测器假定对象实例标识可迭代对象中的节点

由于我没有提供明确的trackBy-Function,这意味着应该按角度跟踪角度,这(在上述情况下)绝对正确地标识了每个对象,并且符合文档的预期。

Morphyish的回答基本上指出,通过身份跟踪的功能已损坏,并建议使用id-Property。起初这似乎是一个解决方案,但后来证明这只是一个错误。使用id属性表现出与我上面的测试完全相同的行为。

penleychan的答案按索引进行跟踪,这导致角度思考,在我取消移动值后,角度认为实际上我推送了一个值,而数组中的所有值恰好都已更新。它可以解决该问题,但是它违反了跟踪跟踪合同,并且违背了按功能跟踪的目的(减少DOM中的流失)。

Mor*_*ish 7

编辑

经过进一步调查,该问题实际上并非来自ngFor。这是输入的ngModel使用name属性。

在循环中,name使用数组index生成属性。但是,当在数组的开头放置一个新元素时,我们突然有了一个同名的新元素。

这可能会导致多个ngModel内部观察相同输入的冲突。

在数组的开头添加多个输入时,可以进一步观察到此行为。最初使用相同name属性创建的所有输入都将采用要创建的新输入的值。无论它们各自的值是否更改。

要解决此问题,您只需要给每个输入一个唯一的即可name。通过使用唯一的id,如下面的示例

<input [name]="'elem' + item.id" [(ngModel)]="item.value">
Run Code Online (Sandbox Code Playgroud)

或者使用唯一的名称/ id生成器(类似于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']);
    });
});
Run Code Online (Sandbox Code Playgroud)

尽管您发表了评论,但这不是解决方法。trackBy是针对使用类型(以及表演,但两者都链接在一起)制作的。

如果您想自己看看,可以在这里找到ngForOf指令代码,但这是它的工作原理。

ngForOf指令正在对数组进行区分以确定所进行的修改,但是,如果没有trackBy传递特定的函数,则将进行软比较。这对于简单的数据结构(例如字符串或数字)很好。但是,当您使用时Objects,它可能会很快变得古怪。

除了降低性能外,缺少对数组内部项目的清晰标识可能会迫使数组重新渲染整个元素。

但是,如果该ngForOf指令能够清楚地确定哪个项目已更改,删除了哪个项目以及添加了哪个项目。它可以使其他所有项目保持不变,根据需要从DOM中添加或删除模板,并仅更新需要删除的模板。

如果添加trackBy函数并在数组的开头添加项目,则差异可以意识到这正是发生的情况,并在循环的开头添加新模板,同时将相应的项目绑定到模板。


pen*_*han 6

找出问题。使用trackBy

问题是如果值更改,则差异报告更改。因此,如果默认函数返回对象引用,则如果对象引用已更改,则它将与当前项目不匹配。

关于trackBy /sf/answers/3177387321/的说明

https://stackblitz.com/edit/angular-testing-gvpdhr