Un control custom e o componentă care se comportă ca un <input> într-un formular: are o valoare, poate fi dezactivată, știe când e invalidă sau atinsă. Angular are două contracte: FormValueControl pentru Signal Forms (simplu, bazat pe model()) și clasicul ControlValueAccessor pentru Reactive / Template-driven forms.
| FormValueControl (v22) | ControlValueAccessor | |
|---|---|---|
| Folosit cu | [formField] (Signal Forms) | formControl, formControlName, ngModel |
| Valoare | value = model<T>() | writeValue + registerOnChange |
| Stări | input-uri opționale: disabled, invalid, errors, touched, required… | doar setDisabledState |
| Provider | niciunul | NG_VALUE_ACCESSOR + forwardRef |
| Linii de cod | ~5 | ~20 |
FormValueControl: varianta Signal Forms
Singurul membru obligatoriu e value ca model(). Orice altă stare (disabled, invalid, erori, required, min/max...) o declari ca input() doar dacă ai nevoie de ea — directiva [formField] o completează automat din schema formularului.
import { Component, booleanAttribute, input, model, output } from '@angular/core';
import { FormValueControl } from '@angular/forms/signals';
@Component({
selector: 'app-rating-control',
host: { '[class.invalid]': 'invalid() && touched()', '(focusout)': 'touch.emit()' },
template: `
@for (star of [1, 2, 3, 4, 5]; track star) {
<button type="button" [class.on]="star <= value()" [disabled]="disabled()" (click)="value.set(star)">★</button>
}
`,
})
export class RatingControl implements FormValueControl<number> {
readonly value = model(0); // singurul membru obligatoriu
readonly disabled = input(false, { transform: booleanAttribute }); // opționale: declari doar
readonly invalid = input(false, { transform: booleanAttribute }); // ce folosești
readonly touched = input(false, { transform: booleanAttribute });
readonly touch = output<void>(); // v22: „am fost atins”
}valid: false · touched: false · model: {
"rating": 0,
"comment": ""
}Controale boolean
// Pentru controale boolean există FormCheckboxControl (proprietatea e `checked`, nu `value`)
import { FormCheckboxControl } from '@angular/forms/signals';
export class Switch implements FormCheckboxControl {
readonly checked = model(false);
readonly disabled = input(false);
}
// <app-switch [formField]="settings.newsletter" />ControlValueAccessor: varianta clasică
Dacă aplicația folosește încă Reactive Forms (sau ai o librărie de componente care trebuie să meargă peste tot), implementezi ControlValueAccessor. Sunt patru metode prin care formularul și controlul vorbesc între ele.
import { forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-rating-cva',
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RatingCva), multi: true }],
host: { '(focusout)': 'onTouched()' },
template: `
@for (star of [1, 2, 3, 4, 5]; track star) {
<button type="button" [class.on]="star <= value()" [disabled]="disabled()" (click)="select(star)">★</button>
}
`,
})
export class RatingCva implements ControlValueAccessor {
protected readonly value = signal(0);
protected readonly disabled = signal(false);
private onChange: (v: number) => void = () => {};
protected onTouched: () => void = () => {};
writeValue(v: number | null) { this.value.set(v ?? 0); } // formular → control
registerOnChange(fn: (v: number) => void) { this.onChange = fn; }
registerOnTouched(fn: () => void) { this.onTouched = fn; }
setDisabledState(d: boolean) { this.disabled.set(d); }
protected select(star: number) {
this.value.set(star);
this.onChange(star); // control → formular
}
}
// <app-rating-cva [formControl]="rating" /> sau formControlName="rating" sau [(ngModel)]Interoperabilitate
Compatibilitate (v22):
Control Signal Forms ([formField]) Reactive / Template forms
──────────────────────────────────────────────────────────────────────────────────────
FormValueControl / Checkbox ✓ nativ ✓ (suport template & reactive pentru FVC)
ControlValueAccessor ✓ (interop CVA) ✓ nativ
<input>, <select>, <textarea> ✓ ✓
ngNoCva: atribut care dezactivează un CVA aplicat automat pe un element (v22).Când DA, când NU
- UI de input care nu există nativ: rating, tag input, color picker, date range, editor.
- Un input nativ „îmbrăcat” care trebuie să se lege direct de formular (ex. input de monedă cu formatare).
- Vrei ca validarea, starea disabled și touched să funcționeze automat.
- Doar stilizezi un
<input>→ pune[formField]pe input-ul nativ dintr-o componentă de layout (label + eroare) și proiectează-l. - Un grup de câmpuri (adresă) → mai bine un sub-formular / o schemă reutilizabilă (
apply), nu un control.
Capcane frecvente
- CVA fără
multi: truesau fărăforwardRef→ erori greu de înțeles la runtime. onChangeapelat înwriteValue→ buclă / formular marcat dirty fără acțiunea utilizatorului.- Nu marchezi touched: erorile „după ce ai atins câmpul” nu apar niciodată. Emite
touch(FVC) sau apeleazăonTouched(CVA) la blur. - Accesibilitate: un control custom trebuie să fie focusabil și să aibă roluri ARIA corecte (vezi Angular Aria).