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:
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) {}
}
<input
[formControl]="formControl"
[controlDisabled]="disabled$ | async"
/>
// 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]();
}
}
<div
formGroupName="days"
[formGroupDisabled]="disabled$ | async"
[form]="form"
>
// 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) {}
}
<input
[formControl]="control"
[setValue]="value$ | async"
/>
// 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});
}
}
<form
[formGroup]="scheduleForm"
[patchFormGroupValues]="formData$ | async"
>
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.
Top comments (9)
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.
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'svalueChanges
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.
Nice article. It would be interesting to see examples of these directives in use.
Good idea. I will edit this article with some simple examples for each.
Just did it. Very simple examples, but should give anyone reading a good starting point.
Very good. Thank you for that 👍
Let me know if you'd like to publish this on This is Angular.
Yeah! How do I do that?
Please DM me on Twitter: twitter.com/LayZeeDK