Building a Reusable Form Control

This guide walks through the creation of a custom form control that plugs into Angular's forms API, working seamlessly with both template-driven and reactive forms just like a built-in control such as <input type="text" ../>.

We'll implement a straightforward color picker component and convert it into a reusable form control. Once finished, you can drop this control into any Angular form setup like so:

Template-driven forms:

<color-picker [(ngModel)]="color"></color-picker>
Enter fullscreen mode Exit fullscreen mode

Reactive (model-driven) forms:

<color-picker [formControl]="color"></color-picker>
Enter fullscreen mode Exit fullscreen mode

Alternatively:

<color-picker formControlName="color"></color-picker>
Enter fullscreen mode Exit fullscreen mode

The complete source code is available in this StackBlitz project, or embedded at the end of this article.

https://stackblitz.com/edit/custom-form-field-color-picker?embed=1&file=src/app/app.component.html

Setting Up the Component

Start by defining a basic component with the following structure:

This component is fairly straightforward:

  • It maintains an array called colors with predefined color values. The template loops over this list, rendering a div for each color, styled with that color as its background.
  • A property named selectedColor stores the currently chosen color.
  • Clicking a color triggers the colorClicked method, which updates the selectedColor property.
  • The template applies the CSS class selected to the div representing the active color.

As it stands, this component is functional but isolated. It has no mechanism to notify the surrounding form about changes to the selected color, nor can the form tell the component which color should be selected. To bridge this gap, we need to convert it into a proper Angular form control. That requires two key steps:

  1. Implement the ControlValueAccessor interface so the component behaves as Angular's forms module expects.
  2. Register the component with Angular's forms system by providing it through the NG_VALUE_ACCESSOR injection token.

Transforming Our Component into a Valid Angular Custom Form Control

1- Implementing the ControlValueAccessor Interface

To allow Angular's forms API to communicate with our custom form control, we must implement the ControlValueAccessor interface. If you inspect Angular's source code on GitHub here, you'll find this explanation of the ControlValueAccessor interface:

  • Defines an interface that acts as a bridge between the Angular forms API and a native element in the DOM.

    * Implement this interface to create a custom form control directive * that integrates with Angular forms.

This interface comprises several methods, each of which we'll implement in our component:

  • WriteValue: The forms API calls this method whenever the model value associated with this control changes programmatically. In essence, this is Angular notifying our component that the form value has been altered, and we need to respond accordingly. The method provides the new value via its sole parameter obj, and we must update the UI to reflect it. In our case, we simply assign the new value to the selectedColor property of the color picker component.
writeValue(obj: any): void {
  this.selectedColor = obj;
}
Enter fullscreen mode Exit fullscreen mode
  • registerOnChange: This method offers a channel for communication in the reverse direction. Whereas writeValue informs our component about changes from the external form, we now need a mechanism to inform the external form about changes originating from our component's UI—in this scenario, when the user selects a new color. This method supplies a callback function fn in its parameter, which we should invoke whenever a UI change occurs. To do this, we store the callback in a variable and call it each time the user picks a different color.
private _onChange: any;
registerOnChange(fn: any): void {
    this._onChange = fn; // Save the callback function
}
colorClicked(color: string) {
    this.selectedColor = color;
    this._onChange(this.selectedColor); // Call the saved callback
}
Enter fullscreen mode Exit fullscreen mode
  • registerOnTouched: This method functions similarly to registerOnChange; it provides a callback to notify the form when the control has been touched. Typically, with an input field, you'd call this callback on blur. For our example, we consider the control touched once a new color is selected.
private _onTouch: any;
registerOnTouched(fn: any): void {
    this._onTouch = fn;   // Save the callback function
}
colorClicked(color: string) {
    this.selectedColor = color;
    this._onTouch(true); // Call the saved callback
}
Enter fullscreen mode Exit fullscreen mode
  • setDisabledState: This is the final method to implement. The forms API invokes it whenever the control's disabled status changes. We're expected to react by disabling color selection in our component, so we'll store the value passed to this method each time it's called.
private _isDisabled: boolean;
setDisabledState?(isDisabled: boolean): void {
    this._isDisabled = isDisabled;
}
Enter fullscreen mode Exit fullscreen mode

2- Registering Our Component with the NG_VALUE_ACCESSOR Injection Token

Our component is now prepared to work with Angular's forms API. However, one additional step is required to enable the forms API to recognize it as a legitimate form control and interact with it—this interaction is possible only because we implemented the ControlValueAccessor interface in the preceding step.

Let's first examine the source code for Angular's official FormControlDirective, which is responsible for associating our component with the form, to understand how this connection is established. Looking at the directive's constructor, we find the following:

constructor( ...
    @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[],
...) {...
    selectValueAccessor(this, valueAccessors);  
}
Enter fullscreen mode Exit fullscreen mode

Notice that the directive injects the NG_VALUE_ACCESSOR token and expects it to supply a list of ControlValueAccessor implementations (the interface we just implemented). This value is then stored and used internally.

What does this imply for us? It means that for FormControlDirective to acknowledge our component and interact with it, we must register our component using the NG_VALUE_ACCESSOR injection token. To accomplish this, we need to modify the options of the Component decorator as shown below:

  • We configure the component's injector with the NG_VALUE_ACCESSOR injection token.
  • We then provide our newly created ColorPickerComponent.
  • Next, we employ forwardRef (learn more about forwardRef) because our class isn't defined at this point; this function allows us to reference our component before its definition.
  • Finally, we set multi:true to indicate this is one of potentially many providers for the same token on the same element. This is also essential because it ensures the injector returns an array of instances, which is precisely the type that FormControlDirective expects in its constructor.

Our custom form control is now ready for use in both template-driven and reactive forms. For instance, we could use it in our AppComponent like this:

  • We define a formGroup containing two controls, title and color, and we add an HTML form element with the formGroup directive.
  • The title control uses a simple native input, while the color control uses our newly created color picker component.
  • We use formControlName to bind these controls to our form.
  • Finally, we output the form's value to verify that everything operates correctly when we modify the form input values.

After applying some styling, the final outcome looks like this:

angular color picker

Happy coding, and always keep exploring!


References