DEV Community

Tomasz Flis
Tomasz Flis

Posted on

Set value of one control by other controls in Angular ReactiveForms

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.

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))
);
Enter fullscreen mode Exit fullscreen mode

And I am listening for changes using name for control selection and value for starting value. The startWith operator from RxJs guarantees that each of my controls will have value on subscription.

Calculation

To trigger calculation I am using combineLatest function. 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).

    const sub = combineLatest(valueChangers$)
      .pipe(map((values: ValuesTuple) => calculateTotalKr(values)))
      .subscribe(value => {
        this.form.get('totalKR').setValue(value);
      });
    this.subscription.add(sub);
Enter fullscreen mode Exit fullscreen mode

Full code

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);
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)