Powrót do strony głównej

Formy sygnałowe Angular 21: architektura i typizacja

Artykuł szczegółowo omawia formy sygnałowe w Angular 21: architektoniczne prymitywy FieldTree i FieldState, zalety typizacji, zarządzanie stanem elementów (hidden/disabled/readonly), wbudowany debounce i walidacja między polami poprzez validateTree.

Angular 21: formy sygnałowe bez abstrakcji
Advertisement 728x90

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ł to Signal<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:

Google AdInline article slot
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:

Google AdInline article slot
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 na touched/dirty, ponieważ element nie istnieje w formie.
  • disabled(path, condition?) — czyni pole niedostępne do edycji i wyklucza je z walidacji oraz stanów touched/dirty.
  • readonly(path, condition?) — blokuje edycję, ale pozostawia pole w cyklu walidacyjnym i umożliwia śledzenie touched/dirty.

Warunki definiowane są poprzez funkcję dostarczającą kontekst { valueOf }. Na przykład:

Google AdInline article slot
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 AbstractControl i konieczność ręcznego sprawdzania instanceof.
  • hidden, disabled, readonly mają jasną semantykę i automatycznie kontrolują walidację oraz stany touched/dirty.
  • debounce i validateTree zintegrowane 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

Advertisement 728x90

Czytaj dalej