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
0 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
0 comments
0 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:

Content imageContent image

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 (0)

Be the first to leave a comment

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.

Featured articles