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))
);
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);
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);
}
Top comments (0)