Custom Angular Form Inputs

Ryan Mackey-Paulsen
4 min readJul 17, 2019

--

The time will inevitably come when your application grows to the point where you have outgrown the standard HTML form inputs. This can come pretty quickly, but how you deal with it can have long term effects. Luckily, there is an easy way to create custom form inputs that work with formControl and ngModel that is quick and simple to implement.

The Old Way

When it comes to creating a custom input, I have seen many developers hit what seems like a brick wall. They have the data on a model that needs manipulated, but the data doesn’t fit the HTML inputs available. So without using ngModel, they resort to creating some UI and use click events to manipulate the data. Simple enough and it works!

The problem comes from the fact that the rest of the data might be part of a larger form that does use ngModel or formControls. Also, you have to have that click event call a function if you want to do any validation. If the data does change, you also have to then perform any updates and set the form’s state manually.

The most problematic issue is that you have two separate ways to interact with your data that you are maintaining in your form and that can lead to unwanted side effects.

ControlValueAccessor

Luckily for all of us developers, Angular provides a way to build our own custom inputs that will work with the Forms modules just like any other HTML input already does. The key is to implement the ControlValueAccessor. Let’s start by building a custom color picker.

import { Component } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
@Component({
selector: 'color-picker',
template: `
<span class="color"
*ngFor="let color of colors"
[ngStyle]="{backgroundColor: color}"
(click)="writeValue(color)">
</span>
`,
styles: [`
.color {
margin: 2px;
border-radius: 50%;
box-sizing: border-box;
display: inline-block;
cursor: pointer;
height: 24px;
width: 24px;
}
`]
})
export class ColorPickerComponent implements ControlValueAccessor {
value: string;
colors: string[] = [
'#f44336',//red
'#e91e63', //pink
'#9c27b0', //purple
'#3f51b5', //darkblue
'#2196f3', //blue
'#4caf50', //green
'#ffeb3b', //yellow
'#ff9800', //orange
'#607d8b'//grey
];
}

For any Angular development, that should be pretty straight forward, but you should notice that we haven’t implemented the ControlValueAccessor methods. Let’s see what those are.

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

As setDisabledState is optional, we only need to implement 3 of these methods. Looking at the Angular documentation, you should be able to guess what writeValue does, but the register methods might be a little more confusing.

Because we are tapping into the form control methods we will get access to the state of the control, which includes whether the control was interacted with and if its value has been changed. Angular will also update the component’s class list with ng-touched ,ng-valid,ng-dirty and all of those other ones. We can simply register those with pass though methods or we can do more specific functionality in our components if we need.

For the color picker, lets keep it simple and do the following.

private controlValueAccessorChangeFn: (value: any) => void = (value) => {};
private onTouched = () => {};
/* ControlValueAccessor */
writeValue(value: string): void {
this.value = value;
this.renderer.setProperty(this.elmRef.nativeElement, 'value', value);
this.controlValueAccessorChangeFn(this.value);
}
/* ControlValueAccessor */
registerOnChange(fn: (_: any) => void): void {
this.controlValueAccessorChangeFn = (value: any) => {
this.onTouched();
fn(value);
};
}
/* ControlValueAccessor */
registerOnTouched(fn: () => void): void {this.onTouched = fn;}

Here, we created placeholder methods for our change and touched methods. Then, in the register methods we set them to new values. For the OnTouched, we just set the method to what we get passed into the register method. For the OnChange method, we set it to a method that will call our onTouched method (so that ng-touched will be added to the class list when the value changes) and then we pass the value to the function given to the register method.

The writeValue method is responsible for updating the value whenever a change takes place. You can think of this like a setter. Here, I have also used the ElementRef and Renderer2 to reflect the value on the native HTML element. We are also calling the change method with our new value. All that is left is to call the writeValue method whenever we want to change the value, which from our template, is when a color is clicked.

The last thing to do is to add this to the list of value accessor providers within Angular.

import { NG_VALUE_ACCESSOR } from '@angular/forms';const COLOR_PICKER_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ColorPickerComponent),
multi: true
};
@Component({
selector: 'color-picker',
...
providers: [COLOR_PICKER_VALUE_ACCESSOR]
})
export class ColorPickerComponent implements ControlValueAccessor {
...
}

Now, Angular knows about our new custom form control and it can be used like any other input

<color-picker [(ngModel)]="someValue"></color-picker>

Here is the final component in all of its glory:

https://gist.github.com/smarth55/00bcce70e3ed5241d7288781e0ba29fe.js

Wrap Up

I hope this helps anyone out there with form building. If anything, this should help you feel empowered to go out there and make all of the custom inputs and have them work in all of your forms. Validate away and watch for interactions.

If you get stuck with anything, one helpful tip is to see what the Angular team has done. Here is an example of an HTML number input directive that comes with Angular: https://github.com/angular/angular/blob/master/packages/forms/src/directives/number_value_accessor.ts

Also, checkout the source for popular Angular libraries such as Angular Material for inspiration: https://github.com/angular/components

--

--

Ryan Mackey-Paulsen

Web App Developer, Web Component Enthusiast, Musician, Human