DEV Community

Cover image for Angular Reactive Forms is basically jQuery
Mike Pearson for This is Angular

Posted on

Angular Reactive Forms is basically jQuery

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.

Discussion (9)

Collapse
londovir profile image
Londovir

One note: it looks like all of your examples rely upon defining observable wrappers around the various properties (disabled, value, etc) and this isn't shown anywhere. If they are already available in AbstractControl I do not think they are advertised as such by Angular, and in that case you would have to be careful to rely upon something which may change in a future release.

Also, creating additional observable streams like this (although handled well with the async pipe) likely leads to [small] additional overhead (performance, memory, etc) for the application for what amounts to a semantic redesign. If you have a very large form I imagine that could be something potentially worth weighing when deciding to use or not use.

Still, a nice technique/idea.

Collapse
mfp22 profile image
Mike Pearson Author

A form control gets disabled by calling disable() on it, so this just automates that. For example, the form I was working on would disable an entire form group based on the value of a certain form control, so that disabled state was downstream from that form control's valueChanges observable.

If reactive programming is just a semantic redesign compared to imperative programming, then it happens to be an extremely valuable semantic redesign. Reactive programming prevents a lot of bugs. When I rewrote the form I was working on this way, most of the bugs went away. This form had 50+ form controls and I noticed no change in performance. It was fast and it stayed fast.

Programming in a reactive style and then working in imperative code feels like going from having a laundry basket to carrying by hand and constantly worrying about dropping socks. Most of the bugs are like, "oops, forgot to reset that value in this circumstance too." Observables let you reuse the consequences of events, which means those consequences only have to be defined in one place instead of being handled multiple times.

Collapse
layzee profile image
Lars Gyrup Brink Nielsen

Nice article. It would be interesting to see examples of these directives in use.

Collapse
mfp22 profile image
Mike Pearson Author

Good idea. I will edit this article with some simple examples for each.

Collapse
mfp22 profile image
Mike Pearson Author

Just did it. Very simple examples, but should give anyone reading a good starting point.

Collapse
layzee profile image
Lars Gyrup Brink Nielsen

Very good. Thank you for that 👍

Collapse
layzee profile image
Lars Gyrup Brink Nielsen

Let me know if you'd like to publish this on This is Angular.

Collapse
mfp22 profile image
Mike Pearson Author

Yeah! How do I do that?

Collapse
layzee profile image
Lars Gyrup Brink Nielsen

Please DM me on Twitter: twitter.com/LayZeeDK