Truly Reactive Forms in Angular: A Unique Approach
Reactive forms are not reactive enough in Angular. Especially when dealing with async observable data to pre-populate a form. Having to subscribe and call methods like setValue
and patchValue
seems very imperative and can be a recipe for data races and memory leaks. It can especially get messy when bulk editing an array of domain models. In this article we explore a different approach to reactive forms. Keep reading to know more6+
EDIT 2023: After 6+ years of experience building complex forms with Angular, I have now found that template driven forms are actually more reactive than “Reactive Forms”. They produce simpler, reactive and more maintainable code. I would write in more detail but this video by Ward Bell couldn’t have said it better.
[Archived]
Observable of Forms!
That’s right! We can think of forms as a stream of UI input elements. Since forms can be thought of reactive setters that a user uses to set
the data model of the view layer. As the data model itself is a stream, the form elements can be thought of as a stream of UI elements mapped from the underlying data stream.
Thinking of forms this way allows us to utilize composition before consumption of the data model in the template. Composition is a powerful and elegant tool in software engineering and mathematics. We can declare our Observable form on our component class like so:
form$: Observable<FormGroup>;
Populating Observable Forms with Async Default values
In this way, populating the form with async data becomes easier than ever.
We can simply take the Observable of source data and map it to the form!
Explanation:
- We create the
ngOnInit
life cycle hook to initialise our Observables. I like to initialise component properties in thengOnInit
hook because it lets me skim through all the class declarations and their type annotations in one sweep. You can initialise your Observables in the constructor if you like. - We create an observable of the post we want to edit by selecting it from the store. Here we are using NGRX as a reactive data store but you can use any async data source to make your observable (Http Service call for example). Here we’re assuming that we aren’t using container presenter pattern.
- We map the value emitted by the observable to a FormGroup using the
FormBuilder
injected into the component using angular’s dependency injection.
Usage in Template
Using this method we can delegate subscription management and observable unboxing to async pipe in the template.
Explanation:
- We’ve wrapped our form in an
ng-container
. This allows us to unbox the Observable of the form as well as show a loading message in case our async data source has not loaded yet! Notice how easy and clean this is. - Then, we create a reference to our unboxed form using the
as form;
syntax along with the async pipe. - We create our form controls using the familiar reactive forms approach with
form
as our FormGroup - In the
(click)
output binding, we pass in the form object `onConfirm(form)
` so that the handler has all the information it needs to build payload and delegate the update functionality to a service or the store.
Building Payloads
Building payload is easy too. Since we pass in our form group to the onConfirm
method, We can use it to extract the data we require to delegate the update functionality to a service call or store effect.
<button
[disabled]="!form.dirty || form.invalid"
(onConfirm)="onConfirm(form)"
>
Save
</button>
You can choose to pass in the form value instead of the formGroup object if you want. What’s more important is the way of thinking and approaching the problem rather than the finer details. You can set your own standards and conventions for your project/organisation.
onConfirm(form: FormGroup) {
const payload: Post = this.form.controls.post.value;
this.store.dispatch(postActions.UpdatePost());
}
Form Arrays
This technique is especially powerful when used with Form Arrays ( FormArray
). Sometimes we wish to bulk edit a collection of domain models. In this case we can utilise `Observable Form Arrays` to compose our form elements and pre-populate them with existing data.
And you can use this observable of form array in your component’s template easily
Form Arrays in Template
Explanation:
- Similar to the Observable Form Group example we saw above, we wrap our Observable in an
ng-container
to unbox, show loading message and store a local reference to the unboxed formArray - We use another
ng-container
to iterate over our pre-populated formGroups which can be accessed fromformArray.controls
. Remember,FormGroup
,FormArray
andFormControl
are all extendAbstractControl
and are hence AbstractControls themselves by Liskov Substitution .FormGroup
andFormArray
are boxing data structures which can contain many controls inside of them. They can be thought of as a Generic Data Structures even though it’s a shame they are not strongly typed as a generic.FormArray.controls
is an iterable ofFormGroup
(s) orFormControl
(s). In this exampleformArray.controls
is an iterable ofFormGroup
because we have initialised it as an array ofFormGroup
(s)! - We are iterating over the form groups in our form array using
*ngFor
and creating form groups and controls as usual using the familiarReactive Forms approach
Observables of User Input
We can easily create Observables of user inputs as well in case we want to compose them for some sort of usage in templates or cross validation.
The shareReplay
operator ensures that any subsequent subscriptions receive the last known value of the form input.
As with any design pattern or decision in software engineering, every benefit has trade offs. Let’s talk about a few
Benefits
- Less imperative code
- No manual subscription management in component class
- Form reacts to data model change. For example, in a realtime collaboration app, the populated form will update to latest value. In such a collaborative use case, you can add custom logic which only updates controls that are untouched
Tradeoffs
- Less imperative code! Reactive programming is hard and requires a different way of thinking. This can be a disadvantage in large teams with a significant number of junior developers as it could be a steep learning curve for them. Often simple solutions that entire team can maintain easily are preferable.
- You
Conclusion
The greatest challenge of event driven programming is “staying up to date with the world”. Push based systems help us avoid challenges like race conditions and and data races. In this article we learnt to think of pre-populated form elements as a reactive stream of UI elements mapped from underlying data stream. We saw how we can initialize Form Groups and Form Arrays with pre-populated default values/existing values. We learnt how to use this in the template. Using this approach we can push our thinking into the more reactive realm and benefit from clean code and software composition.
If you want to learn more about reactive programming, check this book out.
Thank you for reading and I hope this helps you as it helped me.
I would love to know in the comments below if this was useful!