ashishvijay_
ashishvijay_

Reputation: 35

How to get the name of FormControl which got updated using valueChanges?

In a form group that is dynamically generated with different FormControl names, valueChanges() is emitting the whole array. How we can identify the exact FormControl name which got updated?

Upvotes: 3

Views: 1566

Answers (3)

Flavien Volken
Flavien Volken

Reputation: 21309

There is no built-in way. But you can use a helper as follows:

import { DestroyRef, inject } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms';
import { merge, of, tap } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

/**
 *  Generic Type to extract the value of FormGroup
 * */
type FormGroupValue<T extends FormGroup> = ReturnType<T['value']>;

export class ControlHelper {
    /**
     * Constructor for ControlHelper.
     * @param destroyRef - Reference for managing subscriptions to avoid memory leaks.
     * Note: To prevent memory leaks, avoid using a destroyRef of a root service.
     * If the control observer is created within a component, a directive, or a service provided by a component/directive, it's fine.
     */
    constructor(private destroyRef: DestroyRef = inject(DestroyRef)) {}

    /**
     * Watches the control's value, emitting the current value immediately and then on subsequent changes.
     * @param control - The form control to watch.
     * @param callback - The function to call with the control's value and the control itself.
     */
    watch<C extends AbstractControl<any>>(
        control: C,
        callback: (value: C['value'], control: C) => void
    ) {
        this.observeControl(control, callback, true);
    }

    /**
     * Listens for changes to the control's value, emitting only on changes (not the initial value).
     * @param control - The form control to observe.
     * @param callback - The function to call with the control's value and the control itself.
     */
    onChange<C extends AbstractControl<any>>(
        control: C,
        callback: (value: C['value'], control: C) => void
    ) {
        this.observeControl(control, callback, false);
    }

    /**
     * Add a callback on a formGroupChange. The callback provides the value, the formgroup itself and an array of the controls which changed
     */
    onFormGroupChange<FG extends FormGroup>(
        formGroup: FG,
        callback: (
            value: FormGroupValue<FG>,
            formGroup: FG,
            changedControls: Array<AbstractControl<any>>
        ) => void
    ) {
        let changedControls: Array<AbstractControl> = [];
        const directChildren = Object.values(formGroup.controls);
        directChildren.forEach((control: AbstractControl<any>) =>
            this.onChange(control, (_, control) =>
                changedControls.push(control)
            )
        );

        this.onChange(formGroup, (value) => {
            callback(value, formGroup, changedControls);
            changedControls = [];
        });
    }

    /**
     * Observes changes to the control's value and optionally emits the current value immediately.
     * @param control - The form control to observe.
     * @param callback - The function to call with the control's value and the control itself.
     * @param emitCurrentValue - Whether to emit the current value immediately.
     */
    private observeControl<C extends AbstractControl<any>>(
        control: C,
        callback: (value: C['value'], control: C) => void,
        emitCurrentValue: boolean
    ) {
        const observables = emitCurrentValue
            ? [control.valueChanges, of(control.value)]
            : [control.valueChanges];

        merge(...observables)
            .pipe(
                takeUntilDestroyed(this.destroyRef),
                tap((values) => callback(values, control))
            )
            .subscribe();
    }
}

Note: To prevent memory leaks, the helper uses the DestroyRef. You must therefore create the helper in the constructor (or initialize it directly in the class scope) or provide your DestroyRef instance manually.

onFormGroupChange is the method you need to register a callback on a formGroup. The callback will provide the changed value of the formGroup, the formGroup AND an array of the controls (yes you can have many) which triggered the valueChange.

Upvotes: 0

Bellash
Bellash

Reputation: 8184

You can listen to the FormControl itself like this

formGroup.controls.forEach(element => {
   formGroup.get(element).valueChanges = onControlValueChange;
});

onControlValueChange(v){
     // doTheJobHere();
}

You can also follow the same logic for FormArray

formArray.forEach(item => {
  (formArray[item].controls||[]).forEach(element => {
         formArray[item].get(element).valueChanges = onControlValueChange;
     });
});

Upvotes: 1

Nadhir Falta
Nadhir Falta

Reputation: 5267

I don't think there is default out of the box solution from Angular for this.
You'll have to do this kind of manually, after initiating the form, save a copy of form.value in a temp variable, then on your .valueChnages() call compare the new value with the temp value and extract the key for each that doesn't match.

Another way to do it without using a temp variable is to check for which fields got dirty as suggested here:
Angular Reactive forms : how to get just changed values

But the problem with is that sometime fields get dirty but no value changes. user may type something but then change their mind and delete that and leave it empty.

Upvotes: 0

Related Questions