Angular Forms: reactive design patterns catalog

Post Editor

In this post, you'll find a set of design patterns for building Angular forms based on two pillars: separation of responsibilities and reactive programming to tackle the complexity of rich and complex Angular forms.

9 min read
3 comments
post

Angular Forms: reactive design patterns catalog

In this post, you'll find a set of design patterns for building Angular forms based on two pillars: separation of responsibilities and reactive programming to tackle the complexity of rich and complex Angular forms.

post
post
9 min read
3 comments
3 comments

In many Angular enterprise applications, the most important Angular module is the ReactiveFormsModule. Angular forms allow users to create, update and search data. They also enhance the user experience with functionality such as validation and autocompletion.

Application forms and GUI in general are one of the software components that can benefit the most from reactive programming paradigm. Many of Angular modules are based on RxJS (such as HttpClientModule and RouterModule). But, the power of this reactive API is not fully exploited in many Angular applications.

In this post, we present a set of design patterns for building Angular forms that I used and promoted during the last years. The patterns are based on two pillars: separation of responsibilities and reactive programming to tackle the complexity of rich and complex Angular forms.

Before jumping to our first design pattern, the next section will be an overview of the different patterns and their goals.

Design Patterns
Link to this section

A design pattern is a general, reusable solution to a commonly occurring problem within a given context . The patterns will be presented for Angular but they are also valid for other frameworks. The main goals behind these patterns are:

  • Enhancing maintainability
  • Reducing the number of bugs

Our design patterns are the core of a pattern language that I used to create complex Angular forms. The following diagram show the main patterns and their relationships:

The central part of the language is the Form Model. Let's start by presenting it.

Form Model
Link to this section

The most important components in Angular Forms are FormGroup and FormControl. The two components are untyped and present a low-level general-purpose API. In complex applications, FormGroup and FormControl, usually, are not the good abstraction to use in Angular components. Instead, we need a high-level abstraction which is specific to the web page components.

To tackle the application complexity, we need to introduce a layer of abstraction above the Angular API. We call this layer the Form Model pattern which defines a wrapper around a FormGroup instance. It provides a special-purpose API over the Angular generic forms API. So, instead of using Angular model terms, we use our application terminology.

Here a simple example:

<>Copy
class PersonCreationForm { readonly initialValue; constructor(private formGroup: FormGroup) { this.initialValue = formGroup.value; } get asFormGroup() { return this.formGroup; } isValid(): Observable<boolean> { return this.formGroup.statusChanges.pipe( map(() => this.formGroup.valid), startWith(false) ); } ageIsGreaterThan(min: number): Observable<boolean> { return this.formGroup.valueChanges.pipe( map(value => value.age), distinctUntilChanged(), map(it => it > min), startWith(false) ); } }

The PersonCreationForm class presents the following characteristics:

  • present a high-level API: ageIsGreaterThan() is an abstraction over the low level plumbing details to determine if the age is greater than the passed parameter.
  • expose a reactive API: except asFormGroup() property, all data are accessible via Observables,
  • expose Angular non reactive properties via Observable: isValid() return an Observable that emit a value when FormGroup.valid property changes,
  • add missing functionality in Angular form model: Angular do not allow access to form initial value,
  • and make the FormGroup accessible.
Note: The form model is a leaky abstraction because the view needs to access the wrapped FormGroup.

The form model can be used in the component like this:

<>Copy
@Component({ selector: "person-creation-form", template: ` ... <form [formGroup]="form.asFormGroup"> ... </form> ... ` }) export class PersonCreationFormComponent { form: PersonCreationForm; ageIsGreaterThanTen: Observable<boolean>; formIsValid: Observable<boolean>; constructor(formBuilder: FormBuilder) { this.form = createFormModelUsing(formBuilder); this.ageIsGreaterThanTen = this.form.ageIsGreaterThan(10); this.formIsValid = this.form.isValid(); } } function createFormModelUsing(formBuilder: FormBuilder): PersonCreateForm { const formGroup = formBuilder.group({ name: "", age: "" }); return new PersonCreateForm(formGroup); }

