In Angular
, is there a way to identify which FormGroup
/FormControl
in a dynamicFormArray
emitted the valueChanges
event?
My FormArray
is dynamic. It starts out empty and users could add a FormGroup
to the FormArray
by clicking a button.
When valueChanges, I need to re-validate the control. Since I dont know which control emitted the event, I loop through the entire FormArray
and validate all FormGroup
/FormControl
even though only one control changed - and this is every time when anything in the array changes. How can I avoid doing this?
this.myFormArray
.valueChanges
.subscribe(data => this.onValueChanged(data));
onValueChanged(data?: any): void {
// the data I receive is an entire form array.
// how can I tell which particular item emitted the event,
// so I don’t need to loop through entire array and run validation for all items.
for (let control in this.myFormArray.controls) {
// run validation on each control.
}
}
I resolved this issue by adding a formControl
(named groupIndex) in the formGroup
to track the index and subscribing to the valueChanges
at the formGroup
level instead of formArray
level. On valueChanges
event, I could then access the formControl
that stored the current index.
this.myMainFormGroup = this.myFormBuilder.group({
// other formControls
myFormArray: this.myFormBuilder.array([this.addArrayItem()])
});
// this method will be called every time the add button is clicked
addArrayItem(): FormGroup {
const itemToAdd = this.myFormBuilder.group({
// dont forget add input control for groupIndex in html for this. It will give error otherwise.
// i made the input control hidden and readonly
groupIndex:"",
firstName:["", [validator1, validator2]]
//other formControls
});
const myFormArray = <FormArray>this.myMainForm.get("myFormArray");
//set groupIndex
itemToAdd.get("groupIndex").patchValue(myFormArray.length -1);
//subscribe to valueChanges
itemToAdd.valueChanges
.debounceTime(200)
.subscribe(data => this.onValueChanged(data));
myFormArray.push(itemToAdd);
}
onValueChanged(data?: any): void {
const groupIndex = data["groupIndex"];
const myChangedGroup = <FormArray>this.myMainForm.get("myFormArray").controls[groupIndex];
// now I have hold of the group that changed without having to iterate through the entire array.
// run through the custom validator
this.generalValidator(myChangedGroup);
}
You can try something like this, but I am not sure that it will work
merge(...this.myFormArray.controls.map(control => control.valueChanges))
.subscribe(this will be one of your form controls' value);
Not in my PC to test, but maybe using the caller property of a function might guide you in the direction you want. Although this property is not recommended:
This feature is non-standard and is not on a standards track. Do not use it on production sites facing the Web: it will not work for every user. There may also be large incompatibilities between implementations and the behavior may change in the future.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/caller
rough example:
this.myFormArray
.valueChanges
.subscribe(onValueChanged);
onValueChanged(data?: any): void {
var whoYouGonnaCall = onValueChanged.caller.caller...caller;
...
}
Update
Another option is to store the last form value and use something like lodash for comparing the properties.
var lastFormValue = this.myFormArray.value; // maybe in an init function
this.myFormArray
.valueChanges
.subscribe(onValueChanged);
onValueChanged(data?: any): void {
var diff = _.omitBy(data, function(v, k) {
return lastFormValue[k] === v;
});
this.lastFormValue = this.myFormArray.value; // Update for future requests
// diff will contain the properties if the form that changed.
...
}
To build on epsilon's answer, which collects an array of valueChanges observables and merges them - you can also pipe the value changes thru a pipe, which adds necessary context to the changes stream via map.
merge(...this.formArray.controls.map((control: AbstractControl, index: number) =>
control.valueChanges.pipe(map(value => ({ rowIndex: index, value })))))
.subscribe(changes => {
console.log(changes);
});
Output:
{
rowIndex: 0
value: {
<current value of the changed object>
}
}
Note that the first call to map (on controls) is on an array. The second map (in the pipe) is an RxJs map. I really like this website to help get these operators straight and imagine what these streams of events look like: https://rxmarbles.com/#map
EDIT:
Because I was watching a FormArray that could be modified by the user via the add/delete buttons, I added a changesUnsubscribe subject and reference that in the takeUntil. This allows me to discard the old set of watches and setup new ones when the list changes. So now I call watchForChanges() when items are added or removed from the list.
changesUnsubscribe = new Subject();
...
watchForChanges() {
// cleanup any prior subscriptions before re-establishing new ones
this.changesUnsubscribe.next();
merge(...this.formArray.controls.map((control: AbstractControl, index: number) =>
control.valueChanges.pipe(
takeUntil(this.changesUnsubscribe),
map(value => ({ rowIndex: index, control: control, data: value })))))
.subscribe(changes => {
this.onValueChanged(changes);
});
}
If anyone is trying to convert input type="number" to an integer with an array of controls using angular dynamic forms.
This snipped returns the actual control that was changed.
const formArray = this.parentFormGroup.controls['obligationList'] as FormArray
formArray.controls.forEach(control => {
control.valueChanges
.debounceTime(800)
.distinctUntilChanged()
.takeUntil(this.ngUnsubscribe)
.subscribe(() => control.patchValue({amountPayable: parseInt(control.value['amountPayable'], 10)}, {emitEvent : false}))
})