You are on page 1of 13

How to Create a Complex Form in

Angular?
By Emily Xiong • medium.com • 6 min

View Original

Problem I got: form handling is definitely a difficult task in Angular,


or in any front-end frameworks. There are limitless interactions
users could do with a form. I need to validate, show errors, and keep
track of form status.

This blog will go through steps to create a pretty complex checkout


form in Angular: https://xiongemi.github.io/angular-form-ngxs .
Sample checkout form

Github repo: https://github.com/xiongemi/angular-form-ngxs .

This blog will show my solution to these problems:

How to create a reusable form component


How to setup dynamic validation on form control
How to persist form values (using state management with
NGXS)

Before We Start
HTML Form
We don’t need to re-invite the wheels, we are still using HTML form.
Here are some things to we could use:

Specify the input type


(https://www.w3schools.com/html/html_form_input_types
.asp ) when possible to control the field value type. By
specifying the input type, on the mobile browser,
appropriate virtual keyboard would popup. It will also limit
user input to our desired type.
Specify the button type (reset, submit)
(https://www.w3schools.com/tags/att_button_type.asp )
when possible.
We could use minlength and maxlengthto add
validations on the input.
Optional: we could use name or autocomplete to input
field to enable autofill. In this checkout form example, I
followed instructions
https://developers.google.com/web/updates/2015/06/check
out-faster-with-autofill to add autofill.

Reactive or Template-Drive Form?


Always use REACTIVE form. As stated in
https://angular.io/guide/forms-overview:

Reactive forms are more robust: they’re more scalable,


reusable, and testable.

With reactive form, it would be easier to track the form status and
integrate with NGXS.

Create a Reusable Form Component


In my form, I got a address form used in 2 places: shipping address
and billing address.
2 address forms

To create a reusable address form component, I have to implement


ControlValueAccessor to the address form and just to emit one
value at onChange:

Use ControlValueAccessor to create the address form


Note: When click submit, the ControlValueAccessor does not have
way to listen to touched change event (Git issue:
https://github.com/angular/angular/issues/10887 ). I need to add a
touched input to the component.

In the parent component where I want to use address form, I simple


use the address form like a regular input:

<afn-address-form formControlName="billingAddress" [touched]="del

Dynamic Validation
For example, I got this “My shipping address is same as my billing
address” checkbox. Whether this checkbox got check would affect
the validation on the shipping address.

When this checkbox is checked, shipping address is hidden and not


required. However, when it is checked, shipping address would
show up and it would be required.

Checked
Unchecked

I also got a radio group for users to choose whether they want to
checkout as a guest or create a account.

“Create Account” Selected


“Checkout as a Guest” Selected

I could not just passed in the validation to shipping address form


control since it is not static. Shipping address is not required all the
time:

shippingAddress: [null, Validators.required],

There are 2 options:

1. Subscribe on the value changes and


manually set and clear validators
In below code snippet, I subscribe to isShippingSame checkbox
value changes and set and clear validators for shippingAdress
fields accordingly.

Subscribe on the value changes and manually set and clear validators

In this example, I only got a couple of fields that need to update


validators. However, if there are more fields need to update
validators, this form component will soon become very large and
hard to test.

2. Use parent form group’s validator


In below code spinet, I have a function
deliveryPageFromValueValidator that reads the entire
form value and set the errors on the form group level.
If I need to show error for each field, I still need to update form
field’s errorStateMatcher since the error is no longer with
form control, but its parent form group.

To do so, I have create below FormGroupErrorStateMatcher


class. It will check whether or not its parent form group has the
errors got passed in.

FormGroupErrorStateMatcher class

In the component, I have create the errorStateMatcher for


password and confirm password fields.

passwordErrorStateMatcher = new FormGroupErrorStateMatcher([‘pass

In the html, I assign above variable to errorStateMatcher for


those inputs:

So if user try to submit the invalid form, it will highlight all the fields
with errors:
Invalid form got submitted

In this approach, the validation logic is no longer inside the


component file, so it would be easier to unit test.

In short,

approach 1: less bolierplate, hard to unit test, I would use it


when I got just a couple form controls need to change
validators.
approach 2: more boilerplate, easy to unit test, I would use
it when I got more form controls that need to change
validators.

Persist Form Values


In this example, I persist already entered form values when user
refresh the page or navigate forward and back. The archive this, I
need to integrate state management with the form.

Install NXGS Libraries


I choose NGXS as my state management library and it is pretty easy
to do with its plugins @ngxs/form-plugin and @ngxs/storage-
plugin . However, you are free to work it with any state management
library out there.

I also install @ngxs/logger-plugin and @ngxs/devtools-plugin for


logging and debugging.

So I run below command to install libraries:

npm install @ngxs/form-plugin @ngxs/logger-plugin @ngxs/storage-p


npm install @ngxs/devtools-plugin --save-dev

In app.module.ts, inside the imports array, I got:

In my lazily-loaded form.module.ts, inside the imports array, I


include:

NgxsModule.forFeature([FormsState]), NgxsFormPluginModule
Integrate with NGXS
Integrate with NGXS is pretty straightforward,

I only need to add ngxsFrom in html:

<form [formGroup]="deliveryPageForm" ngxsForm="forms.deliveryForm

I have created an interface file for one form state:

I create a const variable for the default form state:

In this example, I got a few forms, so the default state for my forms
module is:

Then create form.state is pretty simple:

I just to need to assign the initFormsStateModel to the


defaults and that is it.

Now I got form values in the state, when I navigate back to a page,
already entered form value will persist.

Also, in the app.module.ts, since I add


NgxsStoragePluginModule.forRoot({ key:
[‘forms’] }), it would put entire forms state into storage. Now
if I refresh the page, the entered form values would will be there.

Clear State
Once the form is submitted, I need to clear the entire form state. To
do so, use the library ngxs-reset-plugin , so I only need to dispatch
an action:

this.store.dispatch(new StateReset(FormsState));

Summary
Above are some challenges I ran into when I create to create a
complex form and how I solve those challenges. Of course there are
definitely other ways to work around it.

I have worked on long and complicate forms before. Not only


developing, but testing is difficult as well. There are chrome
extensions like Form Filler that would help you to develop and test
forms.

Working with forms is not an easy task, hopefully, my blog will help
you to finish this task faster.

Unlock unlimited highlights and never lose track of an idea with


Pocket Premium

You might also like