Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions _authors/wkulczak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
name: Wojciech Kulczak
title: Wojciech Kulczak
short_name: wkulczak
github: wkulczi
bio: Jestem programistą z kilkuletnim doświadczeniem i zażyłością do frameworków frontendowych. Lubię poznawać nowe technologie i eksperymentować z istniejącymi rozwiązaniami kod. Po godzinach gotuję, dbam o ogród, poszukuję idealnej kawy i winyli a czasem postrzelam z łuku.
image: wkulczak.webp
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
---
layout: post
title: "Czy wiesz, że Angular 21 rozszerza API formularzy o Signal Forms?"
description: Wraz z publikacją Angulara w wersji 21 opracowano nowy system definicji formularzy za pomocą sygnałów.
date: 2026-04-24T08:00:00+01:00
published: true
didyouknow: false
lang: pl
author: wkulczak
image: /assets/img/posts/2026-04-24-czy-wiesz-ze-angular-21-rozszerza-api-formularzy-o-signal-forms/thumbnail.webp
tags:
- angular
- signals
---

Wraz z publikacją Angulara w wersji 21 opracowano nowy system definicji formularzy za pomocą sygnałów,
dostępny w pakiecie @angular/forms/signals. Choć jest to obecnie funkcja eksperymentalna,
wyraźnie wyznacza przyszły kierunek rozwoju frameworka.

To rozwiązanie pozwala na scentralizowany kod, lepsze wsparcie typowania, prostsze definicje własnych komponentów i przejrzystą walidację.

## Dlaczego Signal Forms? Różnica w podejściu do danych
Signal Forms stanowią zmianę paradygmatu w porównaniu do Template-Driven Forms i Reactive Forms. Kluczowe zasady to:

1. **Model jako źródło prawdy**: Zamiast wydobywać dane z wewnętrznych struktur frameworka, to Ty dostarczasz własny sygnał, który formularz jedynie odzwierciedla i synchronizuje
2. **Deklaratywna logika**: Logika walidacji jest opisywana w schemacie.
3. **Strukturalne mapowanie**: Struktura pól odzwierciedla model danych w stosunku 1:1, a struktura formularza jest automatycznie wyprowadzana z modelu danych.
W tym podejściu formularz to hierarchia pól, z której automatycznie **wyprowadzany jest stan** (błędy, stan disabled, touched itp.).
**Kluczowa Różnica**: W Signal Forms model formularza jest **źródłem prawdy** (source of truth), a nie **wynikiem/wyjściem** (output).

**Korzyści w pigułce**:
- **Brak boilerplate'u**: Drastyczna redukcja kodu.
- **Silne typowanie**: Wyprowadzane jest z modelu.
- **Automatyczne dwukierunkowe wiązanie**: Za pomocą dyrektywy `[field]`.
- **Brak subskrypcji**: Wykorzystanie wbudowanej reaktywności sygnałów.

## Tworzenie Formularza
Rekomendowanym sposobem definicji formularza jest związanie go z dedykowanym interfejsem.

```typescript
export interface UserAccountRegistration {
email: string;
password: {
pw1: string;
pw2: string;
}
}
```

```typescript
protected readonly userAccountRegistration = signal(userAccountRegistrationInitValue) // Źródło prawdy
protected readonly userAccountRegistrationForm = form(this.userAccountRegistration, registrationValidationSchema);
```

Metoda `form(model, schema)` tworzy `FieldTree` (drzewo pól), które odzwierciedla model danych.
Aby uzyskać aktualny stan i wartość danego pola, wywołujemy je jako funkcję, otrzymując `FieldState` (np. `form.email().value()`).

### Komponenty w Szablonie
Do połączenia pól formularza z szablonem używamy dyrektywy **Field**.

```html
<input type="email" [field]="userAccountRegistrationForm.email" />
```

### Komponenty Formularza (Custom Controls)
Tworzenie własnych komponentów formularza jest znacząco przyjemniejsze do implementacji niż w Reactive Forms, gdzie wymagany był `ControlValueAccessor`.

Aby tworzony komponent mógł być użyty jako pole formularza z dyrektywą `[field]`, należy zaimplementować jeden z dedykowanych interfejsów:

1. `FormValueControl<T>`: Dla większości pól edytujących pojedynczą wartość (np. pole tekstowe, wybór daty).
2. `FormCheckboxControl`: Dla kontrolek typu checkbox lub toggle, które reprezentują stan boolean (w tym przypadku wymagana jest właściwość checked zamiast value).
Oba interfejsy dziedziczą z `FormUiControl` i wymagają jedynie, aby komponent udostępniał właściwość **value** (lub checked) jako `ModelSignal`.

