Signalové formuláře v Angularu 21: jak Signal API mění práci s formuláři na architektonické úrovni
Angular 21 představil signalové formuláře — nejen novou utilitu, ale zásadní posun v paradigmatu řízení stavu formulářů. Nejde o evoluci reaktivních formulářů, ale o jejich přepracování skrze prizmu Signal API: přímou synchronizaci modelu a uživatelského rozhraní, striktní typizaci bez ztráty kontextu, deklarativní řízení stavu prvků a integrovanou podporu debounce a křížové validace. Funkcionalita je zatím experimentální, ale její architektonická řešení již určují směr vývoje pro enterprise aplikace s vysokými nároky na předvídatelnost a udržitelnost.
Architektonický základ: FieldTree a FieldState místo FormGroup/FormControl
Signalové formuláře opouštějí abstrakci AbstractControl ve prospěch dvou striktně typizovaných primitivů:
FieldTree<T>— proxy objekt představující strom formuláře jako signály. Každý uzel jeSignal<FieldState<T>>, přístup k hodnotě probíhá voláním().FieldState<T>— stav konkrétního pole: hodnota, stav (valid,touched,dirty), chyby a metody řízení (set,reset,markAsTouched).
Toto eliminuje nutnost ručně synchronizovat vstupní data (@Input) a interní stav formuláře. U reaktivních formulářů bylo třeba použít valueChanges + patchValue, což přinášelo riziko nesouladu a komplikovalo správu životního cyklu. U signalových formulářů se model stává zdrojem pravdy přímo:
export interface LoginFormModel {
email: string;
password: string;
}
@Component({
imports: [Field],
template: `
<form>
Email: <input [field]="loginForm.email">
Password: <input [field]="loginForm.password">
</form>
`
})
export class LoginForm {
login = model.required<LoginFormModel>();
loginForm = form(this.login); // Propojí signál modelu se stromem formuláře
}
Zde je this.login WritableSignal<LoginFormModel>, a loginForm.email() vrátí FieldState<string>. Změna login.set({...}) okamžitě aktualizuje všechny propojené FieldState, a naopak změna hodnoty prostřednictvím loginForm.email().set(...) aktualizuje login().
Typizace bez ztrát: od AbstractControl k přesným typům polí
Klíčovou výhodou je zachování struktury typu na všech úrovních. U reaktivních formulářů get('nested.field') vrátí AbstractControl | null, čímž se ztrácí informace o vnořené struktuře. U signalových formulářů má cesta path.additionalInformation.firstName striktní typ FieldState<string>, a path.additionalInformation() — FieldState<{ firstName: string; lastName: string; }>. To umožňuje bezpečné používání effect, computed a dalších primitiv Signal API bez převodů typů a kontrol na undefined.
Vezměme si příklad s grupou additionalInformation:
effect(() => {
const info = this.loginForm.additionalInformation();
console.log(info.firstName().value()); // string — kompilátor ví přesně
console.log(info.lastName().touched()); // boolean
});
Taková typizace vylučuje klasické chyby: pokus o volání .value u null, chyby při práci s vnořenými FormGroup, neviditelné any v řetězcích get().get().value.
Řízení stavu prvků: hidden, disabled, readonly
Signalové formuláře nabízejí tři vestavěné mechanismy řízení viditelnosti a interaktivnosti, každý s vlastní semantikou:
hidden(path, condition?)— kompletně vyřadí pole z DOM (prostřednictvím@if) i ze stromu validace. Nemá vliv natouched/dirty, protože element v formuláři neexistuje.disabled(path, condition?)— činí pole nedostupným pro editaci a vyřazuje ho z validace i ze stavůtouched/dirty.readonly(path, condition?)— blokuje editaci, ale zůstává v validačním cyklu a umožňuje sledovattouched/dirty.
Podmínky se zadávají prostřednictvím funkce, která dostává kontext { valueOf }. Například:
form<{ email: string; password: string }>(
{ email: '', password: '' },
(path) => {
disabled(path.password, ({ valueOf }) => !valueOf(path.email));
}
);
To je ekvivalent password.disabled = !email.value, ale provádí se reaktivně a zaručuje soulad.
Debounce a validace: vestavěná řešení místo RxJS
Pro kontrolu frekvence aktualizace dat je implementována funkce debounce, která přijímá buď timeout v milisekundách, nebo uživatelský Debouncer:
debounce(path, (
{ valueOf },
abortSignal
) => {
return new Promise<void>((resolve) => {
const timeout = setTimeout(() => resolve(), valueOf(path.email).length * 100);
abortSignal.addEventListener('abort', () => clearTimeout(timeout));
});
});
Validace je rozdělena na synchronní (required, minLength, pattern) a asynchronní (validateAsync, validateHttp). Specifikum je podpora validateTree, která umožňuje vytvářet křížová pravidla. Například kontrola shody hesel:
validateTree(path, (ctx) => {
const { password, confirmPassword } = ctx.value();
if (password !== confirmPassword) {
return {
field: ctx.fieldTree.confirmPassword,
kind: 'passwordMismatch',
message: 'Hesla se neshodují'
};
}
});
Zde ctx.fieldTree dává přístup ke všem FieldState uvnitř stromu, a ctx.value() — k aktuální hodnotě celého formuláře.
Co je důležité
- Signalové formuláře nejsou náhradou za reaktivní, ale jejich architektonické přepracování pod Signal API: model a formulář jsou nyní jediným zdrojem pravdy.
- Typizace se udržuje na všech úrovních vnoření, eliminuje
AbstractControla nutnost ruční kontrolyinstanceof. hidden,disabled,readonlymají jasnou semantiku a automaticky řídí validaci i stavytouched/dirty.debounceavalidateTreejsou integrovány do jádra, čímž se odstraňuje závislost na RxJS pro základní scénáře.- Experimentální status znamená, že API se může změnit; použití v production vyžaduje jasné pochopení rizik a plánu migrace.
Angular 21 signalové formuláře jsou krokem k předvídatelnějšímu, typově bezpečnějšímu a méně složitému řízení formulářů. Nezjednodušují jednoduché případy, ale radikálně snižují složitost složitých: vnořených formulářů, podmíněných polí, křížové validace a dynamické synchronizace. Pro middle/senior vývojáře to není nástroj „pro rychlý start“, ale architektonický primitiv, který stojí za hluboké studium — až na úroveň implementace FieldTree a chování effect v kontextu formuláře.
— Editorial Team
Zatím žádné komentáře.