Komponenty Angular od podstaw: selektory, szablony, dane wejściowe, dane wyjściowe
Praktyczny przewodnik po nowoczesnych komponentach Angular działających samodzielnie: struktura plików, dekoratory, selektory, szablony, stylizacje skojarzone z konkretnym kontekstem oraz przepływ danych między komponentami rodzicowymi a potomnymi za pomocą funkcji input() i output().
Gdy projekt Angular zostanie stworzony i uruchomiony, następnym pytaniem jest to, jak budowana jest sama strona. Odpowiedzią są komponenty: małe, samodzielne jednostki, z których każda odpowiada za część interfejsu. Ten przewodnik wyjaśnia, z czego składa się komponent, w jaki sposób komponenty samodzielne zmieniają sytuację w porównaniu z starszym kodem opartym na NgModule, oraz jak komponent rodzicowski i potomny wymieniają się danymi i zdarzeniami za pomocą obecnych interfejsów input() i output(). Pod koniec powinieneś być w stanie utworzyć komponent, połączyć go z stroną i przekazywać informacje w obu kierunkach.
Jeśli nadal potrzebujesz projektu do eksperymentowania, przewodnik dotyczący tworzenia nowoczesnego projektu Angular 22 omawia konfigurację i strukturę folderów, które zakłada ten artykuł.
Dlaczego Angular dzieli interfejs na komponenty
Załóżmy zwykłą stronę internetową: pasek nawigacyjny u góry, panel boczny, treść produktu pośrodku oraz stopka poniżej. Można by umieścić całą tę strukturę, stylizację i zachowanie w jednym ogromnym pliku, ale szybko stałby się on trudny do odczytania i jeszcze trudniejszy do modyfikacji. Angular zamiast tego zachęca do podziału strony na części, z których każda ma jeden konkretny zadanie.
Angular Application
│
├── Navbar Component
├── Sidebar Component
├── Product Component
├── Login Component
└── Footer Component
Komponent zazwyczaj zawiera cztery elementy: klasę TypeScript, która przechowuje stan i zachowanie, szablon HTML opisujący strukturę treści, style określające wygląd oraz metadane informujące Angular o sposobie jego obsługi. Ponieważ każdy element ma jasno określoną rolę, można analizować menu nawigacyjne bez myślenia o stopce, ponownie wykorzystywać tę samą kartę produktu w różnych miejscach oraz testować poszczególne części interfejsu użytkownika oddzielnie. Formularze logowania, panele profilowe czy elementy dashboardu to doskonałe przykłady takich komponentów.
Komponenty samodzielne są standardem
Wiele tutoriali dotyczących Angulara, szczególnie tych starszych, organizuje komponenty wewnątrz NgModule, który deklaruje komponenty i wymienia, od czego one zależą. W nowszych wersjach Angulara porzucono ten model. Komponenty są teraz domyślnie samodzielne, więc mogą funkcjonować bez rejestracji w module. Minimalny komponent wygląda tak:
import { Component } from '@angular/core';
@Component({
selector: 'app-welcome',
templateUrl: './welcome.html',
styleUrl: './welcome.css'
})
export class WelcomeComponent {
}
Można by oczekiwać obecności flagi wskazującej na to, że komponent jest samodzielny, podobnie jak w poniższej linii, ale nie jest to już wymagane:
standalone: true
Pominięcie tego elementu ma ten sam efekt, ponieważ domyślnym zachowaniem jest tryb samodzielny. Gdy komponent samodzielny potrzebuje innego komponentu, dyrektywy lub „pipe’a”, wymienia tę zależność w tablicy imports swojej własnej konfiguracji @Component. Zaletą jest lokalizacja: można otworzyć jeden plik i zobaczyć dokładnie, od czego zależy dany komponent, zamiast przeszukiwać definicje modułów w innych częściach projektu.
Jak komponent jest zapisywany na dysku
Typowy komponent znajduje się w swojej własnej folderze z trzema plikami obok siebie:
welcome/
│
├── welcome.ts
├── welcome.html
└── welcome.css
Plik .ts zawiera logikę, plik .html zawiera markup, a plik .css zawiera style. Oto, co może zawierać każdy z nich w przypadku prostego komponentu powitalnego. Najpierw klasa, która udostępnia właściwość title szablonowi:
import { Component } from '@angular/core';
@Component({
selector: 'app-welcome',
templateUrl: './welcome.html',
styleUrl: './welcome.css'
})
export class WelcomeComponent {
title = 'Welcome to Angular 22';
}
Śablon odczytuje tę właściwość za pomocą podwójnych nawiasów klamrowych:
<h1>{{ title }}</h1>
<p>
This is my first Angular component.
</p>
A plik stylów dotyczy wyłącznie elementów w tym szablonie:
h1 {
color: #dd0031;
}
p {
font-size: 18px;
}
Dekorator @Component() pełni rolę łącznika. Wskazuje Angularowi na szablon i plik stylów, dzięki czemu te trzy pliki funkcjonują jako jedna całość, jak pokazano poniżej:
welcome.ts
│
├── Logic
│
├── welcome.html
│ ↓
│ UI
│
└── welcome.css
↓
Style
Przewodnik stylów Angulara zaleca przechowywanie ściśle powiązanych plików w tym samym katalogu, co dokładnie robi ta struktura. Gdy komponent się rozrasta, można precyzyjnie określić, gdzie znajdują się jego struktura, style i logika.
To, co dekorator @Component() mówi Angularowi
Dekorator dołącza metadane do klasy. To właśnie dzięki tym metadanom Angular wie, do czego służy klasa i jak ją renderować:
@Component({
selector: 'app-welcome',
templateUrl: './welcome.html',
styleUrl: './welcome.css'
})
W tym przypadku większość pracy wykonują trzy właściwości:
selectorokreśla nazwę tagu, którą używa się do umieszczenia komponentu w innych szablonach.templateUrlwskazuje na plik HTML zawierający markup komponentu.styleUrlwskazuje na plik z stylem komponentu.
Istnieją dodatkowe opcje, takie jak imports służące do włączania innych komponentów lub funkcji Angular, oraz alternatywy typu inline dla templateUrl i styleUrl, o których będzie mowa później w tym artykule. Nie ma potrzeby uczenia się ich wszystkich od razu; staną się znane w miarę tworzenia kolejnych komponentów.
Selektory przekształcają klasę w element niestandardowy
Selektor to identyfikator, który Angular szuka w szablonach. W tej konfiguracji jest ustawiony na app-welcome:
@Component({
selector: 'app-welcome',
...
})
Zatem sama wartość selektora jest po prostu:
app-welcome
W dowolnym miejscu, gdzie chcesz, aby komponent się pojawił, wpisujesz jego nazwę tak, jakby była tagiem HTML:
<app-welcome></app-welcome>
Gdy Angular napotyka <app-welcome> podczas renderowania szablonu, tworzy instancję odpowiadającego komponentu i renderuje jego szablon wewnątrz tego elementu. Element, który pasuje do selektora, nazywany jest elementem hostowym komponentu i pozostaje w DOM-ie, co ma znaczenie później, gdy chcesz dostosować styl lub dodać zachowanie całemu komponentowi.
Angular CLI generuje selektory z prefiksem skonfigurowanym dla Twojej aplikacji, zazwyczaj app-, chociaż przy tworzeniu komponentu możesz wybrać inny selektor. Prefiks ma znaczenie nie tylko estetyczne: nazwy elementów niestandardowych powinny zawierać myślnik, a prefiks specyficzny dla projektu zapobiega kolizjom Twoich komponentów z elementami natywnymi lub tagami z bibliotek third-party.
Łączniki łączą markup z danymi komponentu
Łącznik określa to, co użytkownik faktycznie widzi. W najprostszej formie jest to zwykły HTML:
<h1>Welcome!</h1>
<p>We are learning Angular components.</p>
To, co sprawia, że łączniki Angular są bardziej funkcjonalne niż statyczny HTML, to możliwość odczytywania danych i wywoływania działań zdefiniowanych w klasie komponentu. Załóżmy, że klasa deklaruje właściwość:
name = 'Mubbassir';
Następnie łącznik może ją wyświetlić za pomocą interpolacji:
<h1>Hello {{ name }}!</h1>
Angular utrzymuje renderowany wynik w synchronizacji z danymi: gdy zmienia się wartość powiązana, strona aktualizuje się bez konieczności ręcznego edytowania DOM-u. To połączenie między klasą TypeScript a jej szablonem stanowi podstawę dla wszystkich innych funkcji w Angularze, od obsługi zdarzeń po formularze.
Stylizacje komponentów są domyślnie ograniczone
Każdy komponent może mieć własne stylizacje:
.card {
padding: 20px;
border-radius: 10px;
}
h2 {
color: #dd0031;
}
Domyślnie Angular stosuje emulowaną inkapsulację widoku do tych reguł. Podczas budowania aplikacji przepisuje selektory i dodaje generowane atrybuty do elementów komponentu, dzięki czemu reguła taka jak h2 { ... } pasuje tylko do elementów h2 w szablonie tego komponentu, a nie do wszystkich nagłówków w aplikacji. W aplikacji z setkami komponentów ta ochrona zapobiega temu, by mała zmiana stylu w jednym miejscu potajemnie zepsuła wygląd innego komponentu.
Encapsulacja nie wyklucza wspólnego stylizowania. Zasady, które powinny obowiązywać wszędzie, takie jak typografia, funkcje służące do przywracania ustawień lub narzędzia do układu, należą do globalnej tabeli stylów aplikacji, natomiast pliki komponentów zawierają style specyficzne dla danego komponentu.
Tworzenie komponentu za pomocą Angular CLI
Ręczne tworzenie plików też jest możliwe, ale CLI robi to szybciej i z spójną nomenklaturą. Z korzenia projektu uruchom:
ng generate component user-card
lub w skróconej formie:
ng g c user-card
Generator tworzy folder w takim wyglądzie (błąd „=” po nazwie pliku specyfikacji to pomyłka w przedstawieniu, a nie część nazwy):
user-card/
├── user-card.ts
├── user-card.html
├── user-card.css
└── user-card.spec.ts=
Nowszy styl nazewania plików jest krótszy od starszej konwencji user-card.component.ts. To, czy zostanie utworzony plik testowy .spec.ts, zależy od konfiguracji testowania projektu. Najnowsze wersje CLI, które przestrzegają zaktualizowanego przewodnika stylu, również mają tendencję do generowania nazw klas bez sufiksu Component (na przykład UserCard); poniższe przykłady zachowują ten sufiks, a oba sposoby nazewania działają poprawnie, o ile importy są zgodne.
Jak komponenty rodzicielskie i potomne ze sobą komunikują się
Prawdziwe ekrany to drzewa składające się z komponentów, które muszą wymieniać między sobą informacje. Rozważmy komponent rodzicielski, który zna dane użytkownika, oraz komponent potomny, którego zadaniem jest wyświetlenie tego użytkownika. Komponent rodzicielski potrzebuje sposobu na przekazanie danych w dół, a komponent potomny – sposobu na zgłoszenie czegoś, gdy coś się wydarzy, np. po kliknięciu przycisku.
Angular obsługuje kierunek w dół za pomocą inputów, a kierunek w górę za pomocą outputów:
Parent Component
│
│ Data
│
▼
Child Component
│
│ Event
│
▼
Parent Component
Dla nowego kodu Angular zaleca funkcję input() opartą na sygnałach oraz funkcję output(). API oparte na dekoratorach @Input() i @Output() są nadal w pełni wspierane, więc nadal można je znaleźć w istniejących bazach kodu.
Przekazywanie danych w dół za pomocą input()
Zacznij od klasy komponentu potomnego:
import { Component, input } from '@angular/core';
@Component({
selector: 'app-user-card',
templateUrl: './user-card.html',
styleUrl: './user-card.css'
})
export class UserCardComponent {
name = input.required<string>();
}
Ważna jest deklaracja inputu:
name = input.required<string>();
Stwierdza to, że UserCardComponent oczekuje łańcucha tekstu name od osoby, która go używa. Użycie input.required() sprawia, że to wymaganie staje się ścisłe: jeśli element nadrzędny zapomni powiązać name, Angular zgłasza błąd zamiast w tajemnicy wyświetlić wartość pustą. W przypadku pól opcjonalnych zwykłe input() przyjmuje wartość domyślną.
Ponieważ name jest sygnałem, a nie zwykłą właściwością, odczytuje się go poprzez jego wywołanie. W klasie pisze się:
this.name()
a w szablonie:
{{ name() }}
Zapomnienie nawiasów to częsty wczesny błąd; bez nich szablon wyświetla samą funkcję sygnału zamiast jej wartości. Szablon elementu potomnego używa tego pola w następujący sposób:
<div class="card">
<h2>{{ name() }}</h2>
<p>Welcome to Angular!</p>
</div>
Po stronie elementu nadrzędnego wartość przypina się do pola podczas umieszczania elementu potomnego:
<app-user-card [name]="userName" />
Jeśli klasa nadrzędna zawiera:
userName = 'Yuvaraj';
dziecko otrzymuje tę ciąg znaków i ją wyświetla:
Yuvaraj
To nawiasy kwadratowe sprawiają, że mamy do czynienia z powiązaniem właściwości:
[name]="userName"
Indukują one Angular, aby ocenił userName jako wyrażenie w klasie nadrzędnej i przekazał wynik do pola name w dziecku. Bez nawiasów Angular przekazałby zamiast tego dosłowny tekst userName. Ponieważ pole to jest sygnałem, dziecko automatycznie się odświeża za każdym razem, gdy zmienia się wartość w klasie nadrzędnej.
Wysyłanie zdarzeń w górę za pomocą output()
Teraz dajmy dziecku przycisk i niech klasa nadrzędna dowie się, gdy zostanie wybrany użytkownik. Dziecko deklaruje output oraz metodę, która przez niego emituje zdarzenia:
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-user-card',
templateUrl: './user-card.html',
styleUrl: './user-card.css'
})
export class UserCardComponent {
name = input.required<string>();
selected = output<string>();
selectUser() {
this.selected.emit(this.name());
}
}
Deklaracja output definiuje własne zdarzenie, które przenosi ciąg znaków:
selected = output<string>();
Wywołanie emit uruchamia to zdarzenie i przekazuje aktualną nazwę jako jego treść:
this.selected.emit(this.name());
Środowisko dziecka wywołuje selectUser(), gdy zostanie kliknięty przycisk. Zwróć uwagę na składnię (click), która jest mechanizmem wiązania zdarzeń w Angular:
<div class="card">
<h2>{{ name() }}</h2>
<button (click)="selectUser()">
Select User
</button>
</div>
Rodzic przysłuchuje się temu niestandardowemu zdarzeniu, używając tej samej składni nawiasów, tym razem z nazwą wyniku:
<app-user-card
[name]="userName"
(selected)="userSelected($event)"
/>
Nawiasy okrągłe oznaczają „przysłuchiwać się temu zdarzeniu”, a $event przechowuje wartość, którą wysłało dziecko – w tym przypadku nazwę użytkownika. Rodzic obsługuje ją w zwykłej metodzie:
userSelected(name: string) {
console.log('Selected user:', name);
}
Taka podział pracy sprawia, że dziecko może być wielokrotnie wykorzystywane. Nie wie ani nie obchodzi go, co robi rodzic z wybranym elementem; jedynie informuje o tym, że coś się wydarzyło. Rodzic decyduje, czy to zapisze, przejdzie do innej strony lub zaktualizuje swój własny stan.
Umieszczenie komponentu na stronie
Ostatni przykład łączy wszystkie elementy. Ta wersja komponentu powitalnego używa inline template oraz styles w dekoratorze zamiast oddzielnych plików, co jest przydatne dla bardzo małych komponentów:
import { Component } from '@angular/core';
@Component({
selector: 'app-welcome',
template: `
<div class="welcome-card">
<h2>Welcome Component</h2>
<p>This component was created using the <strong>app-welcome</strong> selector.</p>
</div>
`,
styles: `
.welcome-card {
padding: 25px;
margin-top: 20px;
border-radius: 12px;
background: #f5f5f5;
text-align: center;
font-family: Arial, sans-serif;
}
h2 {
color: #dd0031;
}
p {
font-size: 18px;
}
`
})
export class WelcomeComponent {}
Komponent korzeniowy importuje WelcomeComponent do swojego arraya imports, co umożliwia użycie selektora app-welcome w jego szablonie. Pominięcie tego kroku jest jedną z najczęstszych przyczyn błędu „is not a known element” w aplikacjach samodzielnych:
import { Component } from '@angular/core';
import { WelcomeComponent } from './welcome';
@Component({
selector: 'app-root',
imports: [WelcomeComponent],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {}
Na koniec szablon korzeniowy umieszcza komponent za pomocą jego selektora:
<div class="container">
<h1>Angular Components</h1>
<p>
Below is our custom Angular component:
</p>
<app-welcome></app-welcome>
</div>
Templaty wstawiane bezpośrednio przechowują wszystko w jednym pliku, ale stają się niewygodne, gdy kod rośnie powyżej kilku wierszy. Rozsądną zasadą jest używanie ich do małych elementów prezentacyjnych, a przechodzenie na oddzielne pliki .html i .css, gdy tylko szablon wymaga rzeczywistej struktury.
Główne wnioski
- Komponent składa się z klasy typu TypeScript, szablonu oraz opcjonalnych stylów, połączonych za pomocą dekoratora
@Component(). - W nowoczesnym Angularze domyślnym podejściem jest model standalone; zaleca się definiowanie zależności w tablicy
importskażdego komponentu, a nie wNgModule. - Selektor to specjalny tag, który służy do umieszczenia komponentu, a odpowiadający mu element staje się jego hostem.
- Szablony łączą się z danymi klas, a Angular aktualizuje interfejs użytkownika, gdy te dane ulegają zmianie.
input() (lub input.required()), a aby wysłać wydarzenia w górę – output(); dane wejściowe odczytuje się poprzez ich wywoływanie jako sygnałów.W ten sposób aplikacja Angular jest drzewem komponentów, z których każdy odpowiada za swoją część interfejsu i komunikuje się za pomocą wyraźnych danych wejściowych i wyjściowych. To właśnie ta struktura sprawia, że duże aplikacje Angular pozostają zrozumiałe w miarę ich rozwoju.
Literatura pokrewna
- Jak Angular Dependency Injection rozwiązuje problemy z usługami za pomocą inject() — Zrozum, w jaki sposób Angular DI dostarcza usługi za pośrednictwem funkcji inject(), providerów oraz hierarchicznych injectorów, a także jak określać zakres instancji i unikać błędów związanych z brakującymi providerami.
- Od pustej foldery do działającego aplikacji: Ustawianie nowoczesnego projektu Angular 22 — Zainstaluj Node.js i Angular CLI, utwórz szkielet aplikacji Angular 22 za pomocą polecenia ng new, zrozum strukturę plików w modelu standalone oraz uruchom aplikację lokalnie z możliwością natychmiastowej ponownej kompilacji.