Wydano kolejną główną aktualizację Angulara, więc najwyższy czas się jej przyjrzeć. Przeanalizujmy, co zostało zmienione i jak możemy wykorzystać te zmiany w naszej codziennej pracy.
Zoneless jest już domyślnie ustawiony w Angularze
Jak zostało wspomniane we wcześniejszym artykule, wraz z wprowadzeniem Angular 20.2 mechanizm wykrywania zmian w trybie zoneless osiągnął stabilny stan. W oparciu o to Angular 21 wprowadza zoneless jako domyślną opcję dla nowych aplikacji, a także przygotowano schematy wspomagające migrację istniejących projektów Angular do trybu zoneless.
Signal Forms
Mamy nadzieję, że każdy deweloper Angulara zna Angular Signals. Jeśli nie – najwyższy czas to nadrobić. Możesz zacząć od przeczytania naszego artykułu: https://angular.love/angular-signals-a-new-feature-in-angular-16.
Prawdopodobnie znasz też Angular Forms. Jeśli nie, sprawdź proszę: https://angular.love/typed-forms-2. Zrozumienie działania sygnałów jest kluczowe przed zagłębieniem się w tę sekcję. Zakładając, że już je znasz, przejdźmy do nowej funkcji Angulara o nazwie Signal Forms.
Stwórzmy bardzo prosty formularz reaktywny
export class App implements OnInit {
private readonly _fb = inject(FormBuilder);
protected form!: PersonForm;
ngOnInit() {
this.initForm();
}
protected onSubmit() {
if (this.form.valid) {
console.log('Form submitted:', this.form.value);
}
}
private initForm() {
this.form = this._fb.group({
name: this._fb.nonNullable.control('',
[
Validators.required,
Validators.minLength(3)
]
),
surname: this._fb.nonNullable.control('',
[
Validators.required,
Validators.maxLength(10)
]
),
telephoneNumber: this._fb.control(
null,
Validators.required
),
});
}
Następnie możemy użyć go w naszej templatce:
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div>
<label>
Name:
<input type="text" formControlName="name" />
</label>
@if(form.controls.name.invalid && form.controls.name.touched) {
<p>Name is required</p>
}
</div>
<div>
<label>
Surname:
<input type="text" formControlName="surname" />
</label>
@if(form.controls.surname.invalid && form.controls.surname.touched)
{
<p>Surname is required</p>
}
</div>
<div>
<label>
Telephone Number:
<input type="number" formControlName="telephoneNumber"/>
</label>
@if(form.controls.telephoneNumber.invalid &&
form.controls.telephoneNumber.touched
) {
<p>Telephone number is required</p>
}
</div>
<button type="submit" [disabled]="form.invalid">Submit</button>
</form>
<pre>{{ form.value | json }}</pre>
`,
Jak widzimy, jest to bardzo prosty przykład. Zainicjalizowaliśmy podstawowy formularz reaktywny z trzema kontrolkami: imię, nazwisko i numer telefonu. W szablonie sprawdzamy, czy użytkownik wchodził w interakcję z daną kontrolką, a jeśli tak to czy spełnione są warunki walidacji. Jeśli nie są, wyświetlamy prosty komunikat o błędzie. Zobaczmy, jak można to zmodyfikować i ulepszyć w nowej wersji Angulara.
Najpierw zaktualizujemy klasę naszego komponentu:
protected readonly person = signal<PersonForm>({
name: '',
surname: '',
telephoneNumber: null
})
protected readonly personForm = form(this.person);
Jak widać, stworzyliśmy prosty sygnał, który pełni rolę modelu dla naszego nowego formularza sygnałowego. Usunęliśmy również metodę OnInit, nie jest już potrzebna. Funkcja form() to nowe API wprowadzone w najnowszej wersji Angulara. Pamiętaj, że gdy zaktualizujemy wartość w personForm, wartość w naszym modelu formularza również zostanie automatycznie zaktualizowana:
changePersonName(value: string) {
this.personForm.name().value.set(value);
console.log(this.person()); // {name: 'John', surname: '', telephoneNumber: null}
}
Przejdźmy teraz do szablonu, aby zobaczyć, jak można sprawić, by formularz działał w jego obrębie. Zanim zaktualizujesz swój formularz, upewnij się, że dyrektywa field – odpowiedzialna za powiązanie pól formularza z komponentami interfejsu użytkownika została prawidłowo zaimportowana. Gdy to będzie gotowe, możemy zaktualizować formularz w następujący sposób:
template: `
<form (ngSubmit)="onSubmit()">
<div>
<label>
Name:
<input [field]="personForm.name" type="text"/>
</label>
</div>
<div>
<label>
Surname:
<input [field]="personForm.surname" type="text"/>
</label>
</div>
<div>
<label>
Telephone Number:
<input [field]="personForm.telephoneNumber" type="number" />
</label>
</div>
<button type="submit" [disabled]="personForm().invalid()">Submit</button>
</form>
<pre>{{ personForm().value() | json }}</pre>
`,
To tak proste, jak w powyższym przykładzie. Prawdopodobnie zauważyłeś, że obsługa błędów została usunięta – zrobiliśmy to celowo, aby pokazać, jak zmienił się mechanizm walidacji. Teraz możemy obsługiwać błędy w prosty sposób, iterując przez możliwe błędy i wyświetlając te, które występują.
protected readonly personForm = form(this.person, (path) => {
required(path.name);
required(path.surname);
minLength(path.name, 3);
maxLength(path.surname, 40);
});
W ten sposób wyświetlamy błędy w naszej templatce:
<div>
<label>
Name:
<input [field]="personForm.name" type="text" />
</label>
@for(err of personForm.name().errors(); track $index) {
@if(err.kind === 'required') {
<p>Name is required</p>
}
}
</div>
Możesz się zastanawiać, co tak naprawdę dzieje się wewnątrz personForm. W rzeczywistości nie ma tu żadnej magii — nasza logika walidacji jest po prostu opakowana funkcją schema. Korzystamy też z nowych narzędzi walidacyjnych, takich jak required(). Wystarczy, że podamy poprawną ścieżkę do każdej funkcji walidacyjnej, którą możemy uzyskać z argumentu przekazywanego do funkcji schema.
Pamiętaj, że Signal Forms wciąż znajdują się w fazie eksperymentalnej, więc zarówno ich API, jak i zachowanie mogą ulec zmianie przed stabilnym wydaniem.
Angular aria – nowa biblioteka UI
Dostępność staje się coraz ważniejszym elementem naszego codziennego procesu tworzenia oprogramowania. To już nie tylko zalecane dobre praktyki – jeśli Twoja aplikacja ma być dostępna na terenie UE, powinna spełniać standardy opisane w tym artykule:
https://angular.love/pl/dostepnosc-cyfrowa-2025-jak-uniknac-kar-i-zyskac-nowych-uzytkownikow
Mając na uwadze rosnącą rolę dostępności, zespół Angulara przygotował nową bibliotekę UI: Angular Aria. Dzięki niej deweloperzy otrzymują kolejne narzędzie do budowania interfejsów użytkownika, obok Angular Material i CDK. Warto pamiętać, że biblioteka znajduje się obecnie w fazie developer preview.
Bibliotekę możesz w prosty sposób dodać do swojego projektu, uruchamiając w terminalu odpowiednią komendę:
npm install @angular/aria
Simple Changes jest typem generycznym
W najnowszej wersji Angular 21 typ SimpleChanges został zaktualizowany tak, aby był typem generycznym. Oznacza to, że możemy teraz jawnie określić typ danych, które przenosi każda właściwość oznaczona dekoratorem @Input(), co pozwala TypeScriptowi wymuszać silniejsze sprawdzanie typów wewnątrz metody ngOnChanges. Wcześniej SimpleChange używał typu any dla previousValue i currentValue, co oznaczało, że deweloperzy nie mieli żadnych gwarancji na etapie kompilacji dotyczących typów przekazywanych wartości. Poniżej możesz zobaczyć, jak działa to obecnie.
export interface User {
userName: string;
age: number;
}
@Component({
//…//
})
export class App {
@Input({required: true}) userName!: string;
@Input({required: true}) age!: number;
ngOnChanges(changes: SimpleChanges<User>) {
if (changes.age) {
const newAge = changes.age.currentValue;
const oldAge = changes.age.previousValue;
const diff = newAge - oldAge;
console.log(`Age increased by ${diff} years`);
}
}
}
HttpClient zapewniony domyślnie
Wraz z wprowadzeniem najnowszej wersji Angulara nie musimy już samodzielnie zapewniać HttpClient w naszej aplikacji. Jest on teraz dostarczany domyślnie. Oznacza to, że podczas tworzenia obiektu konfiguracyjnego możesz pominąć dostarczanie HttpClient, jeśli tylko chcesz.
// import { provideHttpClient } from `@angular/common`
export const appConfig: AppConfig = {
providers: [
...anotherProviders,
// provideHttpClient()
]
}
NgClass directive to style binding — new schematics are on the board
Jak wiemy, używanie ngClass nie jest zalecane; mimo to nadal możesz korzystać z tej dyrektywy w swojej aplikacji. Dobrą praktyką jest jednak jej unikanie. Aby ułatwić i przyspieszyć pracę, zespół Angulara przygotował schemat migracyjny, który automatycznie konwertuje wszystkie użycia ngClass na powiązania klas (class bindings).
Oto, jak wygląda to przed migracją:
@Component({
//…//
imports: [NgClass],
template: `
<button [ngClass]="{
'isNew': isNew()
}">Click me</button> //before migration
`,
})
export class App {
protected readonly isNew = signal(true);
}
Po migracji:
@Component({
//…//
// imports: [NgClass] - is’s no longer needed
template: `
<button [class]="{
'isNew': isNew()
}">Click me</button> //after migration
`,
})
export class App {
protected readonly isNew = signal(true);
}
Jak widać, nie musimy już importować NgClass, co sprawia, że nasz bundle jest mniejszy, a kod krótszy — a więc również przyjemniejszy w czytaniu. Ten schemat migracyjny można uruchomić za pomocą następującego polecenia:
ng generate @angular/core:ngclass-to-class
Migracja dyrektywy NgStyle do powiązań stylów nowe schematy migracyjne
Podobnie jak w przypadku migracji dyrektywy ngClass, przygotowano schemat umożliwiający migrację dyrektywy ngStyle do powiązań stylów.
Jak wcześniej, poniżej znajduje się przykład przed i po migracji:
@Component({
//…//
imports: [NgStyle],
template: `
<button [ngStyle]="{
'border-color': borderColor(),
}">Click me</button> //before migration
`,
})
export class App {
readonly theme = input.required<ColorTheme>();
protected readonly borderColor = computed(() => this.theme() === 'primary' ? 'rgba(0, 0, 0, 0.1)' : 'rgba(0, 0, 0, 0.5)' ));
}
Po migracji:
@Component({
//…//
// imports: [NgStyle], - is’s no longer needed
template: `
<button [style]="{
'border-color': borderColor(),
}">Click me</button> //before migration
`,
})
export class App {
readonly theme = input.required<ColorTheme>();
protected readonly borderColor = computed(() => this.theme() === 'primary' ? 'rgba(0, 0, 0, 0.1)' : 'rgba(0, 0, 0, 0.5)' ));
Jak w poprzednim przykładzie, nie musisz już importować dyrektywy. Możesz uruchomić migrację następującą komendą:
ng generate @angular/core:ngstyle-to-style
KeyValue pipe wspiera opcjonalne klucze
Od najnowszego wydania Angulara można używać pipe keyvalue na obiektach z opcjonalnymi kluczami bez powodowania błędów TypeScript. Ta zmiana poprawia bezpieczeństwo typów i ułatwia pracę z modelami danych, które nie zawsze definiują wszystkie swoje właściwości.
export interface User {
name: string;
surname?: string;
age?: number;
}
@Component({
selector: 'app-root',
imports: [KeyValuePipe],
template: `
@for (prop of user | keyvalue; track $index) {
<p>Property key: {{ prop.key }}, property value: {{ prop.value }}</p>
}
`,
})
export class App {
protected readonly user: User = {
name: 'John',
surname: 'Doe',
age: 37
};
}
Rozszerzenie dla HttpResponse oraz HttpErrorResponse
W najnowszej wersji Angulara klasy HttpResponse i HttpErrorResponse wprowadzają nową właściwość responseType. Właściwość ta udostępnia typ odpowiedzi z natywnego API Fetch (na przykład: ‘basic’, ‘cors’, ‘opaque’ lub ‘opaqueredirect’).
To usprawnienie ułatwia diagnozowanie problemów związanych z CORS i daje lepszy wgląd w kontekst bezpieczeństwa odpowiedzi HTTP, przy jednoczesnym zachowaniu dotychczasowego działania HttpClient.
@Injectable({ providedIn: 'root' })
export class DataService {
private readonly _httpClient = inject(HttpClient);
getData(): Observable<HttpResponse<any>> {
return this.http.get('/api/data', { observe: 'response' }).pipe(
tap(response => {
console.log('Response type:', response.responseType);
if (response.responseType === 'opaque') {
console.warn('CORS issue detected — response is opaque.');
}
})
);
}
}
Vitest jest naszym nowym domyślnym test runnerem
Wraz z wprowadzeniem Angulara 21, Vitest staje się naszym domyślnym test runnerem. Dodatkowo jest on już stabilny wraz z wprowadzeniem najnowszej wersji angular. Jeśli twoje testy wykorzystują Karmę bądź Jest – nie musisz się wahać jeśli chodzi o update wersji Angulara. Wciąż będą one bowiem wspierane. Jakkolwiek eksperymentalna migracja została przygotowana przez zespół Angulara. Możesz z niej skorzystać wpisz komendę w swoim terminalu:
ng g @schematics/angular:refactor-jasmine-vitest
Pełną instrukcję dotyczącą migracji testów znajdziesz tu:
https://angular.dev/guide/testing/migrating-to-vitest
Podsumowanie
Jeśli uruchamiasz aplikację Angular i nie zaktualizowałeś jej jeszcze do wersji 21, to świetny moment, aby to rozważyć. Wersja 21 wprowadza kilka istotnych usprawnień – przede wszystkim pojawienie się Signal Forms. Zmiany te znacząco poprawiają doświadczenie deweloperskie, a w wielu scenariuszach także wydajność.
Aby głębiej zgłębić to, co pojawiło się w tym wydaniu, oraz dowiedzieć się, jak najlepiej wykorzystać te nowości w swoim projekcie, koniecznie zajrzyj do naszej wcześniejszej analizy ewolucji Angulara i jego nowych funkcji.
