How to Build a Custom Form Control in Angular

As a developer, sometimes you are required to take some inputs from a form control and do some preprocessing before submitting – commonly on the submit function. A good example of this is a phone number, where you take a country code and phone number and combine them into a complete phone number before saving.

While this might work when you need to use it once or twice in your application, if you use it more than once, then a custom form control might be your best bet. A custom form control will behave just like a normal form control and will return your complete phone number.

Introduction

To build a custom form control, we are going to be using ControlValueAccessor, this is a bridge between native elements like inputs, text area and angular forms API. In simple, it allows component to behave like an input form control. Allowing you to set value and get value from it.

This allows you to create complex and powerful form controls which can be reused across your application. This also allows you to use form validators to validate the output of your custom form controls quite easily if you wish to.

You can also build them as libraries and use them across multiple application, this greatly improves your development experience. Here are some more tips on how you can improve your development experience.

Getting Started

In this post, we are just going to use the FormsModule and ReactiveFormsModule. Since am a huge fan of Angular Material and Angular Flex Layout, am going to use them for the demo, but those can easily be substituted with bootstrap or any other UI Framework.

This post assumes that you have at least the basics of Angular. In this demo, we are going to look at a very basic example. We are going to create a simple custom form control that lets users enter their email address in two parts: their username and their email provider domain name.

Our custom form control will return the two as a single email address. This will allow us to use built-in validators on. It will also accept a default value as a single email. And then it will separate the email into the username and provider. The it will automatically assign it to the correct input boxes.

Without further ado.

Creating our Component

We will start by creating a normal component using Angular CLI:

ng generate component email-address-input

Then, we need to implement ControlValueAccessor in our component:

export class EmailAddressInputComponent implements ControlValueAccessor { }

And then add a provider for the ControlValueAccessor we just created:

providers: [
{
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => EmailAddressInputComponent),
      multi: true
}]

Next, we need to add the necessary methods for a class implementing ControlValueAccessor:

interface ControlValueAccessor {
  writeValue(obj: any): void
  registerOnChange(fn: any): void
  registerOnTouched(fn: any): void
  setDisabledState(isDisabled: boolean)?: void
}

You can learn more about the above methods here: On top of the above methods, we need to add set and get methods to set and get the value of our component. This will allow us to read and set the email value easily, including separating it into the username and provider.

So,  in our component, we will start by adding 3 properties – username, emailProvider and _value (note the underscore).

public username;
public emailProvider;
_value;

Next, add the methods necessary to implement ControlValueAccessor:

First, add the writeValue method. It will pass the default value and set it to the _value property of the field.

writeValue(value: any) {
  if (value !== undefined) {
      this.value = value;
      this.propagateChange(this.value);
  }
}

 

Next, we add the other methods to implement ControlValueAccessor for our component:

propagateChange = (_: any) => {};

registerOnChange(fn) {
    this.propagateChange = fn;
}

registerOnTouched() {}

Next, we are going to add on change event method. This will update the value of our component when you type your username and email provider. The method will concatenate the username and email Provider into an email address. The method will be triggered by the input change event of either the emailProvider or username input fields. You can use any other event depending on what you want to achieve.

addEvent($event) {
    this.value = this.username + '@' + this.emailProvider;
    this.propagateChange(this.value);
}

And finally, we add set and get methods to get and set the value of our _value property:

get value() {
    const email = this._value;
    return email;
}

The set value property will take the input email address and split it into both the username and email provider and assign to respective property – username and emailProvider.

set value(val) {
    if (val) {
      this._value = val;
      [this.username, this.emailProvider] = val.split('@');
      this.propagateChange(this._value);
    }
}

Next, we create our template:

<div fxFlex="100" fxLayout="row" fxLayoutGap="10px" style="padding: 10px">
  <mat-form-field fxFlex>
    <input matInput type="text" (change)="addEvent($event)" [(ngModel)]="username" [value]="username" placeholder="Username">
  </mat-form-field>
  <mat-form-field fxFlex>
    <input matInput type="text" (change)="addEvent($event)" [(ngModel)]="emailProvider" [value]="emailProvider" placeholder="Email Provider">
  </mat-form-field>
</div>

NB: We are using [(ngModel)] for two-way binding between the template and the component class.

Using our Custom Form Control

Since now we have our custom form control, we can use it as a normal application. First, we are going to create a formGroup for our custom form:

this.formGroup = this.fb.group({
      email: [this.email, [Validators.email]],
      email2: ['@theinfogrid.com', [Validators.email]],
      email3: ['hello', [Validators.email]],
      email4: [null, [Validators.email]]
    }
);

As you can see, we are able to pass default value to our custom form control and use built-in and even custom validators just like normal form control. And using it in our template is the same:

<form [formGroup]="formGroup" fxLayout="column">
    <div fxFlex="100">
      <app-email-address-input formControlName="email"></app-email-address-input>
      <mat-error *ngIf="formGroup.controls['email'].hasError('email')">Your email is invalid email</mat-error>
    </div>
    <div fxFlex="100">
      <app-email-address-input formControlName="email2"></app-email-address-input>
      <mat-error *ngIf="formGroup.controls['email2'].hasError('email')">Your email is invalid email</mat-error>
    </div>
    <div fxFlex="100">
      <app-email-address-input formControlName="email3"></app-email-address-input>
      <mat-error *ngIf="formGroup.controls['email3'].hasError('email')">Your email is invalid email</mat-error>
    </div>
    <div fxFlex="100">
      <app-email-address-input formControlName="email4"></app-email-address-input>
      <mat-error *ngIf="formGroup.controls['email3'].hasError('email')">Your email is invalid email</mat-error>
    </div>
</form>

TIP: If you are using lazy loading, you can create a module where you declare and export all Pipes and Components that are being used across multiple lazy loaded modules.

Where is this Code?

The above source code be found on Github repository here.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.