In Angular Signal Forms, how do you build a custom phone-number input for a registration form by implementing FormValueControl?
answer
- one required model()
- optional inputs mirror field state
- an output for touched
- no providers, no callbacks
basics
~20 sImplement FormValueControl<string> with value = model(''). [formField] syncs it with the field and fills optional inputs such as disabled, errors and touched; the component emits a touch output on blur. No providers or register callbacks are needed.
solid answer
~40 sA Signal Forms custom control is a component that implements `FormValueControl<T>`. Its only required member is `value = model<T>(...)`; `[formField]` binds the field's value to it both ways. Every other member is optional and declared as an `input()` named after a field-state signal (`disabled`, `readonly`, `hidden`, `errors`, `invalid`, `pending`, `touched`, `dirty`, `required`, `min`, `maxLength`, ...); `[formField]` fills whichever ones exist. To report interaction, the component declares `touch = output<void>()` and emits it on blur. There is no provider token, no `writeValue`, no `registerOnChange`. A boolean toggle implements `FormCheckboxControl` with a `checked` model instead, and neither interface may declare the other's property. Since v22 the same component also works with `[formControl]` and `ngModel`.
code
ts · 29 linesimport {Component, input, model, output} from '@angular/core';
import {FormValueControl, ValidationError, WithOptionalFieldTree} from '@angular/forms/signals';
@Component({
selector: 'app-phone-input',
template: `
<input type="tel"
[value]="value()"
[disabled]="disabled()"
[attr.aria-invalid]="invalid()"
(input)="value.set($any($event.target).value)"
(blur)="touch.emit()" />
@if (touched() && invalid()) {
@for (error of errors(); track $index) {
<small>{{ error.message }}</small>
}
}
`,
})
export class PhoneInput implements FormValueControl<string> {
readonly value = model(''); // required: kept in sync with the field's value
readonly disabled = input(false); // optional: bound from the field's state
readonly invalid = input(false);
readonly touched = input(false);
readonly errors = input<readonly WithOptionalFieldTree<ValidationError>[]>([]);
readonly touch = output<void>(); // emit on blur to mark the field touched
}
// usage: <app-phone-input [formField]="registrationForm.phone" />go deeper
Know that a Signal Forms custom control exposes value = model() and that [formField] is then used on it like on a native input.
Explain the optional state inputs, the touch output, and the difference between FormValueControl and FormCheckboxControl.
Contrast the declarative contract with ControlValueAccessor and plan interoperability for a shared component library used by both form systems.
Set the migration order for a design system: which controls move to FormValueControl first and how long CVA-based controls stay supported.
## Why a new contract Before Signal Forms, a component became a form control by implementing `ControlValueAccessor`: four imperative methods, a multi-provider token and hand-managed callbacks. Signal Forms replaces that with a **declarative contract built from signals**. The form system and the component communicate through ordinary component inputs, outputs and a `model()`. ## The two interfaces | Interface | Required member | Must not declare | Typical use | |---|---|---|---| | `FormValueControl<T>` | `value: ModelSignal<T>` | `checked` | text, number, phone, date picker, select-like widgets | | `FormCheckboxControl` | `checked: ModelSignal<boolean>` | `value` | toggles, switches, custom checkboxes | Both extend `FormUiControl`, which lists the **optional** members. The compiler recognises the kind of control from the model input (`value` or `checked`) when it sees `[formField]` on the element. ## The optional state inputs Declare only what the control needs; `[formField]` binds each one that exists from the field's state: - **interaction:** `touched`, `dirty`; - **validation:** `errors`, `invalid`, `pending`; - **availability:** `disabled`, `disabledReasons`, `readonly`, `hidden`; - **constraints:** `required`, `min`, `max`, `minLength`, `maxLength`, `pattern`; - **metadata:** `name`. These are **read-only from the control's point of view**: the form owns them. The control does not flip its own `touched` input; it announces interaction through an output. ## Reporting touched The contract includes an optional `touch` output. Emit it when the user finishes interacting, normally on the native `blur` event, not on focus. The `FormField` directive listens to it and marks the field touched, which is what makes touched-gated error messages appear and what flushes a `debounce()`-held value into the model. Optional methods complete the picture: `focus()` lets `focusBoundControl()` place the caret in the right inner element, and `reset()` lets the control clear any internal UI state. ## A worked control The phone input in the example keeps its displayed text in `value()`, writes user input with `value.set(...)`, disables its inner `<input>` from the `disabled` input, shows `errors()` after `touched()`, and emits `touch` on blur. Using it looks exactly like a native input: ```html <app-phone-input [formField]="registrationForm.phone" /> ``` Rules stay in the schema, for example `required(path.phone)` and `pattern(path.phone, /^\+?[0-9 ]{7,15}$/)`. Because `[formField]` owns the state inputs, binding `[disabled]` or `[value]` yourself on the same element is rejected by the template type-checker. ## Mistakes to avoid - **Keeping a second copy of the value.** The `value` model *is* the state; mirroring it into a private field reintroduces the sync bugs the contract removes. - **Declaring both `value` and `checked`.** Each interface forbids the other's property; pick the one that matches the data type. - **Emitting `touch` on focus.** The contract asks for it when interaction finishes; emitting on focus marks fields touched before the user has typed anything. - **Writing to state inputs.** `disabled`, `errors`, `touched` and the rest are inputs owned by the form; the control only reads them. - **Validating inside the control.** Rules such as `required` or `pattern` belong in the schema, where they also reach the control through the `required` and `pattern` inputs. ## Compared with a ControlValueAccessor | Concern | ControlValueAccessor | FormValueControl | |---|---|---| | registration | `NG_VALUE_ACCESSOR` provider with `forwardRef` and `multi: true` | none: the interface shape is enough | | model to view | `writeValue()` called imperatively | `value` model updated by the directive | | view to model | stored `onChange` callback | `value.set(...)` | | touched | stored `onTouched` callback | `touch` output | | disabled, errors, required | `setDisabledState()` only; the rest needs `NgControl` | optional inputs bound automatically | ## Interoperability - A `FormValueControl` component can be used by reactive (`[formControl]`, `formControlName`) and template-driven (`ngModel`) forms as it is, so a shared library can migrate controls first. - `[formField]` can still bind an existing `ControlValueAccessor` component for backward compatibility, but the Angular docs recommend native inputs or `FormValueControl` for new controls. - Implement **one** of the two contracts on a component, never both.
- Why should the phone input emit touch on blur rather than set its own touched input to true?Inputs flow from the form into the control; the field state owns `touched`. Emitting the `touch` output asks `[formField]` to mark the field touched, which updates the form's state and then flows back into the control's `touched` input. Setting a local flag would leave the form unaware.
- Your team has a library of ControlValueAccessor components. Must they be rewritten before adopting Signal Forms?No. `[formField]` can bind a component that provides a `ControlValueAccessor` for backward compatibility. For new or reworked controls, `FormValueControl` is preferred, and such controls also work in reactive and template-driven forms, so migration can happen one component at a time.
saying these in an interview costs you the question
- A FormValueControl still needs an NG_VALUE_ACCESSOR provider
- The control should set its own touched input to true on blur
- A toggle can implement FormValueControl with both value and checked models
- FormValueControl components cannot be used with reactive forms
- Every optional state input must be declared for [formField] to accept the component