Formy sygnalowe w Angular 21: jak API Signal zmienia pracę z formami na poziomie architektury
Angular 21 wprowadził formy sygnalowe — nie tylko nową ułatwiającą funkcję, ale fundamentalny przewrót w paradygmat zarządzania stanem form. To nie ewolucja form reaktywnych, lecz ich reinterpretacja przez pryzmat API Signal: bezpośredni synchronizowany model i interfejs użytkownika, ścisła typizacja bez utraty kontekstu, deklaratywne zarządzanie stanem elementów oraz wbudowana obsługa debounce i walidacji międzypolowej. Funkcjonalność jest jeszcze eksperymentalna, jednak jej rozwiązania architektoniczne już wyznaczają kierunek rozwoju aplikacji korporacyjnych o wysokich wymaganiach dotyczących przewidywalności i łatwości utrzymania.
Architektoniczna podstawa: FieldTree i FieldState zamiast FormGroup/FormControl
Formy sygnalowe rezygnują z abstrakcji AbstractControl na rzecz dwóch ściśle typizowanych prymitywów:
FieldTree<T>— obiekt proxy reprezentujący drzewo formy jako sygnały. Każdy węzeł toSignal<FieldState<T>>, a dostęp do wartości odbywa się poprzez wywołanie().FieldState<T>— stan konkretnego pola: wartość, status (valid,touched,dirty), błędy oraz metody zarządzania (set,reset,markAsTouched).
To eliminuje konieczność ręcznej synchronizacji danych wejściowych (@Input) i wewnętrznego stanu formy. W formach reaktywnych wymagano valueChanges + patchValue, co stwarzało ryzyko rozdźwięku i komplikowało zarządzanie cyklem życia. W formach sygnalowych model staje się bezpośrednim źródłem prawdy:
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); // Powiązuje sygnał modelu z drzewem formy
}
Tu this.login to WritableSignal<LoginFormModel>, a loginForm.email() zwraca FieldState<string>. Zmiana login.set({...}) natychmiast aktualizuje wszystkie powiązane FieldState, a odwrotnie — zmiana wartości poprzez loginForm.email().set(...) aktualizuje login().
Typizacja bez strat: od AbstractControl do dokładnych typów pól
Kluczową zaletą jest zachowanie struktury typu na wszystkich poziomach. W formach reaktywnych get('nested.field') zwraca AbstractControl | null, tracąc informacje o zagnieżdżonej strukturze. W formach sygnalowych droga path.additionalInformation.firstName ma ściśle określony typ FieldState<string>, a path.additionalInformation() — FieldState<{ firstName: string; lastName: string; }>. Dzięki temu można bezpiecznie używać effect, computed i innych prymitywów API Signal bez konieczności castowania typów i sprawdzania undefined.
Rozważmy przykład z grupą additionalInformation:
effect(() => {
const info = this.loginForm.additionalInformation();
console.log(info.firstName().value()); // string — kompilator wie dokładnie
console.log(info.lastName().touched()); // boolean
});
Taka typizacja wyeliminuje klasyczne błędy: próby wywołania .value przy null, błędów przy pracy z zagnieżdżonymi FormGroup czy niewidocznych any w łańcuchach get().get().value.
Zarządzanie stanem elementów: hidden, disabled, readonly
Formy sygnalowe oferują trzy wbudowane mechanizmy zarządzania widzialnością i interaktywnością, każdy ze swoją semantyką:
hidden(path, condition?)— całkowicie wyklucza pole z DOM (poprzez@if) i z drzewa walidacji. Nie wpływa natouched/dirty, ponieważ element nie istnieje w formie.disabled(path, condition?)— czyni pole niedostępne do edycji i wyklucza je z walidacji oraz stanówtouched/dirty.readonly(path, condition?)— blokuje edycję, ale pozostawia pole w cyklu walidacyjnym i umożliwia śledzenietouched/dirty.
Warunki definiowane są poprzez funkcję dostarczającą kontekst { valueOf }. Na przykład:
form<{ email: string; password: string }>({
email: '', password: ''
},
(path) => {
disabled(path.password, ({ valueOf }) => !valueOf(path.email));
}
);
To ekwiwalent password.disabled = !email.value, ale realizowane reaktywnie i gwarantujące spójność.
Debounce i walidacja: wbudowane rozwiązania zamiast RxJS
Dla kontroli częstotliwości aktualizacji danych zaimplementowano funkcję debounce, która przyjmuje albo czas oczekiwania w milisekundach, albo niestandardowego Debouncer:
debounce(path, (
{ valueOf },
abortSignal
) => {
return new Promise<void>((resolve) => {
const timeout = setTimeout(() => resolve(), valueOf(path.email).length * 100);
abortSignal.addEventListener('abort', () => clearTimeout(timeout));
});
});
Walidacja podzielona jest na synchroniczną (required, minLength, pattern) i asynchroniczną (validateAsync, validateHttp). Cechą charakterystyczną jest wsparcie dla validateTree, umożliwiające tworzenie reguł międzypolowych. Na przykład sprawdzenie zgodności haseł:
validateTree(path, (ctx) => {
const { password, confirmPassword } = ctx.value();
if (password !== confirmPassword) {
return {
field: ctx.fieldTree.confirmPassword,
kind: 'passwordMismatch',
message: 'Hasła nie zgadzają się'
};
}
});
Tu ctx.fieldTree daje dostęp do wszystkich FieldState wewnątrz drzewa, a ctx.value() — do aktualnej wartości całej formy.
Co jest ważne
- Formy sygnalowe to nie zastąpienie form reaktywnych, lecz ich architektoniczna przebudowa pod API Signal: model i forma stają się jedynym źródłem prawdy.
- Typizacja zachowana na wszystkich poziomach zagnieżdżenia, eliminując
AbstractControli konieczność ręcznego sprawdzaniainstanceof. hidden,disabled,readonlymają jasną semantykę i automatycznie kontrolują walidację oraz stanytouched/dirty.debounceivalidateTreezintegrowane są w rdzeń, usuwając zależność od RxJS w podstawowych scenariuszach.- Eksperymentalny status oznacza, że API może ulec zmianie; stosowanie w produkcji wymaga jasnego zrozumienia ryzyka i planu migracji.
Formy sygnalowe w Angular 21 to krok ku bardziej przewidywalnemu, typobezpiecznemu i mniej słowotrawnemu zarządzaniu formami. Nie upraszczają prostych przypadków, ale radykalnie redukują trudność skomplikowanych: zagnieżdżonych form, warunkowych pól, walidacji międzypolowej i dynamicznej synchronizacji. Dla programistów średnio- i wysokiego szczebla to nie narzędzie „do szybkiego startu”, lecz prymityw architektoniczny, który warto studiować w głębi — aż do poziomu implementacji FieldTree i zachowania effect w kontekście formy.
— Editorial Team
Brak komentarzy.