Angular集合更新时模型绑定丢失问题求助
我开发了一个简易食谱计算器,支持用户向列表中添加食材并设置数量。食材列表为数组,用户点击“Add ingredient”按钮时,应创建一个新的空食材项,供用户选择食材和设置数量。
初始状态下功能正常:应用加载后添加第一个食材时,可正常选择食材并设置数量。但添加第二个食材(向数组中新增空项)后,表单似乎丢失了与模型的绑定。不过查看模型的JSON输出显示正常,无法定位问题所在。
添加第一个食材时的效果:
可以看到输入框绑定到了正确的字段。
添加第二个食材后:
JSON显示正常,第一个食材信息保留,数组中新增了第二个项,但输入框重置为空值。
组件模板代码
<form #recipesForm="ngForm" (ngSubmit)="onSubmit(recipesForm)" class="qc-panel" *ngIf="showRecipesForm()"> <div class="mb-3"> <label for="name" class="form-label">Recipes name</label> <input type="text" class="form-control" id="name" required [(ngModel)]="model.name" name="name" #name="ngModel"> <div [hidden]="name.valid || name.pristine" class="alert alert-danger"> Name is required </div> </div> <ul class="mb-3 ingredients-list"> <li *ngFor="let ingredient of model.ingredients; let i = index; trackBy: trackByIndex" class="ingredients-list__item"> <select class="form-select qc-input--lg" [(ngModel)]="model.ingredients[i].definition" name="ingredient"> <option [value]="{}" disabled>Please select</option> <option *ngFor="let definition of definitions" [ngValue]="definition" [disabled]="disableOption(definition)">{{ definition.name }}</option> </select> <div class="input-group qc-input--md" [hidden]="!showQuantity(ingredient)"> <input type="number" class="form-control" [(ngModel)]="model.ingredients[i].quantity" name="quantity" /> <span class="input-group-text">{{ ingredient.definition.measurement }}</span> </div> <a [routerLink]="[]" (click)="removeIngredient(model, ingredient)" class="qc-action"><fa-icon [icon]="faTrash"></fa-icon></a> </li> </ul> <button type="button" class="btn btn-primary d-block" [disabled]="disableAdd()" (click)="addIngredient()">Add ingredient</button> <button type="submit" class="btn btn-primary d-block mt-5" [disabled]="disableSave()">Save recipe</button> <div [hidden]="!showRecipeExists || recipesForm.pristine" class="alert alert-danger mb-3"> A recipe already exists with this name </div> </form>
组件后端代码
import { Component } from '@angular/core'; import { NgForm } from '@angular/forms'; import { faTrash } from '@fortawesome/free-solid-svg-icons'; import { Definition } from '../definition'; import { DefinitionService } from '../definition.service'; import { Ingredient } from '../ingredient'; import { Recipe } from '../recipe'; import { RecipeService } from '../recipe.service'; @Component({ selector: 'app-recipes', templateUrl: './recipes.component.html', styleUrls: ['./recipes.component.scss', '../../assets/sass/shared.scss'] }) export class RecipesComponent { definitions: Definition[] = []; recipes: Recipe[] = []; constructor(private definitionService: DefinitionService, private recipeService: RecipeService) { } // Icons faTrash = faTrash; model: Recipe = { name: '', ingredients: [] as Ingredient[] }; showRecipeExists = false; ngOnInit(): void { this.getDefinitions(); this.getRecipes(); } showRecipesForm(): boolean { return this.definitions.length > 0; } showRecipesTable(): boolean { return this.recipes.length > 0; } disableAdd(): boolean { if (this.definitions.length === this.model.ingredients.length) return true; if (this.model.ingredients.length > 0) { let count = 0; for (let i = 0; i < this.model.ingredients.length; i++) { if (Object.keys(this.model.ingredients[i].definition).length === 0) count++; } if (count !== 0) return true; } return false; } disableSave(): boolean { if (this.model.ingredients.length === 0) return true; let ingredientNotSet = false; for (let i = 0; i < this.model.ingredients.length; i++) { if (Object.keys(this.model.ingredients[i].definition).length === 0) return true; } if (ingredientNotSet) return true; return false; } disableOption(definition: Definition): boolean { for (let i = 0; i < this.model.ingredients.length; i++) { if (this.model.ingredients[i].definition.name === definition.name) return true; } return false; } showQuantity(ingredient: Ingredient): boolean { if (Object.keys(ingredient.definition).length !== 0) return true; return false; } addIngredient(): void { let ingredient: Ingredient = { quantity: 0, definition: {} as Definition } this.model.ingredients.push(ingredient); } removeIngredient(recipe: Recipe, ingredient: Ingredient): void { // Remove the ingredient from the array for (let i = recipe.ingredients.length - 1; i >= 0; i--) { if (Object.keys(ingredient.definition).length === 0 || recipe.ingredients[i].definition.name === ingredient.definition.name) { recipe.ingredients.splice(i, 1); } } } getDefinitions(): void { this.definitionService.getDefinitions() .subscribe(definitions => { this.definitions = definitions }); } getRecipes(): void { this.recipeService.getRecipes() .subscribe(recipes => { this.recipes = recipes }); } trackByIndex(index: number, obj: any): any { return index; } onSubmit(form: NgForm): void { if (this.recipeService.existsWithName(form.value as Recipe)) { this.showRecipeExists = true; return; } this.recipeService.addRecipe(form.value as Recipe) .subscribe(recipe => { this.recipes.push(recipe); }); form.resetForm({ name: '', ingredients: [] as Ingredient[] }); this.getRecipes(); } clearRecipes(): void { if (confirm('Are you sure you want to clear all recipes?')) { this.recipeService.clearRecipes(); this.getRecipes(); } } removeDefinition(definition: Definition) { this.definitionService.removeDefinition(definition); this.getDefinitions(); } removeRecipe(recipe: Recipe) { this.recipeService.removeRecipe(recipe); this.getRecipes(); } }
请问有人能指出问题所在吗?谢谢
问题根源
Angular模板驱动表单中,每个表单控件的name属性必须唯一。你当前的select和input控件的name都是固定值(name="ingredient"、name="quantity"),当通过*ngFor循环生成多个食材项时,所有控件的name完全重复,导致Angular无法正确区分不同的表单控件,进而破坏ngModel的绑定关系。
修复步骤
修改模板中select和input的name属性,结合循环索引i生成唯一名称:
- 修改select的name:
<select class="form-select qc-input--lg" [(ngModel)]="model.ingredients[i].definition" name="ingredient-{{i}}">
- 修改quantity输入框的name:
<input type="number" class="form-control" [(ngModel)]="model.ingredients[i].quantity" name="quantity-{{i}}" />
修改后的完整食材项模板:
<li *ngFor="let ingredient of model.ingredients; let i = index; trackBy: trackByIndex" class="ingredients-list__item"> <select class="form-select qc-input--lg" [(ngModel)]="model.ingredients[i].definition" name="ingredient-{{i}}"> <option [value]="{}" disabled>Please select</option> <option *ngFor="let definition of definitions" [ngValue]="definition" [disabled]="disableOption(definition)">{{ definition.name }}</option> </select> <div class="input-group qc-input--md" [hidden]="!showQuantity(ingredient)"> <input type="number" class="form-control" [(ngModel)]="model.ingredients[i].quantity" name="quantity-{{i}}" /> <span class="input-group-text">{{ ingredient.definition.measurement }}</span> </div> <a [routerLink]="[]" (click)="removeIngredient(model, ingredient)" class="qc-action"><fa-icon [icon]="faTrash"></fa-icon></a> </li>
为什么这样能解决问题?
Angular模板驱动表单依赖name属性来追踪每个控件的状态和绑定关系。当多个控件使用相同的name时,Angular会将它们视为同一个控件,导致后续新增的控件覆盖之前的绑定上下文,表现为输入框重置、绑定失效的现象。通过在name中加入索引i,确保每个控件拥有唯一标识,Angular就能正确维护每个食材项的表单绑定。
内容的提问来源于stack exchange,提问作者sterix24

