Sandeep Gupta
Sandeep Gupta

Reputation: 7250

ngModel's unexpected behavior inside Angular Form

The following code will generate two inputs with same value bye (instead of hello and bye). It would be great if someone could (theoretically) explain this behaviour and tell the exact cause.

<form>
  <div *ngFor="let item of ['hello', 'bye'];">
    <input name="name" [(ngModel)]="item">
  </div>
</form>

enter image description here

Edit: To explain my question better:

The reason couldn't be that because they are bind to the same object, they will have identical value. If that is so the following case would have the same value for both of inputs, which is obviously not the case.

app.component.html

<form>
  <div *ngFor="let item of arr;">
    <input name="name" [(ngModel)]="item">
  </div>
</form>

app.component.ts

  arr = [1,4]

  ngOnInit(){
    setTimeout(()=>{
      this.arr[1] = 5;
    });
  }

enter image description here

Please note: I think I have explained my question properly and also why I think @DeborahK's solution doesn't seem fit to me. I am looking for a reason for such behavior. And not the workaround. Also, I know that changing name in each input would make it work fine. So please stop suggesting that.

Upvotes: 0

Views: 437

Answers (3)

Martin Parenteau
Martin Parenteau

Reputation: 73731

There seems to be a combination of two problems in your code sample:

  1. The two inputs have the same name, which causes them to share the same FormControl
  2. Each input element is removed and recreated when it is updated on change detection. If the other value was not modified, the corresponding input element is not recreated. This seems to cause a desynchronisation with the FormControl, and we see different values in the two fields.

To illustrate the last point, you can force the two inputs to be recreated on change detection by modifying both of them in code:

changeValues() {
  this.arr[0] = 2;
  this.arr[1] = 3;
}

You can see in this stackblitz that both inputs have the same content after the update.


The destruction/creation of bound input elements in an ngFor loop can be prevented with the help of a trackBy method, to track the array elements by their index instead of tracking them by their value. You can see in this stackblitz that the two input elements correctly share the same FormControl.

<div *ngFor="let item of arr; trackBy: trackByFn">
  <input name="name" [ngModel]="item">
</div>
trackByFn(index, value) {
  return index;
}

In the end, the correct behavior can be obtained with 3 changes to the original code:

  1. Give each input a unique name
  2. Prevent input element destruction/creation with a trackBy method (by index)
  3. For two-way binding, bind the array value by its index instead of binding to the loop variable item
<div *ngFor="let item of arr; let i = index; trackBy: trackByFn">
  <input name="name_{{i}}" [(ngModel)]="arr[i]">
</div>
trackByFn(index, value) {
  return index;
}

You can see this stackblitz for a demo.


Data flow in template-driven forms (model to view)

The ngModel directive updates the FormControl when the bound data is modified, by calling FormControl.setValue:

ngModel source code:

ngOnChanges(changes: SimpleChanges) {
  ...    
  if (isPropertyUpdated(changes, this.viewModel)) {
    this._updateValue(this.model);
    this.viewModel = this.model;
  }
}

private _updateValue(value: any): void {
  resolvedPromise.then(
      () => { this.control.setValue(value, {emitViewToModelChange: false}); });
}

and you can see that FormControl.patchValue also calls setValue:

FormControl source code:

patchValue(value: any, options: {
  onlySelf?: boolean,
  emitEvent?: boolean,
  emitModelToViewChange?: boolean,
  emitViewToModelChange?: boolean
} = {}): void {
  this.setValue(value, options);
}

Upvotes: 0

DeborahK
DeborahK

Reputation: 60518

Here is some further explanation as to why the answer here is to have a unique name. And this solution is not a work around. It is just the way it works.

When you use template-driven forms, which you are when you use ngModel, then Angular automatically builds a data structure to hold all of the form information. This includes state information (dirty, touched, etc) and the form values. It holds this information based on the CONTROL NAME!

So if your names are the same, they are in the data structure as ONE ELEMENT and cannot then have two values.

enter image description here

You can view this data structure yourself if you define a template reference variable for the form:

<form #myForm="ngForm">
  <div *ngFor="let item of ['hello', 'bye'];">
    <input name="name" [(ngModel)]="item">
  </div>
  <div>{{ myForm.value | json }}</div>
</form>

I just did a stackblitz to demonstrate your array example to show that it is still only one element with one value:

https://stackblitz.com/edit/angular-xjyslr

Upvotes: 0

Chellappan வ
Chellappan வ

Reputation: 27303

Name attribute sould be unique

 <form>
      <div *ngFor="let item of ['hello', 'bye'];let i =index">
        <input  name="{{i}}" [(ngModel)]="item">
      </div>
    </form>

Upvotes: 3

Related Questions