Control Value Accessor: Custom Form Components in Angular
Tags: angular, webdev, tutorial
Published: 2022-05-30 20:30:21

Custom components controlled by a FormControl.

{% embed https://youtu.be/HiW5CYOw6-Q %}
_See this and many other articles at [lucaspaganini.com](lucaspaganini.com)_

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>
```

## Native 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 `FormControl`s to native input elements.

If you look at the [`FormsModule` source code](https://github.com/angular/angular/tree/master/packages/forms/src/directives), 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.

## Control Value Accessor

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

```ts
@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.

```ts
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`.

```ts
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`.

```ts
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();
  }
}
```

## Providing the `NG_VALUE_ACCESSOR`

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

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.

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

## Conclusion

Everything should be working now. You can [play with the code in this repository](https://github.com/LucasPaganini/published-content/tree/master/control-value-accessor).

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!

## References

1. [Repository _GitHub_](https://github.com/LucasPaganini/published-content/tree/master/control-value-accessor)