How to get FormControlName of the field which value changed in Angular reactive forms

Viewed 12762

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
}
7 Answers

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

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

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.

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

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.

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.

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.

Related