Angular Reactive Forms is basically jQuery

Mike Pearson - Mar 24 '21 - - Dev Community

I know they're way different internally, but Angular Reactive Forms makes your code look a lot like jQuery code.

A couple years ago I was assigned to fix a bunch of bugs on a large form that was written in Angular Reactive Forms. The types of bugs that were popping up strongly reminded me of the types of bugs common in jQuery apps. Inconsistent state everywhere!

I suddenly realized how similar the code was to jQuery code. In fact, with only a couple of cosmetic changes, it would have been the same:

Angular Reactive Forms to jQuery diff

This is actually opposed to the pattern Angular traditionally encouraged: Just update variables and set up the DOM to update appropriately. With a single variable update, potentially multiple DOM elements could react all on their own. Now, with reactive forms, you go back to imperative commands for each individual form control... This is a huge step backwards in my opinion.

I know Angular Reactive Forms is the standard answer for forms in Angular, and they are more dynamic than template-driven forms, but I really wanted to go back to the declarative days of old Angular forms.

Luckily, I wasn’t the only person who noticed that reactive forms needed help to be reactive. Other developers have written articles explaining how to create directives that could hide the imperative interface of reactive forms behind a declarative interface. Check out this article from Netanel Basal, and this one by... Austin.

After using these directives, I never want to go back.

Here is my own implementation, plus a couple extra directives:

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

Notice the {emitEvent: false} in this one. I was subscribing to valueChanges on the form group, so this prevented it from entering an infinite loop, which I think actually shows up as a change detection error. I gave a talk at a meetup and someone said they ran into the error, and I forgot what I did to fix it. I think {emitEvent: false} was what fixed it.

The same thing probably applies to the setValue directive, but I haven't tested it, because I recommend just doing explicit state management for the whole form and using patchFormGroupValues.

Hope this helps!


Thanks for reading. This was my first post on dev.to. I was expanding on part of a post I made over on medium. That one was behind a paywall, and the editors mutilated the beginning, so I decided to redo the Reactive Forms section here because it was my favorite part and I think it deserved more attention.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .