angularangular-forms

Deleting item of a FormArray that was used in a *ngFor throws an error


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" :

after removing

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>

Solution

  • 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 FormControls 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.