Custom form controls in Template Driven Forms

Problem

Given the below form I am able to manage the data and validation state of my component automatically as driven by the underlying Angular and HTML implementation

import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SampleInput } from './sample-input';

@Component({
  standalone: true,
  selector: 'example-form',
  imports: [CommonModule, FormsModule, SampleInput],
  template: `
    <form #form="ngForm">
      <h1>Example form</h1>

      <sample-form-input
        [required]="true"
        [(ngModel)]="data['isActive']"
        name="Name"
        label="This is a name input"
      />

      <div>
        <button type="submit" [disabled]="!form.form.valid">
          Is Form Valid: {{ form.form.valid }}
        </button>
      </div>
    </form>
    <pre>{{
      {
        data: data,
        value: form.form.value,
        status: form.status
      } | json
    }}</pre>
  `,
})
export class ExampleFormComponent {
  data: Record<string, any> = {};
}

However, as soon as I move my input into a different component I am usually forced to do lots of weird things that steer me further away from a more simplified HTML-directed form. For the sake of simplicity and maintainability however I would like to be able to define a component that is able to take advantage of the Angular/HTML form integration while also providing me with the benefits of a component-based form input

Implementing a ControlValueAccessor

In order to do this, I can move the input into a new component provided that the component implements the ControlValueAccessor interface and the Validation interface if I would also like to use Angular Validation with my component

The basic implementation of a component that meets this requirement can be seen below:

import { Component, forwardRef } from '@angular/core';
import {
  AbstractControl,
  ControlValueAccessor,
  FormsModule,
  NG_VALIDATORS,
  NG_VALUE_ACCESSOR,
  ValidationErrors,
  Validator,
  Validators,
} from '@angular/forms';

@Component({
  standalone: true,
  selector: 'sample-form-input',
  imports: [FormsModule],
  providers: [
    {
        // Tell Angular that we can handle the value management by way of NgModel
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => SampleInput),
      multi: true,
    },
    {
        // Tell angular that we also want to enable validation on our component
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => SampleInput),
      multi: true,
    },
  ],
  template: `
    <!-- Below is an example implementation that meets the UI requirements for
         the form to be template-driven -->

    <input
      class="block"
      type="text"
      [ngModel]="value"
      (ngModelChange)="onChange($event)"
      [disabled]="disabled"
    />
  `,
})
export class SampleInput implements ControlValueAccessor, Validator {
  value?: any;
  disabled: boolean = false;
  focused: boolean = false;

  onChange: any = () => {};
  onTouched: any = () => {};

  writeValue(value: any): void {
    this.value = value;
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

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

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  onBlur(): void {
    this.focused = false;
  }

  onFocus(): void {
    this.focused = true;
  }

  getValidator() {
    return Validators.maxLength(10);
  }

  validate(control: AbstractControl<any, any>): ValidationErrors | null {
    const validator = this.getValidator?.();
    if (!validator) {
      return null;
    }

    return validator(control);
  }
}

Now that we have this component, we can simply swap out the use of input to use our new component in our example form:

// rest of component
template: `
    <form #form="ngForm">
      <h1>Example form</h1>

      <sample-form-input
        [required]="true"
        [(ngModel)]="data['isActive']"
        name="Name"
        label="This is a name input"
      />

<-- rest of template -->
`
// rest of component

The above provides a basis for any input component we want. It is also possible to define the above as a base class that can then be extended by other components to provide a more specific implementation such as working with a generic value to be a bit more type safe or to allow more specific variations or any additional styling to be contained to a specific component