MattDionis MattDionis - 18 days ago 6
Javascript Question

Losing two-way binding when pushing to array inside form (Angular 2)

I'm building a basic CRUD app with Angular 2. One of the form fields is an array of ingredients. I have an

addIngredient
method which pushes a new
Ingredient
to my
ingredients
array. As soon as I click the button which triggers this method the two-way binding appears to be lost.
When looking at the diagnostic data everything appears correct, but the data is lost from the form UI (see gif below):

enter image description here

relevant HTML:

<div *ngFor="let ingredient of newRecipe.ingredients; let i = index; let f = first; let l = last">
<md-input type="text" id="ingredientName" name="ingredientName" [(ngModel)]="ingredient.name" placeholder="ingredient" required></md-input>
<md-input type="text" id="ingredientAmount" name="ingredientAmount" [(ngModel)]="ingredient.amount" placeholder="amount" required></md-input>
<select id="ingredientMeasurement" name="ingredientMeasurement" [(ngModel)]="ingredient.measurement" required>
<option *ngFor="let measurement of measurements" [value]="measurement">{{measurement}}</option>
</select>
<button md-icon-button color="primary" *ngIf="l" (click)="addIngredient()">
<md-icon>add</md-icon>
</button>
<button md-icon-button color="warn" *ngIf="!f" (click)="removeIngredient(i)">
<md-icon>remove</md-icon>
</button>
</div>


relevant code from class:

addIngredient() {
this.newRecipe.ingredients.push(new Ingredient());
}


NOTE: The
div
referenced above appears inside a
form
element. When I move this
div
outside the
form
everything works as expected.

Answer

What is happening here is that the <form> is using input's name properties to synchronise the models' values. In this case it's basically overriding the [ngModel] synchronisation.

What you can do to fix this is make names dynamic:

<div *ngFor="let ingredient of newRecipe.ingredients; let i = index;">
   <md-input type="text" name="ingredientName_{{i}}"   
    [(ngModel)]="ingredient.name" placeholder="ingredient" required>
   </md-input>
</div>

(i.e. name="ingredientName_{{i}}")

You can read more about this in the docs: https://angular.io/docs/ts/latest/api/forms/index/NgModel-directive.html

When using the ngModel within tags, you'll also need to supply a name attribute so that the control can be registered with the parent form under that name.

It's worth noting that in the context of a parent form, you often can skip one-way or two-way binding because the parent form will sync the value for you.