Angular Reactive Forms may be built on a fundamentally different engine than jQuery, but the code you write with it often ends up looking suspiciously similar to the old jQuery style.

A few years back, I was handed a large, bug-ridden form built with Angular Reactive Forms. As I worked through the issues, it struck me that the bugs were the same kind of inconsistencies I used to chase in jQuery applications. State was all over the place, and nothing stayed in sync on its own.

The more I looked at it, the clearer the resemblance became. With just a few cosmetic tweaks, the imperative, command-by-command code was indistinguishable from classic jQuery manipulation:

Angular Reactive Forms to jQuery diff

This feels like a direct contradiction of the philosophy Angular has traditionally pushed: update a variable, and let the DOM react declaratively. A single state change could trigger multiple UI elements to update simultaneously. With reactive forms, though, you're back to issuing specific commands for each individual form control. That feels like a major regression.

Angular Reactive Forms is certainly the standard approach for complex forms, and it offers more flexibility than its template-driven sibling. Still, the pull toward the old declarative style is powerful, and I couldn't shake the desire to return to it.

Thankfully, the community saw the same problem. Other developers have shared techniques for wrapping the imperative API of reactive forms in custom directives, hiding the complexity behind a declarative interface. Netanel Basal's post on the topic is a great starting point, as is this one from Austin.

Once I adopted those patterns, there was no going back to the old way.

Here is my own collection of such directives, which includes a few extra utilities beyond the basics:

// control-disabled.directive.ts
import {Directive, Input} from '@angular/core';
import {NgControl} from '@angular/forms';

@Directive({
    selector: '[controlDisabled]',
})
export class ControlDisabledDirective {
    @Input()
    set controlDisabled(disabled: boolean) {
        const method = disabled ? 'disable' : 'enable';
        this.ngControl.control[method]();
    }

    constructor(private ngControl: NgControl) {}
}
Enter fullscreen mode Exit fullscreen mode
<input 
  [formControl]="formControl" 
  [controlDisabled]="disabled$ | async"
/>
Enter fullscreen mode Exit fullscreen mode

// form-group-disabled.directive.ts
import {Directive, Input} from '@angular/core';

@Directive({
    selector: '[formGroupDisabled]',
})
export class FormGroupDisabledDirective {
    @Input() form: any;
    @Input() formGroupName: string;
    @Input()
    set formGroupDisabled(disabled: boolean) {
        const method = disabled ? 'disable' : 'enable';
        this.form.get(this.formGroupName)[method]();
    }
}
Enter fullscreen mode Exit fullscreen mode
<div 
  formGroupName="days" 
  [formGroupDisabled]="disabled$ | async"
  [form]="form"
>
Enter fullscreen mode Exit fullscreen mode

// set-value.directive.ts
import {Directive, Input} from '@angular/core';
import {NgControl} from '@angular/forms';

@Directive({
    selector: '[setValue]',
})
export class SetValueDirective {
    @Input()
    set setValue(val: any) {
        this.ngControl.control.setValue(val);
    }

    constructor(private ngControl: NgControl) {}
}
Enter fullscreen mode Exit fullscreen mode
<input 
  [formControl]="control" 
  [setValue]="value$ | async" 
/>
Enter fullscreen mode Exit fullscreen mode

// patch-form-group-values.directive.ts
import {Directive, Input} from '@angular/core';

@Directive({
    selector: '[patchFormGroupValues]',
})
export class PatchFormGroupValuesDirective {
    @Input() formGroup: any;
    @Input()
    set patchFormGroupValues(val: any) {
        if (!val) return;
        this.formGroup.patchValue(val, {emitEvent: false});
    }
}
Enter fullscreen mode Exit fullscreen mode
<form 
  [formGroup]="scheduleForm" 
  [patchFormGroupValues]="formData$ | async"
>
Enter fullscreen mode Exit fullscreen mode

Pay close attention to the {emitEvent: false} option in that last directive. Since I was listening to valueChanges on the whole form group, this flag was essential for avoiding an infinite loop, which often manifests as a change detection error. I once gave a talk at a meetup and someone mentioned hitting that exact error; I couldn't remember my original fix at the time, but I believe {emitEvent: false} was the solution.

The setValue directive likely has the same requirement, though I haven't verified it myself. For that reason, I generally recommend taking an explicit, state-management-centric approach for the entire form and using patchFormGroupValues instead.

Hopefully, these patterns spare you some of the headaches I ran into.


Thanks for sticking with me. This was my first post on dev.to. It expands on a section from an earlier article on Medium. That one was paywalled, and the editorial process mangled the intro, so I decided to rework the Reactive Forms part here—it was always my favorite segment, and I believed it deserved a proper, accessible treatment.