4

I have a reactive form with over 10 form controls and using subscription on valueChanges observable to detect changes. It works perfectly but output is always the entire form value object(meaning all the form controls and their values). Is there a way to simply get the form control name of the field that changed?

this.form = this.fb.group({
    field1: ['', Validators.required],
    field2: ['', Validators.required],
    field3: ['', Validators.required],
    field4: ['', Validators.required],
    field5: ['', Validators.required],
    field6: ['', Validators.required],
    field7: ['', Validators.required],
    field8: ['', Validators.required],
    field9: ['', Validators.required],
    field10: ['', Validators.required],
    field11: ['', Validators.required],
    field12: ['', Validators.required],
    field13: [{ value: '', disabled: true }, Validators.required]
});

this.form.valueChanges.subscribe(
    result => this.calculateParams(result)
);

calculateParams(result) {
    console.log(result); // giving the entire form.value object
}
dimitri
  • 45
  • 1
  • 1
  • 5

7 Answers7

4

The rxjs way of Eliseo's answer

this.form.valueChanges.pipe(
    startWith(this.form.value),
    pairwise(),
    map(([oldValues, newValues]) => {
        return Object.keys(newValues).find(k => newValues[k] != oldValues[k]);
    }),
).subscribe(key => {
    console.log( key )
});
  • This seems like a nice solution. However, the first time this is triggered, despite the `startWith`, it will always return the first control of the form for me, regardless of which one actually had the change. Second time onwards, it works fine. I'm confused because even the first time, if I compare oldValues and newValues, the only difference is in the control that actually changed its value. – Daniel Saner Mar 31 '21 at 07:50
3

It's a work-around but if store the old values you can do some like

this.old={...this.myForm.value}
this.myForm.valueChanges.subscribe(res=>{
  const key=Object.keys(res).find(k=>res[k]!=this.old[k])
  this.old={...this.myForm.value}
})
Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • This seems to do the trick. I originally though of something similar but hoped that there's maybe a "slicker" solution.. Thanks! – dimitri Jun 24 '19 at 08:21
  • In Angular 6 and Angular 7, there was a different between myform.get('...').value and "res", or between res and myform.value[...], (even I think remember that you could subscribe to (res,old)=>{..}) but in Angular 8 looks like there's no diference. Really I don't know if ther'e a better solution – Eliseo Jun 24 '19 at 08:38
1

You can isolate the formcontrol from the formgroup by using the get method and the access the valueChanges method on it.

this.form.get('formcontrolName').valueChanges().

Note: form.get returns you the AbstractControl, which also has the valueChanges method on it.

KiraAG
  • 773
  • 4
  • 19
  • right, but then in order to subscribe to all values I would have to subscribe to each of them separately(for each form control) and I want to avoid that because I'll have forms with a lot more fields than this. – dimitri Jun 24 '19 at 03:08
  • you can extract out the config for `fb.group` so that you can use `Object.keys` on it and subscribe to each field in a forEach loop – Phu Ngo Jun 24 '19 at 03:11
  • @dimitri Can you try this `this.form.valueChanges().pipe(map(form => form.controls), filter(control => control.dirty)).subscribe()`; – KiraAG Jun 24 '19 at 03:22
  • 1
    @PhuNgo wouldn't that many subscriptions(e.g. 50+) on a single component at some point be a memory bottleneck? – dimitri Jun 24 '19 at 03:26
  • @KiraAG - not getting any results(`Cannot read property 'dirty' of undefined`). What's the chain of thoughts here? – dimitri Jun 24 '19 at 03:36
  • The idea is to get the form controls and filter them using the property `dirty`, which filters out the controls whose values has been changed in the UI. – KiraAG Jun 24 '19 at 03:38
  • Ok Sorry, I misread `form.controls` returns array, it actually return you an object. Can you try this `this.form.valueChanges().pipe(map(form => Object.values(form.controls)), filter(control => control.dirty)).subscribe()`. Now you will get an array of `form controls`. – KiraAG Jun 24 '19 at 03:43
  • @dimitri subscribe to each field in the forEach loop, not to the whole group. But you made a good point with the bottleneck issue – Phu Ngo Jun 24 '19 at 03:48
  • @KiraAG - but wouldn't that just return all "dirty" values, meaning all the fields that have been "touched"? What if I decide to switch back between fields and continue changing them? – dimitri Jun 24 '19 at 03:53
  • Isn’t that what you wanted? – KiraAG Jun 24 '19 at 03:55
  • @KiraAG sorry if question was not clear, but no - just a single field which value was updated last – dimitri Jun 24 '19 at 04:00
  • Oh , I think this is a bit complex requirement, for which may you need pairwise operator. – KiraAG Jun 24 '19 at 04:06
1

Haven't fully tested the code but the idea is to pair the controls and their key then listen to valueChanges on each control simultaneously and return object key instead of value (and of course you can map both value and key to the output)

const fields={
    field1: ['', Validators.required],
    field2: ['', Validators.required],
    field3: ['', Validators.required],
    field4: ['', Validators.required],
    field5: ['', Validators.required],
    field6: ['', Validators.required],
}

zip(
 from(Object.values(fb.group(fields).controls)),
 from(Object.keys(fields))
).pipe(mergeMap([control,key])=>control.valueChanges.pipe(mapTo(key)))
Fan Cheung
  • 10,745
  • 3
  • 17
  • 39
1

You could subscribe to each change individually.

for (const controlProperty in this.myForm.controls) {
    if (this.myForm.controls.hasOwnProperty(controlProperty)) {
      this.myForm.controls[controlProperty].valueChanges.pipe(untilDestroyed(this)).subscribe(result => {
        console.log(controlProperty + " is now ", result);
      });
    }
}

This just felt more angular-y to me.

Shawn Palmer
  • 321
  • 3
  • 5
0

Use valueChanges for each control after adding control dynamically to FormGroup.

const ct = {key: 'myControl'};    
this.myForm.addControl(ct.key, new FormControl('', Validators.required))
this.myForm.controls[ct.key].valueChanges
.subscribe(d => {
    // here ct will be available as closure , so that we can access every form control here and do operation accordingly
    console.log(d, ct.key);
})

Here ct object will be available inside subscribe as a closure.

shan22
  • 131
  • 8
0

The method from @alex-walker (https://stackoverflow.com/a/64830665/134120) seems to be the best, but it only works well on flat forms, with simple key-value pairs. If you have nested forms, with FormArrays of FormGroups, the key property in that method will only return the top-level key of the form, which is not very useful if you want e.g. to indicate to the user the particular input field that triggered the event.

So I combined this method with a deep object diff method described here: https://stackoverflow.com/a/50278133/134120

To get something like this:

this.form.valueChanges
    .pipe(
        startWith(this.form.value),
        pairwise(),
        map(([oldValues, newValues]) => {
            return bdiff(oldValues, newValues);
        })
    )
    .subscribe(keys => {
        console.log(keys);
    });

bdiff(a: Record<string, any>, b: Record<string, any>): string[] {
    return _reduce(
        a,
        (res, val, key) =>
            res.concat(
                (_isPlainObject(val) || Array.isArray(val)) && b
                    ? this.bdiff(val, b[key]).map(x => key + (key.trim ? '' : ']') + (x.search(/^\d/) ? '.' : '[') + x)
                    : !b || val !== b[key]
                    ? [key + (key.trim ? '' : ']')]
                    : []
            ),
        []
    );
}

Which will return an array of nested keys such as keys = ['fieldgroup2.fieldArray[3].myNestedField'] which you should easily access and set as invalid with form.get(keys[0]).valid.

AsGoodAsItGets
  • 2,886
  • 34
  • 49