Search code examples
angulartypescriptsubscriptionangular-reactive-formsformarray

Angular Reactive Form - Subscribe to instances of Form Control in Form Array to get total value


I am using Angular Reactive Forms to iterate through an array of values I would like to have a total field after the Form Array that updates on changes of the Form Array control values.

Sample data:

  primaryData = {
    products: [
      {
        name: 'test-product',
        unmoved: 21,
        moved: 18 
      },
      {
        name: 'another-product',
        unmoved: 18,
        moved: 42
      }
    ]
  }

I am creating the Reactive Form Controls and Array as follows:

  setPrimaryQuantities() {
    const control = <FormArray>this.primaryForm.controls.quantities;
    this.primaryData.products.forEach(product =>
      control.push(this.fb.group({
        name: [product.name, Validators.required],
        unmoved: [product.unmoved, Validators.required],
        moved: [product.moved, Validators.required]
      }))
    )
  }

  ngOnInit() {
    this.primaryForm = this.fb.group({
      quantities: this.fb.array([]),
      unmovedTotal: '',
      movedTotal: ''
    })
    this.setPrimaryQuantities();
  }

What is the best way to have my unmovedTotal and movedTotal Controls update based on the changes in the Array controls. Here's a StackBlitz demonstrating my structure.


Solution

  • The totals you're looking for shouldn't be in the form in my opinion. It's disabled without any condition in your Typescript and I can't think of a use case where you'd edit the totals directly. It's just a computed value out of the form.

    That said, a nice thing would be to have an observable of type:

    total$: Observable<{ moved: number, unmoved: number }>
    

    To do that, as you've used a reactive form, it's pretty straight forward :)

    this.total$ = this.primaryForm.valueChanges.pipe(
      startWith(this.primaryForm.value),
      map(f => f.quantities.reduce(
        (acc, q) =>
        ({
          moved: acc.moved + q.moved,
          unmoved: acc.unmoved + q.unmoved
        }),
        { moved: 0, unmoved: 0 }
      ))
    );
    

    Every time the form changes, you want to get the new totals. When you want to produce a new value (totals) out of an initial one (values of the form), use the map operator.

    On the form, we want to loop over the quantities and produce a sum for both moved and unmoved properties. So here we cannot use map (the basic JS map, not the one from RXJS) because it'd produce an array. For that, we can use reduce (once again the one from pure JS, no RXJS here).

    Now that we've got our observable, we just need to use it into the view. In order to avoid two subscriptions, we can use ngIf with the as syntax:

    <tr *ngIf="total$ | async as total">
      <td>Totals</td>
      <td>{{ total.unmoved }}</td>
      <td>{{ total.moved }}</td>
    </tr>
    

    Here's a complete example based on your initial Stackblitz example:

    https://stackblitz.com/edit/angular-kbk3zm?file=src%2Fapp%2Fapp.component.ts