Angular Material Chips with Reactive Forms and Custom Validation
Angular Material Design components provide modern UI, optimized performance and a lot of powerful features out of the box. Most of these components can be used for our reactive forms right away. However, there is one guy that doesn't have a direct handler to manage its state by using only FormControl and its name is MatChipList.
What is MatChipList? A chip (a.k.a. tags) input is an input box that creates chips from typed text every time a certain key is pressed. You can benefit from it when you want to let users enter an inline list of items such as tags for a post or multiple contacts, etc. Here is an example from the Angular Material docs:
This component consists of input and mat-chip which in its turn represents an array of values. I want to introduce one to solve this issue. Let's take a closer look at it and create a new project:
ng new tag-input-form
We won't need anything in app.component.html generated by Angular so, let's remove everything from there.
Now we need to install angular material itself:
npm install --save @angular/material @angular/cdk @angular/animations
And add bootstrap to make our styling simpler:
npm install bootstrap jquery --save
Now open styles.scss/css and paste there next import:
Alright, now we are good to go. To start using Material components we need to import their modules to our module declaration file. We are going to import all the modules that we will use in our project, so your imports in the app.module.ts should be the following:
Now we can create our template and set up a form. I'll use a FormBuilder to create a form in case we need to scale this app in the future.
1 - Specifying the key codes for separators. This will automatically fire matChipInputTokenEnd event and call our method addEmail()
2 - Linking the form control values with emails array by referencing them.
"<mat-form-field> is a component used to wrap several Angular Material components and apply common Text field styles such as the underline, floating label, and hint messages." - Angular Docs
The form looks good but it's still not usable. We can't add or remove any chip yet. To do that we need to handle the event in addEmail() and onRemoveEmail() respectively.
Since we referenced emails array with the form controller all changes in the array will be reflected on the value of the controller.
Finally, our form is ready and works as it should. We can proceed to the next step: add validation to our chip input. Since a chip list uses an array we have to implement our own validation methods. Unfortunately, it's not possible to use any of Angulars predefined validators because they are not designed to work with arrays. Even the simplest Required validator will fail with our chip list. The value of the control is never undefined, it is an empty array initially. So, let's create our custom validators to make this field required and to make sure a user has entered a valid email address.
First, we're going to create a new typescript file in the app folder. After that, we are creating a pure ts class CustomValidators with two methods inside validateEmailFormat () and validateRequired(). Here they are:
So, what is going on there? We pass a form-control to the validator function, iterate its values and return an error if the value hasn't passed a regex test. To check if values are present is much simpler, we just have to check if the value of the control is not an empty array.
To display an error message on our form we need to add mat-error components right under the input. This component will be displayed if a form-control has a particular error (the one we generate in validators) and will display a message to a user. In addition, we don't want to display the Required error before a user has touched the field or submitted a form.
Next step is to finish our component. First of all, let's add our validators to the form group:
Now we are going to change addEmail method. What should it do? We need to check if the user's input is not empty or not just spaces. Set new value to the form-control and check if it's valid. If it hasn’t passed validation, personally I wouldn’t add this value into the chip array. I would keep it in the input and allow a user to edit his email and try to add it again.
1 - If an input is not empty reset all errors
2 - Store current state of the form values to the temporary variable
3 - Push a new value to the buffer array and set it to the form.
4 - After we set a value to the form-control it checks its validity immediately.
5 - If a new value is valid we reset an input value.
6 - If we got an error after validation we remove this value from the form by removing it from our referenced array of emails.
7 - If a user just hit space, tab or enter check the validity and ignore his input.
But what happens if a user initially had some values in the form and wants to delete all these chips? Right, at this point he won't get any errors. So, we have to handle this scenario as well. Just add a check of validity in our onRemove handler:
And finally, the onSubmit method. Here everything is straight forward. In case, a form field has not been touched we have to touch it to invoke validation and if it's still valid submit the form.
Let's summarise. We discussed how to set up a project with Angular Material. Unfortunately, this step has not been covered properly in the docs. But now we know how to use material chip list in reactive forms and we applied several custom validators to our new form component.