Custom Form Inputs with ControlValueAccessor

Published on Jan 04, 2023

tldr;

There are times when the default inputs available to us in the browser don’t meet our needs or need to be included in a reusable Angular component. You may want to create a custom checkbox or radio button, or just want to use a card with a button as a form input. In any of these situations, you can use the Control Value Accessor to allow an Angular component to communicate with your form. In this article, we’ll look at an example of using control value accessor.

Okay, Here’s the Situation

Bonus points if you get the reference for the heading of this section. 😉

I was recently working on a form where the user was going to need to select an option, and only one option, like you would normally use a radio button for. This is what it looked like:

Demo form inputs

The user will choose one of these items, and when they select it the form goes to another step. I needed their selection to be stored as a value in the form as well. I could have used an Output that patched the value of the form, but I wanted it to be more semantic and obvious about what was going on. To get the value to store in the form, I needed a custom form input, where each of the above cards was the input. I could wrap them in an Angular component and loop over a list of options that were available to the user.

This is where I remembered a talk from Jennifer Wadella at ng-conf 2019 that talked about using ControlValueAccessor. There is a lot of content out there, and I encourage you to read more about it (and watch the video from Jennifer’s talk). This example will not go into all the details of what ControlValueAccessor can do, but it will give you an example and get you started.

Solution

The first step in creating our custom input with control value accessor is to implement the ControlValueAccessor interface. This requires us to implement three methods: writeValue, registerOnChange, and registerOnTouched.

export class CustomInputComponent implements ControlValueAccessor {
  @Input() option: any;

  onChanged: Function = () => {};
  onTouched: Function = () => {};

  registerOnChange(fn: Function) {
    this.onChanged = fn;
  }

  registerOnTouched(fn: Function) {
    this.onTouched = fn;
  }

  writeValue(value: any): void {}
}

registerOnChange

This sets the callback function that the forms API will call when the form value changes. You must provide a function that you will call when the user interacts with the input and the value should be changed. In this case, when the user interacts with our component and we want to update the changed value, we will call this.onChanged(value);.

registerOnTouched

This is similar to the registerOnChange method, but runs when the input is touched by the user. To register the touch, we will call this.onTouched();.

onWriteValue

This method is used to write a new value to the element. In my case, I didn’t need to implement this method, but you may need to do something like this in your case:

writeValue(value: any): void {
  this._renderer.setProperty(this._elementRef.nativeElement, 'value', value);
}

This would write the value that is passed in to the method to the native input’s value in the DOM.

setDisabledState

This is an optional method that you can implement, and it allows you to run a block of code when the forms API sets the input to or from disabled.

Setting the Providers for the Component

After implementing the required methods for the ControlValueAccessor interface we need to set some providers for the component. The providers are set in an array in the component metadata:

export const CUSTOM_CONROL_VALUE_ACCESSOR: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CustomInputComponent),
    multi: true,
};

@Component({
  selector: 'app-custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.css'],
  providers: [CUSTOM_CONROL_VALUE_ACCESSOR]
})

Essentially this turns our component into the custom input for the forms API to interact with.

Interacting with the Component

Now that we have our card (as shown above) and the ControlValueAccessor is implemented, we need to make sure that the user can select the input by clicking on the “Choose this Option” button. This part is pretty simple, though. All we need to do is run a function when the click event occurs, something like this:

<!-- custom-input.component.html -->
<button (click)="onChanged(option.id)">Choose this Option</button>

Now when the user clicks on the button, the form’s value will update to the value of option.id.

Setting the formControlName

Now that we have our custom input, we need to implement it in the parent component with the form. This is actually easy as well after doing the other work. If you’ve set up your ControlValueAccessor, all you need to do is add the formControlName attribute to the component in the HTML file.

<!-- app.component.html -->
<form [formGroup]="form">
  <app-custom-input *ngFor="let option of options" [option]="option" formControlName="option"></app-custom-input>
</form>

As long as your FormGroup has an attribute called option, then clicking on the card will set the option’s ID as the value in the form.

Demo App

You can check out this StackBlitz for a working example of this custom input component using control value accessor.

Conclusion

I have been working with reactive forms for a few years now, and only just implemented ControlValueAccessor for the first time. You may not need to use this technique frequently, but in the case that you do need to implement it, don’t worry about the difficulty. In just a few steps (as outlined above) you can get it working.

I’ve shown the example of using reactive forms, but the steps above should work the same if you’re using template driven forms. Instead of using formControlName on the <app-custom-input> tag, you’ll use [(ngModel)].