Set value of one control by other controls in Angular ReactiveForms
Tags: angular, rxjs, typescript, javascript
Published: 2021-03-26 13:24:36

While making a form with control which value relates to other controls values, I noticed that it is quite easy to use `combineLatest` operator from RxJS. I am listening only to given controls by names. Additionally, I can set starting values for each of them. 

### The reason
I was working on Angular Custom Control which was containing other controls. The feature which I needed was to calculate a value for one of the controls based on other controls. This is how I solved it.

### Set streams
I want to calculate value only when specific controls were changed, so I set array with name of the controls and starting values.
```javascript
const nameWithStarters = [
    { name: 'quantityKR', value: 0 },
    { name: 'quantity', value: 0 },
    { name: 'priceKR', value: 0 },
    { name: 'hbtPercentage', value: 100 },
];
const valueChangers$ = nameWithStarters.map(({ name, value }) =>
      this.form.get(name).valueChanges.pipe(startWith(value))
);
```
And I am listening for changes using `name` for control selection and value for starting value. The `startWith` [operator](https://rxjs.dev/api/operators/startWith) from RxJs guarantees that each of my controls will have value on subscription.  

### Calculation
To trigger calculation I am using `combineLatest` [function](https://rxjs.dev/api/index/function/combineLatest). It emits when any of the given streams emit and pass values to my simple `calculateTotalKr` function. In the end, it sets the value for my result control. I am adding it to my `subscription` by using `add` method to have the possibility to unsubscribe when the component is destroyed (Avoiding memory leaks).
```typescript
    const sub = combineLatest(valueChangers$)
      .pipe(map((values: ValuesTuple) => calculateTotalKr(values)))
      .subscribe(value => {
        this.form.get('totalKR').setValue(value);
      });
    this.subscription.add(sub);
```

## Full code

```typescript
import {
  Component,
  EventEmitter,
  Input,
  OnDestroy,
  OnInit,
  Output,
} from '@angular/core';
import {
  ControlValueAccessor,
  FormBuilder,
  NG_VALUE_ACCESSOR,
} from '@angular/forms';
import { map, startWith } from 'rxjs/operators';
import { Benefit } from 'src/app/models/benefit';
import { combineLatest, Subscription } from 'rxjs';

type ValuesTuple = [number, number, number, number];

@Component({
  selector: '[app-lines-table-row]',
  templateUrl: './lines-table-row.component.html',
  styleUrls: ['./lines-table-row.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: LinesTableRowComponent,
    },
  ],
})
export class LinesTableRowComponent
  implements ControlValueAccessor, OnInit, OnDestroy {
  @Input() benefitList: Benefit[] = [];
  @Output() benefitRemove = new EventEmitter<void>();
  form = this.formBuilder.group({
    date: [null],
    type: [null],
    performance: [null],
    performanceName: [null],
    quantity: [null],
    quantityKR: [null],
    priceKR: [null],
    hbtPercentage: [100],
    totalKR: [0],
    included: [null],
  });
  private subscription = new Subscription();

  onChange = (value: any) => {};

  onTouched = () => {};

  constructor(private readonly formBuilder: FormBuilder) {}

  ngOnInit(): void {
    this.form.get('performance').valueChanges.subscribe(value => {
      this.selectBenefit(value);
    });

    const sub = this.form.valueChanges.subscribe(value => this.onChange(value));
    this.subscription.add(sub);
    this.setCalculateTotalKRValue();
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }

  writeValue(value: any): void {
    this.form.patchValue(value || null);
  }

  registerOnChange(onChange: any) {
    this.onChange = onChange;
  }

  registerOnTouched(onTouched: any) {
    this.onTouched = onTouched;
  }

  onBenefitRemove(): void {
    this.benefitRemove.emit();
  }

  private selectBenefit(benefitValue: string): void {
    const selectedBenefit = this.benefitList.find(
      ({ value }) => value === benefitValue
    );
    this.form.patchValue({
      type: selectedBenefit.extraField === 'OrdinaryBenefit' ? 'AHT' : 'UHT',
      performanceName: selectedBenefit.text,
    });
  }

  private setCalculateTotalKRValue(): void {
    const nameWithStarters = [
      { name: 'quantityKR', value: 0 },
      { name: 'quantity', value: 0 },
      { name: 'priceKR', value: 0 },
      { name: 'hbtPercentage', value: 100 },
    ];
    const valueChangers$ = nameWithStarters.map(({ name, value }) =>
      this.form.get(name).valueChanges.pipe(startWith(value))
    );
    const sub = combineLatest(valueChangers$)
      .pipe(map((values: ValuesTuple) => calculateTotalKr(values)))
      .subscribe(value => {
        this.form.get('totalKR').setValue(value);
      });
    this.subscription.add(sub);
  }
}

function calculateTotalKr([
  quantityKR,
  quantity,
  priceKR,
  hbtPercentage,
]: ValuesTuple): number {
  return (quantityKR * quantity - priceKR) * (hbtPercentage / 100);
}

```