3
votes

The component model:

private SomeArray = [{ key: "Initial" }];

User can add/remove items dynamically:

addField() {
    this.SomeArray.push({ key: Math.random().toString() });
}

removeField(index: number) {
    this.SomeArray.splice(index, 1);
}

Template markup:

 <div class="col-xs-12">
     <button (click)="addField()" type="button">Add</button>
 </div>

 <div *ngFor="let field of SomeArray; let i = index;">
     <input [(ngModel)]="field.key" #modelField="ngModel" [name]=" 'SomeArray['+i+'].key' " type="text" class="form-control" required />
     <div [hidden]="modelField.pristine || !(modelField.errors && modelField.errors.required)" class="alert alert-danger">
        Required error
     </div>

    <button (click)="removeField(i)" class="btn btn-danger">Remove</button>
 </div>

This works untill user removes any item from SomeArray. If I add some two items initially: enter image description here

and remove the one with 1 index:

enter image description here

then after adding another item Angular treat it as item has both 0 and 1 index (the new item "occupies" both two inputs):

enter image description here

(item with key 0.1345... is not displayed)

It's worth to noting items of SomeArray are as expected, but data binding fails. What can be the reason of it?

Update: Thanks to the comments of @Stefan Svrkota and @AJT_82 it's known for me the issue can be resolved by adding [ngModelOptions]="{standalone: true}" to the needed input. But I couldn't stop thinking about the reason of the issue in my cause, without setting standalone option (there is unique value for each name attribute so it's excepted nothing wrong here).

Finally I have found that behavior occurs when input elements are into <form> tag only - Plunker sample here (enclosing of template with form tag is the reason that issue).

Any ideas of this behavior?

1
@Stefan Svrkota solved, thank) - Mergasov
@AJT_82 I think [name]=" 'SomeArray['+i+'].key' " returns unique name value (with index of iteration i)? am I wrong? - Mergasov
@Mergasov Nevermind what I was babbling about :D Yeah, ngModelOptions is a good solution here since I guess you don't really need the name attribute here in the form (I assume it is), as you are using two-way-binding. - AJT82

1 Answers

11
votes

The reason why it happens is ngFor mixes name properties when you delete some item.

When you use ngModel inside form each ngModel control will be added to form controls collection.

Let's see what happens if we have added three items and clicked on Remove the second

1) Step1 - SomeArray[1].key exists in collection controls enter image description here

2) Step2 - SomeArray[1].key has been removed from controls collection

enter image description here

3) Step3 - Html looks like

enter image description here

4) Step4 We are adding a new item

enter image description here

So formGroup returns existing item.

How we can solve it?

1) Don't wrap our controls in form tag

2) Add ngNoForm attribute to form

<form ngNoForm>

3) Use

[ngModelOptions]="{standalone: true}

With all three solutions above:

  • We can remove [name] property binding

  • We can't use the built in Form group validation

4) Use trackBy for ngFor

template.html

<div *ngFor="let field of SomeArray; let i = index; trackBy: trackByFn">

component.ts

trackByFn(i: number) {
  return i;
}

Plunker Example

This way our built in form will work properly