diff --git a/_authors/wkulczak.md b/_authors/wkulczak.md new file mode 100644 index 000000000..fe1f19893 --- /dev/null +++ b/_authors/wkulczak.md @@ -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 +--- diff --git a/_posts/pl/2026-04-24-czy-wiesz-ze-angular-21-rozszerza-api-formularzy-o-signal-forms.md b/_posts/pl/2026-04-24-czy-wiesz-ze-angular-21-rozszerza-api-formularzy-o-signal-forms.md new file mode 100644 index 000000000..38e412ba4 --- /dev/null +++ b/_posts/pl/2026-04-24-czy-wiesz-ze-angular-21-rozszerza-api-formularzy-o-signal-forms.md @@ -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 + +``` + +### 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`: 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 { + // 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(false); + readonly touched = model(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((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 \ No newline at end of file diff --git a/assets/img/authors/wkulczak.webp b/assets/img/authors/wkulczak.webp new file mode 100644 index 000000000..10cd8804b Binary files /dev/null and b/assets/img/authors/wkulczak.webp differ diff --git a/assets/img/posts/2026-04-24-czy-wiesz-ze-angular-21-rozszerza-api-formularzy-o-signal-forms/thumbnail.webp b/assets/img/posts/2026-04-24-czy-wiesz-ze-angular-21-rozszerza-api-formularzy-o-signal-forms/thumbnail.webp new file mode 100644 index 000000000..095819f84 Binary files /dev/null and b/assets/img/posts/2026-04-24-czy-wiesz-ze-angular-21-rozszerza-api-formularzy-o-signal-forms/thumbnail.webp differ