```typescript
@Component({
selector: 'app-password-strength',
// ...
})
// Wymagana jest implementacja FormValueControl
export class PasswordStrength implements FormValueControl<string> {
// Właściwość 'value' musi być model signal. Model signal to połączenie input i output.
readonly value = model('');
// Opcjonalne: synchronizacja stanu formularza
readonly invalid = input<boolean>(false);
readonly touched = model<boolean>(false);
readonly label = input('Password');

protected changeInput(input: Event) {
const target = input.target as HTMLInputElement;
// Zmiana wartości poprzez set() automatycznie synchronizuje stan z modelem formularza.
this.value.set(target.value);
}

protected markAsTouched() {
this.touched.set(true);
}
}
```

Dyrektywa `[field]` automatycznie łączy ten model z modelem formularza i przekazuje stany takie jak disabled, invalid,
required i errors jako opcjonalne sygnały `input()` do komponentu.

## Walidacje: Schemat Deklaratywny

Walidacje tworzone są za pomocą metody schema, a następnie przekazywane do metody form. Schemat jest definiowany deklaratywnie w jednym miejscu.

Biblioteka wyposażona jest w szereg wbudowanych funkcji walidacyjnych, takich jak `email`, `required`, `minLength`, `maxLength`, `min`, `max` i `pattern`.

```typescript
export const registrationValidationSchema = schema<UserAccountRegistration>((schemaPath) => {
required(schemaPath.email, {message: 'Email is required'});
email(schemaPath.email, {message: 'Invalid email address'});
required(schemaPath.password.pw1, {message: 'Password is required'});
minLength(schemaPath.password.pw1, 4, {message: 'Password must be at least 4 characters long'})...
}
```

### Walidacja Międzypolowa (validateTree)

Możliwe jest powiązanie walidatorów z innymi polami formularza, co jest niezbędne np. do sprawdzania zgodności haseł. Używamy do tego funkcji `validateTree()`.

```typescript
validateTree(schemaPath.password, (ctx) => {
return ctx.value().pw2 === ctx.value().pw1
? undefined
: {
field: ctx.field.pw2, // Przypisanie błędu do konkretnego pola
kind: 'confirmationPassword',
message: 'Entered password must match with the one specified above'
}
});
```

### Walidacja Asynchroniczna (validateAsync)

Signal Forms wspierają walidację asynchroniczną (np. sprawdzanie dostępności nazwy użytkownika lub e-maila na serwerze) za pomocą `validateAsync`.

```typescript
export const registrationValidationSchema= schema((schemaPath) => {
...
// Przykład walidacji asynchronicznej
validateAsync(schemaPath.email, {
params: ({value}) => value(),
factory: (params) => {
const registrationService = inject(RegistrationService);
return resource({
params,
loader: async({params}) => {
return await registrationService.checkEmailTaken(params)
}
});
},
onSuccess: (result) => {
return result ? {
kind: 'mailTaken',
message: 'Mail address is already taken. Please choose another one.'
} : undefined
},
onError: () => undefined
});
});
```

Podczas oczekiwania na wynik walidacji asynchronicznej pole ma ustawiony stan `pending()` na true, co można wykorzystać do wyświetlania informacji zwrotnej w UI.

### Walidacja Warunkowa i Kontrola Stanu

Możemy warunkowo stosować schematy lub kontrolować stan pól (`disabled`, `hidden`, `readonly`) w oparciu o inne wartości w formularzu, używając `applyWhenValue` lub `applyWhen`.

Przykłady kontroli stanu pól:

- Wyłączanie (Disabling):
```typescript
disabled(schemaPath.newsletterTopics, (ctx) => !ctx.valueOf(schemaPath.newsletter));
```
Warto dodać, że w Signal Forms możemy zwracać powód wyłączenia. Powody te są dostępne przez sygnał `disabledReasons()`.

- Ukrywanie (Hiding):
```typescript
hidden(schemaPath.someField, (ctx) => !ctx.valueOf(schemaPath.otherField));
```

Przykładowe użycie:
```typescript
applyWhen(schemaPath, (ctx) => ctx.value().newsletter, (schemaPathWhenTrue) => {
required(schemaPathWhenTrue.newsletterFrequency, {message: 'Select a frequency'});
})
```