The unique role of PersonCreationFormComponent is to define the view and connect it to the Form Model.

The source code can be found here.

Let’s see now how we should create the form model.

Form Factory
Link to this section

The first step to add a complex Angular form to a web page is to create a FormGroup object and bind it to a view template. The form group creation mainly defines form fields, initial values and validators. This creational logic is usually leaked and mixed with other types of logic inside Angular components.

The creation of the FormGroup or better the Form Model should be separated from the rest of logic. To isolate it, we should use the Form Factory pattern. The Angular component showing the form should be unaware of this details.

Let's illustrate this pattern with a concrete example:

<>Copy
@Injectable() class PersonCreationFormFactory { constructor(private formBuilder: FormBuilder) {} create(): PersonCreationForm { const formGroup = this.createFormGroup(); return new PersonCreationForm(formGroup); } private createFormGroup() { return this.formBuilder.group({ name: [""], age: [""] }); } }

PersonCreationFormFactory is a simple example of a form factory. It has only the FormBuilder as a dependency. In more complex scenarios we can have more dependencies to set for example initial values.

The factory can be injected directly in the component class or we can use factory provider definition mode as following:

<>Copy
@Component({ selector: "person-creation-form", template: `...`, providers: [ PersonCreationFormFactory, { provide: PersonCreationForm, useFactory: (factory: PersonCreationFormFactory) => factory.create(), deps: [PersonCreationFormFactory] } ] }) export class PersonCreationFormComponent { constructor(public form: PersonCreationForm) { } }

Using useFactory is a little bit verbose, but it's worthwhile doing it when using the patterns described in this post. The Form Model will be easy to inject.

In summary, Form Factory responsibilities are:

  • create form group
  • define validation rules
  • define initial values

The source code can be found here.

With our two first patterns, we create the form model to be able to get user entered and selected values. The next pattern will be about providing the data to be selected by the application users.

Form Data Provider
Link to this section

Form fields can provide advanced assistance to users. The most frequent assistance is the autocomplete. This logic is usually used only in the view. But, it is prepared by the component class.

The Form Data Provider is a pattern that promotes the segregation of this logic in a service which is used only by component's HTML template. The unique role of this service is to provide the dynamic data needed by the fields.

Let's see the pattern with an example:

<>Copy
@Injectable() class PersonCreationFormDataProvider { constructor(private httpClient: HttpClient) {} searchCountry = (termChanged: Observable<string>): Observable<string[]> => termChanged.pipe( debounceTime(200), distinctUntilChanged(), switchMap(term => term.length < 3 ? of([]) : this.findCountryBy(term)), map(values => values.map(country => country.name)) ); private findCountryBy(term: string) { return this.httpClient.get<any[]>(`https://restcountries.eu/rest/v2/name/${term}?fields=name`); } }

The searchCountry method receives an observable emitting the search term and returns an Observable containing the search result. We use restcountries.eu API to do the search.

The data returned by this service can depend on the field search term and also on other field value. We can add a city field that depends on the selected country. In this case, the searchCity method will depend on the user search term and the form model. Let's keep our example more simple to make it easy to grasp but in real life the Form Data Provider is more complex.

To use the PersonCreationFormDataProvider, we need to declare it as a component provider, inject it in the PersonCreationFormComponent and use it only in the template.

<>Copy
@Component({ selector: "person-creation-form", template: ` <form [formGroup]="form.asFormGroup" class="form-horizontal"> ... <div class="form-group"> <label for="country">Country</label> <input id="country" type="text" formControlName="country" class="form-control" [ngbTypeahead]="formDataProvider.searchCountry"/> </div> ... </form> `, providers: [ PersonCreationFormFactory, { provide: PersonCreationForm, useFactory: (factory: PersonCreationFormFactory) => factory.create(), deps: [PersonCreationFormFactory] }, PersonCreationFormDataProvider ] }) export class PersonCreationFormComponent { ageIsGreaterThanTen: Observable<boolean>; formIsValid: Observable<boolean>; constructor( public form: PersonCreationForm, public formDataProvider: PersonCreationFormDataProvider ) { this.ageIsGreaterThanTen = this.form.ageIsGreaterThan(10); this.formIsValid = this.form.isValid(); } }

In a big form with many autocomplete fields and a complex logic, we should use multiple data provider services to keep the code easy to grasp. We can even have a dedicated data provider for each field. But in less complex cases, we can create a single service for each form.

The source code can be found here.

With the three previous patterns, the user can enter and select data. The next step will be to add form actions.

Form Actions
Link to this section

Each form has at least one action that can be executed after filling its fields. The form action is usually entangled with creational and computational logic. For a form with a few logic, it is not a real problem. But with complex forms, mixing this logic with other ones make the code implicit and difficult to understand very quickly.

A good solution to enhance the maintainability is to use the Form Actions pattern which puts the action logic in a dedicated class.

<>Copy
@Injectable() class PersonCreationFormActions { validateButtonClicked = new Subject<void>(); resetButtonClicked = new Subject<void>(); constructor(private form: PersonCreateForm) { this.handleValidateButtonClick(); this.handleResetButtonClick(); } private handleValidateButtonClick() { this.validateButtonClicked .subscribe(() => alert('The form is validated!')) } private handleResetButtonClick() { this.resetButtonClicked .subscribe(() => this.form.reset()) } }

The PersonCreateFormActions example use a dumb implementation to illustrate the concept. The form propose two actions: validate and reset.

<>Copy
@Component({ selector: "person-creation-form", template: ` <form [formGroup]="form.asFormGroup" class="form-horizontal"> ... <button class="btn btn-primary" [clickEvent]="formActions.validateButtonClicked">Validate</button> <button class="btn btn-secondary" [clickEvent]="formActions.resetButtonClicked">reset</button> </form> ... `, providers: [ PersonCreationFormFactory, { provide: PersonCreationForm, useFactory: (factory: PersonCreationFormFactory) => factory.create(), deps: [PersonCreationFormFactory] }, PersonCreationFormDataProvider, PersonCreationFormActions ] }) export class PersonCreationFormComponent { ageIsGreaterThanTen: Observable<boolean>; formIsValid: Observable<boolean>; constructor( public form: PersonCreationForm, public formDataProvider: PersonCreationFormDataProvider, public formActions: PersonCreationFormActions ) { this.ageIsGreaterThanTen = this.form.ageIsGreaterThan(10); this.formIsValid = this.form.isValid(); } }

We no longer use the click event. Instead, we use a custom directive to define a click event listener. So, the view directly notify our service subjects.

<>Copy
@Directive({ selector: '[clickEvent]' }) export class ClickEventDirective { @Input() clickEvent: Subject<void>; @HostListener('click') onClick() { this.clickEvent.next(); } }

The source code is here.

Now, we have the required patterns to create a form allowing to create or update business objects. In the next pattern, we will present a pattern used in search forms.

Search Form
Link to this section

In many cases, web application users want to share a search result or tag a search request/criteria in the browser bookmarks. In SPA, a best practice is to put the search criteria in the URL query parameters. The native browser behavior allows users to share URL and to navigate in the history of search.

A simple implementation would be:

<>Copy
export interface ParamsConverter<T> { fromUrl(Params): T; toUrl(T): Params; } export const URL_STORE_CONVERTER = new InjectionToken<ParamsConverter<any>>('ParamsConverter'); @Injectable() export class UrlStore<T> { changed: Observable<T>; refreshed = new Subject<T>(); changedOrRefreshed: Observable<T>; constructor( @Inject(URL_STORE_CONVERTER) private converter: ParamsConverter<T>, private router: Router, private route: ActivatedRoute, ) { this.changed = this.route.queryParams.pipe(map(converter.fromUrl)); this.changedOrRefreshed = merge(this.changed, this.refreshed); } setSource(paramsChanges: Observable<T>) { paramsChanges.subscribe(params => { const urlParams = this.converter.toUrl(params); const extras = { relativeTo: this.route, queryParams: removeEmptyAtrributes(urlParams), } as NavigationExtras; this.router.navigate(['.'], extras).then(result => { const urlIsTheSame = result === null; if (urlIsTheSame) { this.refreshed.next(params); } }); }); } }

The UrlStore is a generic class that can be extended by implementing the ParamsConverter interface. Its API is composed of two main parts:

  • First, it allows to change URL content using setSource method. This method could be replaced by a Subject as a class attribute.
  • Second, it expose three attributes to detect different types of change in URL: changed, refreshed and changedOrRefreshed.

In a search form, the URL store is used in the form actions class to update the URL query parameters.

<>Copy
@Injectable() class PersonCreationFormActions { searchButtonClicked = new Subject<void>(); constructor( private form: PersonCreateForm, private urlStore: UrlStore<PersonSearchCriteria> ) { this.handleSearchButtonClick(); } private handleSearchButtonClick() { const searchAction = this.searchButtonClicked.pipe( map(it => this.form.asFormGroup.value) ); this.urlStore.setSource(searchAction); } }

The source code is here.

This is the last pattern of this article. Let's now take the time to answer few questions that are frequently asked when implementing the patterns described above.

FAQ
Link to this section

Why Angular is not a full reactive framework?

Angular form API for example is not fully reactive. FormControl has some properties such valid without a reactive equivalent. The other example is the Outputs: even under the hood Angular uses RxJS to handle the outputs, it does not expose an observable allowing it to listen to output events. But, on the other side Angular has many reactive API such as Router, HttpClient.

Is @Output() useless?

Under the hood, the @Output() is based on RxJS Observables, but the subscribe isn’t visible in our application. If we want to fully use Observable in our code we can pass a Subject as input.

Why we don't use Presentational and Container patterns?

Because it is an anti-pattern. It was promoted in a framework having a limitation that Angular does not have. An Angular component should have only the responsibilities related to the view and the view model patterns. I stopped thinking about this pattern more than a year ago and my code is much better now.

Why do we use only component providers?

Our implementation is based mainly on component providers. The main advantage of this type of provider is having the same lifecycle as their component.

What about code source navigation?

Adopting the patterns of this post, makes the navigation between files a little bit harder. We should use a good IDE that simplifies navigation between related files.

What about third party libraries?

If we adopt a reactive approach in our Angular applications, we should carefully choose our third party libraries. Libraries should provide reactive APIs. In our autocomplete example we used ngx-bootstrap, which exposes a reactive API that not all other component libraries expose.

If we develop a home made components library encapsulating third party dependencies, it should expose the two style API the reactive and the old school: as not all teams can adopt a reactive approach.

Conclusion
Link to this section

In this post we tried to present a pattern language for the more used part of Angular in Enterprise Intensive Form Applications. The set of patterns was explained with just enough details to expose the big picture. The implementation details of every pattern varies depending on the use cases.

Adopting this kind of patterns make the application better structured and easy to grasp and to maintain. Another advantage to have such a stable design is facilitating the adoption of a good testing strategy.

Comments (3)

authortripathiarpit
12 May 2021

Hey Gara! I would like to understand something with following lines { provide: PersonCreationForm, useFactory: (factory: PersonCreationFormFactory) => factory.create(), deps: [PersonCreationFormFactory] }, Why deps: [PersonCreationFormFactory], what is the role of this line in getting a depenency.

authormohamed-gara
19 May 2021

Hi Arpit, Defining just useFactory: (factory: PersonCreationFormFactory) => factory.create() is not sufficient for Angular to determine what dependency to inject. At runtime, the parameter factory does not have the type PersonCreationFormFactory. The type exists only at the compile time. So, Angular adds the deps array to make the dependency injection possible. I hope this is the response you are looking for.

