In the "learning angular no nonsense" book, there is an example of how to use FormArray that I try to improve. This example uses a dynamic form based on a cart containing products, I try to add the possibility to remove some products (by using removeAt method from the FormArray collection) that are in the cart but I get an error.
Cart.component.html, when a product is removed an error ("ERROR TypeError: ctx_r1.cart2[i_r4] is undefined") is triggered at {{ cart2[i].name }} and (click)="removeProduct(i)" lines :
<form [formGroup]="cartForm2" (ngSubmit)="validateCart()">
<div
formArrayName="products"
*ngFor="let product of cartForm2.controls.products.controls; let i = index"
>
<label>{{ cart2[i].name }} : </label>
<input type="number" [formControlName]="i" />
<button type="button" (click)="removeProduct(i)">Remove</button>
<span *ngIf="product.touched && product.hasError('required')">
The field is required</span
>
<span *ngIf="product.touched && product.hasError('min')">
The field can't be lower than 1</span
>
</div>
<p>cartForm.value : {{ cartForm2.value | json }}</p>
<div>
<button type="submit" [disabled]="!cartForm2.valid">Validate</button>
</div>
</form>
cart.component.ts, get the cart from the service and create a control for each product that is in the cart. The removeProduct method will delete the product in the Cart Service,update cart2 property with the last reference, remove the control by using the good index :
cartForm2 = new FormGroup({
products: new FormArray<FormControl<number>>([]),
});
cart2: Product[] = [];
ngOnInit(): void {
this.cart2 = this.cartService.cart;
this.cart2.forEach(() => {
this.cartForm2.controls.products.push(
new FormControl(1, {
nonNullable: true,
validators: [Validators.required, Validators.min(1)],
})
);
});
}
removeProduct(index: number) {
this.cartService.deleteProduct(index);
this.cart2 = this.cartService.cart
this.cartForm2.controls.products.removeAt(index);
console.log(JSON.stringify(this.cart2));
}
cart.service.ts, is used to store and share between components the products that have been bought :
cart: Product[] = [];
constructor() {}
addProduct(product: Product) {
this.cart.push(product);
}
deleteProduct(index: number) {
this.cart.splice(index, 1);
}
After removing a product, I get
ERROR TypeError: ctx_r1.cart2[i_r4] is undefined"
:
Any idea ?
Edit : Problem was solved using FormGroup
this.cart2.forEach((product) => {
this.cartForm2.controls.products.push(
new FormGroup({
name: new FormControl(product.name),
quantity: new FormControl(1, {
nonNullable: true,
validators: [Validators.required, Validators.min(1)],
}),
})
);
});
<label>{{ product.controls["name"].value }}</label>
You remove from cart2
before the controls
array, but iterate the controls. That means that if the template is rendering during those removal lines, the controls
array will be longer than cart2
, but i
is limited by the controls' length, so it may go out of bounds when indexing cart2.
If you don't care about the deleted entry being present briefly longer, you could make the access cart2[i]?.name
. Since the control is about to be removed anyway, this should show nothing and prevent the out-of-bounds access attempt.
You could also try to do away with the "parallel arrays" you're using. The controls
array could be turned into a array of FormGroups
instead of FormControl
s where each group is a product, and all required information (like the name
) is added to the inner FormGroups
. This may not be appropriate though in a case like this where the name
is (presumably) not a part of the form itself.