### Debouncing (Opóźnianie Aktualizacji)

Aby zapobiec nadmiernej liczbie wywołań API podczas szybkiego wpisywania (np. w walidacji asynchronicznej), możemy łatwo zastosować debouncing za pomocą funkcji `debounce()`:

```typescript
// Opóźnienie aktualizacji wartości pola email o 500ms
debounce(schemaPath.email, 500);
validateAsync(schemaPath.email, { ... });
```

Dzięki temu aktualizacje do modelu i walidatory asynchroniczne są uruchamiane dopiero po ustaniu pisania na dany czas.

### Integracja z Zewnętrznymi Schematami

Zespół Angular umożliwił również walidację za pomocą zewnętrznych bibliotek implementujących reguły walidacyjne za pomocą **Standard Schema** (np. Zod, czy Valibot). Odbywa się to za pomocą helpera `validateStandardSchema`.

```typescript
import {z} from 'zod';
validateStandardSchema(schemaPath.phoneNumber, z.e164("Not a phone number!"))
```

## Zarządzanie Stanem Wysyłania i Błędami Serwera

Do obsługi wysyłania formularza (szczególnie operacji asynchronicznych) zaleca się użycie dedykowanej funkcji `submit()`.

Funkcja `submit()` automatycznie zarządza stanem `submitting()` (dostępnym jako sygnał: `form().submitting()`).

Jeśli podczas zapisu wystąpią błędy serwera, możemy je przypisać z powrotem do konkretnych pól formularza lub do całego formularza, zwracając tablicę obiektów `ValidationErrorWithField`.

```typescript
protected submitForm() {
submit(this.registrationForm, async (form) => {
const errors: ValidationErrorWithField[] = [];
try {
await this.#registrationService.registerUser(form().value);
} catch (e) {
// Przypisanie błędu do konkretnego pola
errors.push({
field: form.username,
kind: 'serverValidation',
message: 'Username is not available.'
});
}
return errors; // Zwrócone błędy serwera są automatycznie dodawane do stanu formularza.
});
return false;
}
```

Dzięki Signal Forms w o wiele prostszy sposób można, implementować złożone reguły walidacyjne (np. asynchroniczne czy między polowe) w jednym, centralnym schemacie.

Ten nowy, reaktywny model znacząco redukuje boilerplate i gwarantuje silne typowanie-cechy, których brakowało w tradycyjnych Reactive Forms.
Choć Signal Forms pozostają funkcją eksperymentalną w Angular v21, stanowią optymalny wybór dla nowych projektów opartych na sygnałach,
oferując lepsze doświadczenie deweloperskie i fine-grained reactivity.

## Stan Signal Forms i Podsumowanie
Signal Forms (dostępne w @angular/forms/signals) zostały wprowadzone w Angular v21 jako funkcja eksperymentalna.
Oznacza to, że API i funkcjonalność mogą ulec zmianie w przyszłych wydaniach, zanim zostaną ustabilizowane.

Zespół Angulara rekomenduje Signal Forms do nowych projektów, pamiętając jednak, że to wciąż funkcja eksperymentalna,
która może ulec zmianie przed stabilizacją.

Mimo to, nowy model reaktywny z Signal Forms rozwiązuje wiele problemów związanych z Reactive Forms:

| Obszar | Reactive Forms | Signal Forms |
|---------------------|--------------------------------------------|---------------------------------------------|
| **Boilerplate** | Duży (FormGroup, FormControl, FormBuilder) | Bardzo niski (Czysty model + schema) |
| **Typowanie** | Wymaga jawnych typów lub Typed Forms | Silne, wyprowadzane z modelu |
| **Reaktywność** | Oparta na Observables (subskrypcje) | Oparta na Signals (Fine-grained reactivity) |
| **Custom Controls** | Wymaga ControlValueAccessor | Wymaga FormValueControl (znacznie prostsze) |
| **Źródło Prawdy** | Hierarchia FormControl/FormGroup | Writable Signal Model (dane użytkownika) |


Signal Forms demonstrują, w jakim kierunku ewoluuje Angular.
Po Template-Driven Forms i Reactive Forms, Signal Forms są **trzecim głównym podejściem** do obsługi formularzy w Angularze,
które ma na celu uczynienie ich bardziej **bezpiecznymi pod kątem typowania, reaktywnymi i deklaratywnymi.**


CodeSandbox: https://codesandbox.io/p/github/wkulczi/ngsignals/master
Binary file added assets/img/authors/wkulczak.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading