How to Build a Custom Form Control in Angular

| By Maina Wycliffe | | | 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: ['@codinglatte.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.

Angular 6 - Angular CLI Workspaces

One of the least talked about features of Angular 6 is Angular CLI Workspaces. Workspaces or Angular CLI Workspaces give angular developers …

Read More
Angular Reactive Forms – Building Custom Validators

Angular has built-in input validators for common input validation such as checking min and max length, email etc. These built-in validators …

Read More
SVG/Webfont Icon Packs for your Angular App

If you are looking to improve your web app both visually and functionally, icons are a very good place to start. Icons if applied correctly, …

Read More
Lazy Loading Scripts and Styles in Angular

In my earlier post, I covered about lazy loading of angular modules. In this post, I will cover lazy loading of scripts and styles, both …

Read More
Working with Environment Variables in Angular 6

It’s common for developers to have multiple application versions that target different environments i.e. development and production. It’s …

Read More
Top VSCode Extensions for Angular Developers

Since the release of VSCode a few years ago, it has become very popular among developers. VSCode is my favorite text editor and use it every …

Read More

Comments