authoragarciabz
19 May 2021

Hi Mohamed,

That was a really interesting read. I'm used to write reactive forms in a single component so seeing these patterns applied for Angular is refreshing for me. I have two questions:

  1. Could these patterns be reused for different components? For example: PersonForm for Person creation and Person edit.
  2. Could you elaborate more on why Presentational/Container is an anti-pattern? Honestly it's the first time I hear this.
authormohamed-gara
19 May 2021

Hi Agustin,

  1. Could these patterns be reused for different components? For example: PersonForm for Person creation and Person edit. => If the creation and edition forms have exactly the same structure, validation, and behavior, we can reuse the same Form Model for both. But, from my experience, the two forms are usually different even if they seem similar at first.
  2. Could you elaborate more on why Presentational/Container is an anti-pattern? Honestly it's the first time I hear this. => As I know, the presentation/Container pattern is inspired by the React community. And now is considered an anti-pattern as you can see in the Update from 2019 section of Dan Abramov's post here. You can also see his presentation here Personally, I stopped using the pattern before reading Dan's new opinion for a simple reason. A component should focus on rendering the view and reacting to user actions. In complex applications, any other responsibility should be delegated to a service. We should not pollute the components tree with components that have no visual rendering. It's understandable to do it with a framework that has constraints. But, in Angular we have services.

This is just my humble opinion. Thanks for your feedback.

authoryurakhomitsky
31 May 2021

So it looks like I will have to implement all methods that formGroup provides(setValue,getRawValue, etc) to make a good wrapper? And How can I deal with the case when for example I need extra value to use in "ageIsGreaterThan" , which is not a part of form and can be changed over time

authoryurakhomitsky
31 May 2021

This extra value might be passed by @Input into component but how do I combine it with the stream that form wrapper provides

Share

About the author

author_image

I am a Software Engineer with more than 10 years of experience. I have a strong orientation towards Craft/Clean Code/DDD/BDD/TDD and DevOps. I am mainly specialized in Angular and Spring.

author_image

About the author

Mohamed Gara

I am a Software Engineer with more than 10 years of experience. I have a strong orientation towards Craft/Clean Code/DDD/BDD/TDD and DevOps. I am mainly specialized in Angular and Spring.

About the author

author_image

I am a Software Engineer with more than 10 years of experience. I have a strong orientation towards Craft/Clean Code/DDD/BDD/TDD and DevOps. I am mainly specialized in Angular and Spring.

Looking for a JS job?
Job logo
PDQ team| Senior JavaScript developer (Angular/Node)

SD Solutions

Ukraine
Remote
$60k - $80k
Job logo
Senior Full-Stack Developer (Node+Angular)

A-Listware

Ukraine
Remote
$48k - $78k
Job logo
Senior Full stack (Angular+Node)

Monolith

Ukraine
Remote
$60k - $84k
Job logo
AngularJS Developer/.net Core - Remote Contract

InfoMagnus

United States
Remote
$115k - $134k
More jobs
NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

Angularpost
13 September 20218 min read
Tracking user interaction area

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Angularpost
13 September 20218 min read
Tracking user interaction area

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Read more
AngularpostTracking user interaction area

13 September 2021

8 min read

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Read more
Angularpost
7 September 202122 min read
Designing Angular architecture - Container-Presentation pattern

Designing architecture could be tricky, especially in the agile world, where requirement changes are frequent. So your design has to support that and provides extendibility without the need for serious modification. In such cases, you will find the Container-Presentation pattern instrumental.

micro frontendspost
6 September 202125 min read
Taking micro-frontends to the next level

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

micro frontendspost
6 September 202125 min read
Taking micro-frontends to the next level

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

Read more
micro frontendspostTaking micro-frontends to the next level

6 September 2021

25 min read

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

Read more