How to identify which item in FormArray emitted valueChanges event?
Asked Answered
R

6

27

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.
    }
}
Rankle answered 6/12, 2018 at 15:44 Comment(0)
R
23

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);
}
Rankle answered 10/12, 2018 at 23:52 Comment(4)
Awesome! This is exactly what I was looking for.Cucumber
hi, at the addArrayItem() function, why do I get an error when I add : FormGroup to it? Anything that I need to add?Stuppy
may be you are using javascript and not typescript? if so just dont add the :FormGroup or any type annotations.Rankle
In my case, I needed valuechanges on a control in the formgroup. This worked perfectly for me.Loveinidleness
M
31

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: I now think this post is far better: https://indepth.dev/learn-to-combine-rxjs-sequences-with-super-intuitive-interactive-diagrams/)

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);
    });
}
Matterhorn answered 16/12, 2019 at 13:55 Comment(8)
The second map is not available for meTimberwork
The second map is an rxjs operator. ie: import { map, takeUntil } from 'rxjs/operators';Matterhorn
You saved my life, Thanks !Heartburn
@Matterhorn if you had another formArray nested in the first, could you get the index of the formcontrol that changed in second formArray as well? if so, how?Overlying
@Overlying Maybe something like this? watchForChanges() { // cleanup any prior subscriptions before re-establishing new ones this.changesUnsubscribe.next(); const controlChanges = this.formArray.controls.flatMap((control: FormArray, rowIndex: number) => control.controls.flatMap((nestedControl: AbstractControl, nestedRowIndex: number) => nestedControl.valueChanges.pipe( takeUntil(this.changesUnsubscribe), map(value => ({ rowIndex, control, nestedRowIndex, nestedControl, data: value })) ) ) ); to be continued...Matterhorn
merge(...controlChanges).subscribe(changes => { this.onValueChanged(changes); }); }Matterhorn
I figured out a way based off of your answer. Just adding for others in case it is helpful. @MatterhornOverlying
Thanks @Matterhorn I just saw your message. Posted my solution below as well but I may use this as well at some point. Really appreciate the generosity of your time.Overlying
C
24

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);
Cleavers answered 6/12, 2018 at 16:9 Comment(6)
amazing solution.Martini
do you know how to get the index of the said control being changed? @CleaversStitching
@Stitching If you want to get a ref to changed control you can do like this control => control.valueChanges.pipe(mapTo(control)) and you will get the control instance. If you want the exact index, then you can do the same but with index (control, index) => control.valueChanges.pipe(mapTo(index))Cleavers
thank you @Cleavers great help! btw, is have you tried, subscribing directly to the the last formControl from the array of formGroups? and at the same time i need its index.Stitching
How to compare previous & current value with this method?Damage
@Damage you can use pairwise() rxjs operator (control.valueChanges.pipe(pairwise())). This way an array with prev and curr values will be returned.Cleavers
R
23

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);
}
Rankle answered 10/12, 2018 at 23:52 Comment(4)
Awesome! This is exactly what I was looking for.Cucumber
hi, at the addArrayItem() function, why do I get an error when I add : FormGroup to it? Anything that I need to add?Stuppy
may be you are using javascript and not typescript? if so just dont add the :FormGroup or any type annotations.Rankle
In my case, I needed valuechanges on a control in the formgroup. This worked perfectly for me.Loveinidleness
N
3

When binding controls of FormArray to an array in your model, you can pass index or any other id from the model to the event handler:

 <div *ngFor="let m of modelArr; let ix = index" formArrayName="ControlArr">
    <input type="checkbox" [formControlName]="ix" (change)="my_handler($event, ix, m.id)">
 </div>

and in component typescript:

 my_handler(ev: any, ix: number, id: any): void {
    console.log(ix, id);
 }
Niveous answered 17/12, 2020 at 1:9 Comment(0)
O
0

Expanding on @Flyer's answer because I had a nested form control that I need to listen on. Just sharing in case it is helpful to someone else.

Adding some context, I needed to listen to a field on the nested form array, two form arrays in, and update a field if a user makes a change on an inherited entity.

ngOnInit(): void {
    this.initializeMappings();
    this.onUpdateClassMapFormChanges();
  }

  onUpdateClassMapFormChanges() {
    const arrayOfObs = this.classMapFormArray.controls.map((classMapControl: AbstractControl, classMapIndex: number) => {
       const ruleControlsObservables =  classMapControl.get('_rules')['controls'].map((ruleControl: FormControl, ruleIndex: number) => {
          return ruleControl.valueChanges.pipe(map(value => ({classMapIndex: classMapIndex, ruleIndex: ruleIndex, value})));
        });
          return ruleControlsObservables;
        }
    )

    merge(...arrayOfObs.flat()).pipe(filter((control: {classMapIndex: number, ruleIndex: number, value: Record<string, unknown>}) => {
      // only continue if inherited is selected
      return control.value._source === 'INHERITED';
    })).subscribe((changes: {classMapIndex: number, ruleIndex: number, value}) => {
          this.classMapFormArray.at(changes.classMapIndex).get('_rules')['at'](changes.ruleIndex).patchValue({
            _source: 'OVERRIDE'
          }, {emitEvent: false, onlySelf: true});

        });
  }
Overlying answered 19/7, 2023 at 15:32 Comment(0)
U
-1

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.
     ...
}
Undaunted answered 6/12, 2018 at 16:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.