Tracking Changes in Angular Forms Without Losing Your Mind 🤯

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • MyrinNew
    Senior Member
    • Feb 2024
    • 5175

    #1

    Tracking Changes in Angular Forms Without Losing Your Mind 🤯

    If you’ve spent more than five minutes as an Angular developer, you’ve probably wrestled with forms. They’re everywhere — login screens, checkout flows, massive onboarding wizards that make you question life choices.


    And hey, don’t get me wrong — Angular forms are powerful. But let’s be real: sometimes you just wanna know what actually changed without swimming through a sea of boilerplate subscriptions and giant form objects. Like, if a user just updated their street name, why should you care about the fact their hobbies array is still intact?


    That’s exactly the itch I had, and — spoiler alert — I scratched it with a neat little utility that tracks changes like a pro.





    Why the Usual Way Feels Like Overkill 🐘💨

    Sure, you can always subscribe to formControl.valueChanges. Easy peasy… until you’ve got a form with a nested FormGroup for addresses, a FormArray for hobbies, and who knows what else.


    The problem? A single change anywhere in that giant form blasts the whole form value at you. Boom. Every time. All of it.


    It’s like asking someone “Did you move your desk a little?” and they respond by shipping you the blueprint of the entire office building. Not helpful.


    Enter the Hero: A Smart Diff Utility 🦸‍♂️

    So here’s the cool part. Instead of drowning in form values, I cooked up a simple utility that does two magical things:


    Performance win: It waits for the user to pause typing before it even bothers checking changes. (Because nobody wants to diff JSON objects on every keystroke.

    Crystal clarity: It gives you a clean, tidy “diff” object showing only what actually changed.

    Think of it like having a friend who only tells you the juicy gossip, not the whole town’s history.

    The Magic Sauce 🍝 (a.k.a. Code)

    Here’s the core of our utility:






    /**
    * Recursively computes a "diff" object between two values.
    * Returns null if the values are identical.
    * For objects, it returns a new object with only the changed properties.
    * For arrays, it returns a new array with a diff for each element.
    */
    function getDiff(original: any, current: any): any {
    if (original === current) {
    return null;
    }

    // Handle nested objects (FormGroups)
    if (original !== null && typeof original === 'object' && !Array.isArray(original) &&
    current !== null && typeof current === 'object' && !Array.isArray(current)) {
    const diff: any = {};
    let hasChanges = false;
    for (const key in current) {
    if (Object.prototype.hasOwnProperty.call(current, key)) {
    const itemDiff = getDiff(original[key], current[key]);
    if (itemDiff !== null) {
    diff[key] = itemDiff;
    hasChanges = true;
    }
    }
    }
    return hasChanges ? diff : null;
    }

    // Handle arrays (FormArrays)
    if (Array.isArray(original) && Array.isArray(current)) {
    const diff: any[] = [];
    let hasChanges = false;
    const maxLength = Math.max(original.length, current.length);
    for (let i = 0; i maxLength; i++) {
    const itemDiff = getDiff(original[i], current[i]);
    diff[i] = itemDiff;
    if (itemDiff !== null) {
    hasChanges = true;
    }
    }
    return hasChanges ? diff : null;
    }

    // Handle primitive types
    return current;
    }







    And here’s the wrapper that ties it all together:






    export function trackFormChanges(control: AbstractControl, initialValue: any): Subscription {
    return control.valueChanges
    .pipe(debounceTime(300))
    .subscribe(currentValue => {
    const diff = getDiff(initialValue, currentValue);
    console.log("Changes detected:", diff);
    });
    }







    Breakdown of the Code

    • getDiff(original, current): This is the heart of the utility. It's a recursive function that compares the original form value (our baseline) with the current value.
    • If the values are identical, it returns null.
    • If they are objects (FormGroups), it iterates over the keys and calls itself on each property to find nested changes. It only adds properties to the diff object if a change is found.
    • If they are arrays (FormArrays), it iterates over the elements and calls itself to find changes.
    • For primitive values (strings, numbers), it simply returns the new value.
    • trackFormChanges(control, initialValue): This is the public function you'll use in your component.
    • It takes the form control you want to track and its initialValue.
    • It subscribes to the valueChanges observable of the root control.
    • The debounceTime(300) operator waits for 300 milliseconds of inactivity before emitting a value. This is crucial for performance, as it prevents the diff function from running on every single keystroke.
    • Once a value is emitted, it calls getDiff and logs the resulting diff object.


    Why I Fell in Love with This 💘

    The first time I tried it, I was building a user profile form with nested groups. Normally, I’d have to check dirty states on every field like a detective with too much coffee. But with this? One neat diff object, clear as day.


    Suddenly, enabling a “Save” button only when something actually changed went from a gnarly headache to a one-liner:






    [disabled]="!hasChanges">Save







    Chef’s kiss. 👌


    A Real-Life Example: User Profile Form 🧑‍💻

    Here’s how it looks in action:






    @Component({...})
    export class UserProfileComponent implements OnInit, OnDestroy {
    userForm: FormGroup;
    private formSubscription: Subscription;
    public hasChanges = false;

    constructor(private fb: FormBuilder) {
    this.userForm = this.fb.group({
    name: ['John Doe', Validators.required],
    address: this.fb.group({
    street: ['123 Angular Ave'],
    city: ['Codeville']
    })
    });
    }

    ngOnInit() {
    const initialValue = this.userForm;
    this.formSubscription = this.userForm.valueChanges
    .pipe(debounceTime(300))
    .subscribe(() => {
    const diff = getDiff(initialValue, this.userForm.value);
    this.hasChanges = diff !== null;
    });
    }

    ngOnDestroy() {
    if (this.formSubscription) {
    this.formSubscription.unsubscribe();
    }
    }

    onSave() {
    if (this.hasChanges) {
    console.log('Saving changes:', getDiff(this.userForm, this.userForm.value));
    }
    }
    }







    It’s simple, clean, and doesn’t leave you second-guessing what’s happening under the hood.


    Comments are welcome for code suggestions, or any better idea (for example making it a custom rxjs operator maybe ?? 🎁).


    I would love to hear any personal experiences on this matter.


    If you feel this article helped you even a little bit , please support by sharing it.


    Wrapping Up 🎁

    Forms can be messy, but tracking changes doesn’t have to be. With a smart recursive diff and a little debounce magic, you get a reliable, testable, and sanity-saving way to know exactly what’s changed — nothing more, nothing less.


    I’ve been using this trick across projects, and honestly, I don’t want to go back. If you’re tired of wrestling with dirty and touched states, give this a spin.


    Who knows — you might just fall in love with Angular forms again. (Okay, maybe that’s pushing it. 😅)




    More...
Working...