Control Value Accessor: Custom Form Components in Angular

Custom components controlled by a FormControl

Angular allows us to control form inputs using the FormsModule or the ReactiveFormsModule. With them, you can bind a FormControl to your input and control its value.

HTML
<input type="text" [(ngModel)]="name" />
<input type="text" [formControl]="nameControl" />

But what if you create your own custom component? Like a datepicker, a star rating, or a regex input. Can you bind a FormControl to it?

HTML
<app-datepicker [(ngModel)]="date"></app-datepicker>
<app-datepicker [formControl]="dateControl"></app-datepicker>

<app-stars [(ngModel)]="stars"></app-stars>
<app-stars [formControl]="starsControl"></app-stars>

<app-regex [(ngModel)]="regex"></app-regex>
<app-regex [formControl]="regexControl"></app-regex>

LinkNative Inputs and FormControls

Your first guess may have been to add an @Input() in your component to receive the formControl. That would work, but not when using formControlName or [(ngModel)].

What we really want is to reuse the same logic that Angular uses for binding FormControls to native input elements.

If you look at the FormsModule source code, you'll see directives for the native input elements implementing an interface called ControlValueAccessor.

This interface is what allows the FormControl to connect to the component.

LinkControl Value Accessor

Let's create a simple date input component to test this out. Our component needs to implement the ControlValueAccessor interface.

TypeScript
@Component({
  selector: 'app-date-input',
  ...
})
export class DateInputComponent implements ControlValueAccessor {
  public readonly dayControl = new FormControl();
  public readonly monthControl = new FormControl();
  public readonly yearControl = new FormControl();
}

This interface defines 4 methods:

  1. writeValue(value: T | null): void
  2. registerOnChange(onChange: (value: T | null) => void): void
  3. registerOnTouched(onTouched: () => void)
  4. setDisabledState(isDisabled: boolean): void

registerOnChange receives a callback function that you need to call when the value changes. Similarly, registerOnTouched receives a callback function that you need to call when the input is touched.

TypeScript
private _onChange = (value: Date | null) => undefined;
public registerOnChange(fn: (value: Date | null) => void): void {
  this._onChange = fn;
}

private _onTouched = () => undefined;
public registerOnTouched(fn: () => void): void {
  this._onTouched = fn;
}

public ngOnInit(): void {
  combineLatest([
    this.dayControl.valueChanges,
    this.monthControl.valueChanges,
    this.yearControl.valueChanges,
  ]).subscribe(([day, month, year]) => {
    const fieldsAreValid =
      this.yearControl.valid &&
      this.monthControl.valid &&
      this.dayControl.valid;
    const value = fieldsAreValid ? new Date(year, month - 1, day) : null;

    this._onChange(value);
    this._onTouched();
  });
}

writeValue is called when the FormControl value is changed programmatically, like when you call FormControl.setValue(x). It can receive anything, but if you're using it correctly, it should only receive T (T = Date in our case) or null.

TypeScript
public writeValue(value: Date | null): void {
    value = value ?? new Date();

    const day = value.getDate();
    const month = value.getMonth() + 1;
    const year = value.getFullYear();

    this.dayControl.setValue(day);
    this.monthControl.setValue(month);
    this.yearControl.setValue(year);
  }

The last method is optional. setDisabledState() is called when the FormControl status changes to or from the disabled state.

This method receives a single argument indicating if the new state is disabled. If it was disabled, and now it's enabled, it's called with false. If it was enabled, and now it's disabled, it's called with true.

TypeScript
public setDisabledState(isDisabled: boolean): void {
  if (isDisabled) {
    this.dayControl.disable();
    this.monthControl.disable();
    this.yearControl.disable();
  } else {
    this.dayControl.enable();
    this.monthControl.enable();
    this.yearControl.enable();
  }
}

LinkProviding the NG_VALUE_ACCESSOR

The last step to make this work is to tell Angular that our component is ready to connect to FormControls.

All classes that implement the ControlValueAccessor interface are provided through the NG_VALUE_ACCESSOR token. Angular uses this token to grab the ControlValueAccessor and connect the FormControl to it.

So, we'll provide our component in this token and Angular will use it to connect to the FormControl.

By the way, since we're providing our component before its declaration, we'll need to use Angular's forwardRef() function to make this work.

TypeScript
@Component({
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => DateInputComponent),
      multi: true,
    },
  ],
  ...
})
export class DateInputComponent implements ControlValueAccessor { ... }

LinkConclusion

Everything should be working now. You can play with the code in this repository.

There's another thing I'd like to do with our custom date input: I want it to validate the inputs. February 31 is not a valid date, and we shouldn't be accepting that.

Also, I only want to accept business days. For that, we'll need a synchronous validation to see if it's a weekday and an asynchronous validation to consult an API and see if it's not a holiday.

We'll do that in another article.

Have a great day, and I'll see you soon!

LinkReferences

  1. Repository GitHub

Join our Newsletter and be the first to know when I launch a course, post a video or write an article.

This field is required
This field is required