Back to blog
Miloš Krstić3 min read

Custom Form Field with Signal Forms

Custom Form Field with Signal Forms
Click image to enlarge

For years we were relying on the Control Value Accessor.

With Signal Forms comes the new interface we need to implement.

From ControlValueAccessor to FormValueControl

With classic Reactive Forms, if you wanted a custom form field (date picker, rich text editor, dropdown, etc.), you had to:

  • implement ControlValueAccessor
  • register it with NG_VALUE_ACCESSOR
  • manually implement writeValue, registerOnChange, setDisabledState, etc.

With Signal Forms, the contract moves to interfaces instead of opaque callbacks:

  • FormUiControl — optional UI/status inputs (errors, disabled, touched, etc.)
  • FormValueControl<TValue> — the main contract for controls that edit a value

If your component implements FormValueControl<TValue>, the Field directive knows how to bind a field to it.

What these interfaces really mean

FormUiControl

This is a "nice to have" set of inputs. Each property is:

  • optional
  • an InputSignal<…> (or similar)
  • automatically bound by Field if present

Examples:

errors?: InputSignal<ValidationError[]>

→ If you implement it, Field will push validation errors into your component.

disabled?: InputSignal<boolean>

→ If implemented, your component will receive true/false based on field state.

touched?, dirty?, invalid?, pending?, required?, min, max, minLength, maxLength, pattern, etc.
→ All these make it possible to build super smart custom components that fully reflect field state without you manually wiring things up.

You can implement none, some, or all of these. Only value is required — and that’s on the next interface.

FormValueControl<TValue>

This is the core contract for a "normal" custom field:

interface FormValueControl<TValue> extends FormUiControl {
  readonly value: ModelSignal<TValue>;
  readonly checked?: undefined;
}

Key points:

  • value: ModelSignal<TValue> is required
    → This is the two-way binding between your component and the form.
  • checked?: undefined is explicitly disallowed here
    → That’s reserved for a different contract (checkbox style components).

So:

If your component has a value model signal, it can be used with [field].

No writeValue, no registerOnChange, no providers. Just a model signal.

Minimal example: Custom input with Signal Forms

Imagine a simple custom input component:

import { Component, model, input } from '@angular/core';
import { Field } from '@angular/forms/signals';
import type { FormValueControl } from '@angular/forms/signals';

@Component({
  selector: 'app-fancy-input',
  standalone: true,
  template: `
    <label class="flex flex-col gap-1">
      <span class="text-sm font-medium">Fancy input</span>
      <input
        class="border rounded px-3 py-2"
        [value]="value()"
        (input)="value.set($any($event.target).value)"
        [disabled]="disabled() ?? false"
      />
    </label>
  `,
})
export class FancyInputComponent implements FormValueControl<string> {
  // required by FormValueControl
  readonly value = model<string>('');

  // optional, from FormUiControl — Field will bind them if present
  readonly errors = input<ReadonlyArray<{ message: string }> | undefined>();
  readonly disabled = input<boolean>();
  readonly invalid = input<boolean>();
}

Then you can use it in a Signal Form like this:

import { Component, signal } from '@angular/core';
import { form, Field } from '@angular/forms/signals';
import { FancyInputComponent } from './fancy-input.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Field, FancyInputComponent],
  template: `
    <form>
      <app-fancy-input [field]="form.name"></app-fancy-input>
    </form>
  `,
})
export class AppComponent {
  private readonly model = signal({ name: '' });

  protected readonly form = form(this.model);
}

That’s it. No CVA, no NG_VALUE_ACCESSOR.

Struggling with a similar frontend issue?

Get a Frontend Diagnosis
  • Identify performance bottlenecks
  • Spot architectural issues
  • Get actionable recommendations
Run Free Analysis