Truly Reactive Forms in Angular: A Unique Approach

Nivrith
Evergreen Engineer
Published in
5 min readNov 9, 2020

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>;
Photo by Ben White on Unsplash

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 the ngOnInit 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 from formArray.controls. Remember, FormGroup , FormArray and FormControl are all extend AbstractControl and are hence AbstractControls themselves by Liskov Substitution . FormGroup and FormArray 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 of FormGroup(s) or FormControl(s). In this example formArray.controls is an iterable of FormGroup because we have initialised it as an array of FormGroup(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!

You can follow me on GitHub or Twitter
Happy Engineering!

--

--

Nivrith
Evergreen Engineer

Software Engineer living a polymath lifestyle in Adelaide, Australia