Tim Deschryver

Working with Angular forms in an enterprise environment

Modified
@tim_deschryver

At our company, we struggled with Angular Forms at the start. This is because we dived in head first, without talking about how we would want to use it throughout our projects. This post shows how we're currently using Angular Forms to be more productive.

A bit of background

Anyone that has worked on projects of caliber in the past, will know that there is a high probability that these applications will contain large amounts of complex forms. We were not an exception, we're working in a medical domain to make the administration of clients easier. We come into contact with simple and more complex forms on a daily basis.

As a newly formed team starting on a new project we agreed that we would use Reactive Forms, besides that we hadn’t made agreements around forms and form validation. After a few sprints we started to notice that we were writing a lot of (the same) code, both Angular as HTML. At the same time, we received some design feedback and noticed that we had to touch too much code to get everything right. This is where we started to think that there should be a better way of dealing with forms.

Input form fields

We starting writing input form fields containing all of the orchestration code that is responsible for the field’s behavior. The first iteration of these fields consisted of passing the form control and form group as input to these controls. While this worked at the beginning, it wasn’t great. We always had to be reminded to pass down the form group to the form field as this wasn’t the default “Angular way”. For some of the controls, we ended up with an internal form inside the form field component that had to be kept in sync with the main component, with all the problems and nasty code that came with it.

After some iterations, we learned about Control Value Accessors and this opened up possibilities together with NgControl. From the Angular docs we can see that a CVA has the following API:

This combination allowed us to use our custom form fields just as we would have previously but with more functionality inside of them. The code also looked a lot cleaner. Think of a standardized behavior and visualization for developers as well as for our users, e.g. form validation, and binding the label to the correct input field. For each type of control, we created our implementation and ended up with an abstract class BaseFormField , containing generic code that we needed in each of our form fields.

As you can see, we're also using these form field components to implement a generic behavior across form fields:

An implementation of a checkbox list looks as follows:

The checkbox list field component can be used like a normal input field:

Form directives

By following the practice above, it allows us to extend these controls with custom directives. For example, if we want to populate a radio list or a select box, we can simply assign values to our items.

Super-charged Control Value Accessor’s

CVA’s allows us to create common reusable components, think of a generic person’s component asking for personal information. Before we learned about CVA’s we implemented these control multiple times, with all drawbacks included. More than less whenever a new ticket appeared to add a new field, tweak the validation, or change the behavior of a form field we forgot to update a form on another location. By using a CVA, this can be prevented. It allows us to define the form template and to define the form group with validation built-in. This is nothing special since this can also be done with a default component. The difference lays inside the parent component, where we can use the CVA as a normal form field by just defining it inside the form group. In other words, we can now create a part of a form and just use it as a normal form field. For example, if we would take a very simple form asking for the person’s first and last name, the implementation looks as follows:

This allows us to use this component inside our parent form::

These two persons are defined in the parent’s form group as form controls:

Resulting in the following form value:

Form validation

There was already a glance visible about validation in the previous code snippets. Here again, we felt the pain of writing and maintaining the same code every time we had to implement form validation. That’s why we created an error container, which sole responsibility is to show error messages.

We also have a humanizeFormMessages pipe to map the error to a human-friendly message. We inject FormMessages, containing the default messages. An enterprise environment wouldn't be an enterprise environment if there are no exceptions to the default behavior, that's why we made it possible to override the default messages with case-specific messages.

Creating wizards with FormGroupDirective

To make big wizard forms more manageable we chopped them up into multiple smaller steps. For every step in the wizard, we create an isolated form. The wizard form is made up by stitching up all these little forms together. This improves the maintainability and the testing capabilities. By having this loose coupling it becomes easier to make some modifications to the forms, you have the option to re-use the step forms in different screens e.g. using the form in the wizard and using the form as a stand-alone form.

To implement this, use the FormGroupDirective as ControlContainer and provide them via viewProviders (not via providers). We can now inject the FormGroupDirective inside the component and append the child form to its parent form, in our case this is the wizard.

Testing forms

To test our forms we’re using @angular-extensions/testing-library, which is an Angular wrapper around dom-testing-library. This prevents us from testing implementation details and we're testing our forms the way our users would use them.

We navigate to the form fields by using the form labels, we submit forms by clicking on the submit button. We don't care about the methods from the form component, we care about what our users see.

Some of the resources that helped us to tackle this problem


Please consider supporting me if have you enjoyed this post and found it useful:

Buy Me A Coffee PayPal logo
Support the blog Share on Twitter Discuss on Twitter Edit on GitHub