Lekcja 9. Angular I — architektura komponentowa i szablony
Trudny / egzaminacyjnyPo co się tego uczymy?
Czas na główny framework tego działu. Angular to sposób budowania interfejsu z małych, niezależnych "klocków" (komponentów), które można łączyć w większe całości — dokładnie ta sama idea, co komponenty Blazor, do których być może zajrzysz w dodatkowym bloku na końcu tego działu, tylko tutaj realizowana w TypeScript. Ta lekcja pokazuje anatomię pojedynczego komponentu i podstawową składnię szablonów HTML rozszerzoną przez Angulara.
Teoria
Komponent — podstawowa jednostka budowy w Angularze. Każdy fragment interfejsu (przycisk, formularz, cała strona) to komponent — trójka powiązanych plików: .ts (logika w TypeScript), .html (szablon — widok), .css (style TYLKO dla tego komponentu, nie wyciekające do reszty aplikacji). Widziałeś to już pobieżnie w lekcji 4 — teraz czas zrozumieć to dokładnie.
Dekorator @Component. Nad klasą komponentu w TypeScript umieszcza się dekorator (specjalną adnotację) opisującą metadane komponentu:
| Właściwość dekoratora | Znaczenie |
|---|---|
selector |
nazwa znacznika HTML, pod którym ten komponent można wstawić w INNYM komponencie, np. <app-produkt></app-produkt> |
standalone: true |
nowoczesny styl komponentu — nie wymaga rejestracji w dodatkowym pliku "modułu" (starszy styl Angulara) |
imports: [...] |
lista innych komponentów/modułów, z których TEN komponent korzysta w swoim szablonie |
templateUrl / template |
ścieżka do pliku .html (albo szablon wpisany bezpośrednio jako string) |
styleUrl / styleUrls |
ścieżka do pliku(ów) .css z lokalnymi stylami |
Interpolacja — wypisywanie wartości w szablonie. Podwójne klamry {{ wyrazenie }} wypisują wartość pola/właściwości z klasy TypeScript bezpośrednio w HTML — bezpośredni odpowiednik @Model.Właściwość z Razor albo @zmienna z Blazor: <h1>{{ tytul }}</h1>.
Dyrektywy strukturalne — @if i @for. Nowoczesny Angular (od wersji 17+) wprowadza własną, wbudowaną składnię sterowania renderowaniem, bardzo podobną do Razor/Blazor:
| Składnia | Działanie |
|---|---|
@if (warunek) { ... } @else { ... } |
warunkowe renderowanie fragmentu szablonu |
@for (item of lista; track item.id) { ... } |
pętla renderująca fragment DLA KAŻDEGO elementu tablicy — track wskazuje unikalny identyfikator, pomagający Angularowi efektywnie odświeżać listę |
@switch (wartosc) { @case (x) { ... } @default { ... } } |
wybór jednego z kilku wariantów renderowania |
(Starsze projekty Angular używają zamiast tego dyrektyw *ngIf i *ngFor — działają identycznie, ale wymagają importu CommonModule; nowa składnia @if/@for jest wbudowana i zalecana w nowych projektach).
Komponenty potomne — komunikacja między "klockami". Duży komponent (np. lista produktów) może w swoim szablonie użyć MNIEJSZYCH komponentów (np. pojedynczej karty produktu) — <app-karta-produktu></app-karta-produktu> — dokładnie ta sama idea, co komponenty potomne w Blazor, tylko w składni Angulara; pełne przekazywanie danych między nimi (@Input/@Output) poznasz w kolejnej lekcji.
Schemat
Plik .ts (logika, TypeScript) Plik .html (szablon, widok)
@Component({ <h1>{{ tytul }}</h1>
selector: "app-produkty", │
templateUrl: "./produkty.html" @if (produkty.length === 0) {
}) <p>Brak produktów.</p>
export class ProduktyComponent { } @else {
tytul = "Lista produktów"; <ul>
produkty = [...]; @for (p of produkty; track p.id) {
} <li>{{ p.nazwa }} - {{ p.cena }} zł</li>
│ }
│ łączy dane z widokiem </ul>
└──────────────────────────────► }
Komponent nadrzędny: Komponent potomny:
<app-lista-produktow> <app-karta-produktu>
używa wewnątrz szablonu: (osobny plik .ts + .html + .css)
<app-karta-produktu></app-karta-produktu>
Przykład z życia
Strona sklepu internetowego zbudowana w Angularze to dziesiątki małych komponentów: nagłówek, stopka, karta produktu (powtórzona wielokrotnie na liście), formularz wyszukiwania — każdy z nich to osobny, niezależny "klocek", który można rozwijać i testować oddzielnie, a potem złożyć w całość, dokładnie tak jak klocki LEGO składają się w większą konstrukcję.
produkty.component.html
<!-- produkty.component.html -->
<div class="container mt-4">
<h1>{{ tytul }}</h1>
@if (produkty.length === ) {
<p>Brak produktów do wyświetlenia.</p>
} @else {
<ul class="list-group">
@for (produkt of produkty; track produkt.id) {
<li class="list-group-item d-flex justify-content-between">
<span>{{ produkt.nazwa }}</span>
<strong>{{ produkt.cena.toFixed(2) }} zł</strong>
</li>
}
</ul>
<p class="mt-2">Łącznie produktów: {{ produkty.length }}</p>
}
</div>
produkty.component.ts
// produkty.component.ts
import { Component } from '@angular/core';
interface Produkt {
id: number;
nazwa: string;
cena: number;
}
@Component({
selector: 'app-produkty',
standalone: true,
templateUrl: './produkty.component.html',
styleUrl: './produkty.component.css'
})
export class ProduktyComponent {
tytul = 'Lista produktów';
produkty: Produkt[] = [
{ id: 1, nazwa: 'Klawiatura', cena: 89.99 },
{ id: 2, nazwa: 'Monitor', cena: 799.00 },
{ id: 3, nazwa: 'Myszka', cena: 45.50 }
];
}
Komentarz i wyjaśnienie kodu
interface Produkt opisuje kształt pojedynczego produktu (poznane w lekcji 6), a produkty: Produkt[] deklaruje TABLICĘ obiektów tego typu — TypeScript teraz "wie", że każdy element tej tablicy ma właściwości id/nazwa/cena, i podpowie/zabezpieczy przed literówką przy odwołaniu się do nieistniejącej właściwości w szablonie.
@for (produkt of produkty; track produkt.id) — składnia track produkt.id jest OBOWIĄZKOWA w nowym Angularze (w przeciwieństwie do starszego *ngFor, gdzie trackBy było opcjonalne) — informuje mechanizm renderowania, jak rozpoznać, KTÓRY konkretnie element listy się zmienił, żeby zaktualizować tylko jego fragment DOM, a nie przerysowywać całej listy od nowa przy każdej zmianie.
Wyrażenie produkt.cena.toFixed(2) wewnątrz interpolacji {{ }} pokazuje, że wewnątrz podwójnych klamer można wywoływać zwykłe metody TypeScript/JavaScript (tu: formatowanie liczby do dwóch miejsc po przecinku) — nie tylko wypisywać "gołe" wartości pól.
Ćwiczenie samodzielne
Utwórz nowy komponent standalone (ng generate component produkty --standalone) z tablicą co najmniej 3 obiektów (dowolnych, np. filmów albo książek) i wyświetl je w pętli @for, z warunkowym komunikatem @if pokazywanym, gdy tablica jest pusta.
Zadania do pracy własnej
Dodaj do listy produktów licznik pokazujący, ile produktów kosztuje więcej niż 100 zł, wyliczony bezpośrednio w interpolacji (podpowiedź: metoda tablicy
filter, np.{{ produkty.filter(p => p.cena > 100).length }}).Rozbuduj przykład o
@switchnadający każdemu produktowi etykietę cenową ("Tani" dla ceny poniżej 50 zł, "Średni" dla 50-200 zł, "Drogi" powyżej 200 zł), wyświetlaną obok nazwy produktu.Podziel przykład na DWA komponenty:
ProduktyComponent(rodzic, trzyma tablicę danych) i nowyKartaProduktuComponent(potomny, na razie BEZ przekazywania danych z rodzica — po prostu wyświetlający wpisany na sztywno przykładowy produkt) — użyj komponentu potomnego wewnątrz szablonu rodzica przez jego selektor, i sprawdź w przeglądarce, że oba renderują się poprawnie razem.
Typowe błędy
Zapomnienie o track w pętli @for — nowoczesny Angular WYMAGA tej klauzuli (w przeciwieństwie do starszego *ngFor), bez niej kod się nie skompiluje.
Próba wywołania metod modyfikujących dane (np. dodających element do tablicy) BEZPOŚREDNIO w interpolacji {{ }} — interpolacja powinna być używana WYŁĄCZNIE do odczytu/wyświetlania danych; logika zmieniająca stan aplikacji powinna być w metodach klasy, wywoływanych przez zdarzenia (pełne omówienie w kolejnej lekcji).
Mylenie selector komponentu ze zwykłą nazwą klasy TypeScript — selector: "app-produkty" to nazwa ZNACZNIKA HTML używanego do wstawienia komponentu w innym miejscu, zupełnie inna sprawa niż nazwa klasy ProduktyComponent.
Nawiązanie do egzaminu zawodowego
To realizacja INF.04.7.2 i INF.04.7.3 (framework Angular, budowa interfejsu z komponentów i kontrolek) — architektura komponentowa poznana tutaj jest sercem WSZYSTKICH kolejnych projektów w tym dziale. Kolejna lekcja rozszerza komponenty o przekazywanie danych (data binding), zdarzenia i serwisy do komunikacji z backendem.