creating custom form control
Feb 25 2019 03:23pm | | Share

Creating Angular Custom Form Controls

By Alain Thibodeau

Angular forms have come a long way and the Angular team continues to make them better. Whether you choose to use template-driven or reactive forms, there will come a time when you need to create a custom control to enhance user experience or satisfy requirements.

 

In this exercise, we will create a custom dropdown control using the ngbDropdown component from the ng-bootstrap library. The set-up of bootstrap and ng-bootstrap in Angular is outside the scope of this post. If you are unfamiliar with these libraries, you can follow the instructions in their respective documentations.

 

At the end of this exercise, your form should look like this:

angular form

 

Start off by creating our custom form control. We create an Angular component which will wrap the ngbDropdown component and implement Angular’s ControlValueAccessor. As explained in Angular’s documentation, ControlValueAccessor is an interface that behaves as the bridge between Angular’s forms API and the native element. In our case, our underlying element is the ngbDropdown component.

 

Let’s take a look at the template of our custom form control.

 

<div ngbDropdown class="d-inline-block">

   <button type="button" [disabled]="isDisabled" class="btn btn-block btn-outline-secondary" ngbDropdownToggle>

      <span *ngIf="value; else noItem">

         {{ value }}

      </span>

      <ng-template #noItem>{{ defaultText }}</ng-template>

   </button>

 

   <div ngbDropdownMenu class="dropdown-menu">

      <button type="button" [disabled]="isDisabled" class="dropdown-item" *ngFor='let item of items' (click)="selectItem(item)">

         {{ item }}

      </button>

   </div>

</div>

 

We’ve added a few extra features to the standard ngbDropdown component:

- “isDisabled” flag that disables the dropdown.

- “defaultText” string that shows when there is no selection

- “selectItem” function that handles the selected item when the user clicks on it.

 

Now let’s take a look at the component class which is a little bit more involved.

 

import { Component, forwardRef, Input } from '@angular/core';

import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

 

@Component({

    selector: 'dropdown',

    templateUrl: './dropdown.component.html',

    styleUrls: ['./dropdown.component.scss'],

    providers: [

        {

            provide: NG_VALUE_ACCESSOR,

            useExisting: forwardRef(() => DropdownComponent),

            multi: true

        }

    ]

})

export class DropdownComponent implements ControlValueAccessor {

    @Input() items: string[];

    @Input() defaultText = 'Select Item';

 

    public value: string;

    public isDisabled: boolean;

 

    private onChange;

 

    registerOnChange(fn: any): void {

        this.onChange = fn;

    }

 

    registerOnTouched(fn: any): void {

    }

 

    setDisabledState(isDisabled: boolean): void {

        this.isDisabled = isDisabled;

    }

 

    writeValue(value: any): void {

        this.value = value;

    }

 

    selectItem(item): void {

        this.onChange(item);

        this.value = item;

    }

 

}

 

First we provide this class as an NG_VALUE_ACCESSOR, then the class needs to implement the ControlValueAccessor interface in order to interact with Angular’s forms API.


Here are the relevant parts:

- writeValue: Takes care of updating our selected value set by Angular.

- registerOnChange: Upon initialization of the component this registers a callback function to be called when an update occurs from the view.

- registerOnTouched: Although we are not doing anything with it, this is also called upon initialization and registers a callback function for when the control is blurred or touched.

- setDisabledState: This is called when the control changes its disabled state. It’s a hook for us to update our DOM based on the new disabled value. This occurs when calling this.form.controls['role'].disable() for example.

 

Now that the interface is satisfied let’s take a look at the inputs. This component accepts an array with items we display in our dropdown component. The other input is ‘defaultText’ which allows us to set some default text when there are no values selected. This is defaulted to 'Select Item'.

 

We are now ready to use this custom control in a form, we can use it just like any other Angular form control as show below:

 

<form [formGroup]="form" (submit)="submitForm()">

   <input type="text" placeholder="First Name" formControlName="firstName"><br/>

   <input type="text" placeholder="Last Name" formControlName="lastName"><br/>

   <dropdown formControlName="role" defaultText="Select a role" [items]="roles"></dropdown>

  

   <button class="btn btn-sm btn-primary" type="submit" [disabled]="!form.valid">Submit</button>

</form>

 

As you can see, we use dropdown directly in our reactive form and assign it a ‘formControlName’. We also pass it the items we want to display and the default string via ‘defaultText’ to display when there is no item selected.

 

The class would then look like this:

 

public form: FormGroup;

public roles: string[] = ['Developer', 'Manager', 'Partner'];

 

constructor(private fb: FormBuilder) {

    this.form = this.fb.group({

        'firstName': [],

        'lastName': [],

        'role': ['', Validators.required]

    });

}

 

We can then get the value of the role at any time within our form model.

 

If your form is template-driven, then our custom control component should also work just as expected and you can use it like any other form control.

 

<form #form2="ngForm">

   <dropdown defaultText="Select Role" [(ngModel)]="role"  [items]="roles" name="role"></dropdown>

</form>

 

As you see it is not that difficult to create your own Angular custom form control. By implementing the ControlValueAccessor interface we can create a custom form control that you can use in reactive or template-driven forms.