W Angularze komponenty mogą współdzielić dane lub wywoływać swoje akcje poprzez komunikację, niezależnie od tego, czy są ze sobą bezpośrednio powiązane. Istnieje wiele mechanizmów, za pomocą których komponenty wchodzą w interakcje. W miarę rozwoju projektu, dzielenie aplikacji na osobne komponenty dla poszczególnych funkcji staje się standardem. W tym artykule przyjrzymy się różnym sposobom komunikacji między komponentami w Angularze.
Podstawowy komponent
Podstawowy komponent w Angularze to taki, który jest „bezstanowy” (stateless), nie posiada rozszerzeń, serwisów ani zaawansowanych funkcji.
Oto przykład prostego komponentu:
// profile-photo.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-profile-photo',
template: `<img src="profile-photo.jpg" alt="Your profile photo">`,
styles: `img { border-radius: 50%; }`,
})
export class ProfilePhoto { }
Lub alternatywnie, z wydzielonymi plikami:
// profile-photo.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-profile-photo',
templateUrl: './profile-photo.html',
styleUrl: './profile-photo.css',
})
export class ProfilePhoto { }
// profile-photo.html
<img src="profile-photo.jpg" alt="Your profile photo">
// profile-photo.css
img { border-radius: 50%; }
Źródło przykładu: https://angular.dev/guide/components
Uwaga: Wszystkie przykłady w tym artykule bazują na najnowszej wersji Angulara.
Rozszerzanie programu
Aby użyć komponentu wewnątrz innego komponentu lub skorzystać z pipe’a, dyrektywy czy modułu, należy go zaimportować w tablicy imports wewnątrz dekoratora.
Spójrzmy na przykład, który importuje komponent ProfilePhoto zbudowany w poprzednim przykładzie oraz wbudowany DatePipe, który przekształca daty w postaci ciągu znaków na format daty.
// user-profile.component.ts
import { Component } from '@angular/core';
import { DatePipe } from '@angular/common';
import { ProfilePhoto } from './profile-photo';
@Component({.
selector: 'app-user-profile',
template: `
<main>
<app-profile-photo/>
<p>Joined on: {{ joinedOn() | date }}</p>
</main>
`,
styleUrl: './user-profile.css',
imports: [ProfilePhoto, DatePipe],
/* ... */
})
export class UserProfile {
protected readonly joinedOn = signal<string>('2025-07-15');
}
Gdy zaimportowane komponenty są używane w kodzie HTML, odwołujemy się do nich poprzez selektor zdefiniowany w klasie komponentu
Od wersji Angular 14 komponenty mogą być definiowane jako standalone, a począwszy od wersji 19 są one takie domyślnie. Dzięki temu można je bezpośrednio dodawać do tablicy imports w innych komponentach typu standalone. W starszych wersjach konieczne było ręczne dodawanie zapisu standalone:true wewnątrz dekoratora. Warto jednak pamiętać, że komponenty mogą mieć właściwość standalone ustawioną na false – w takim przypadku ich import musi odbywać się tradycyjnie poprzez NgModule.
Dziedziczenie
Dziedziczenie to sposób na rozbudowę komponentów lub dyrektyw. Gdy komponent rozszerza inny komponent, dziedziczy po nim udekorowane elementy klasy, takie jak właściwości publiczne i chronione (public/protected), wejścia (inputs), wyjścia (outputs), metody cyklu życia itp. Jest to zagadnienie bardziej zaawansowane, ponieważ w wielu projektach tradycyjna interakcja poprzez właściwości wejściowe i zdarzenia wyjściowe okazuje się w zupełności wystarczająca.
Uwaga: W większości przypadków lepiej jest współdzielić logikę za pomocą dyrektyw lub stosować wzorzec kompozycji zamiast dziedziczenia, które może prowadzić do ograniczeń w przyszłości wraz z rozrostem bazy kodu.
Ważne jest, aby zaznaczyć, że dziedziczenie nie służy do komunikacji ani zarządzania stanem.
Przyjrzyjmy się przykładowi komponentu rodzica oraz rozszerzającego go komponentu potomnego:
// parent.component.ts
@Component({ ... })
export abstract class Parent {
readonly isLoading = signal<boolean>(false);
protected startLoading() {
this.isLoading.set(true);
}
protected stopLoading() {
this.isLoading.set(false);
}
}
// child.component.ts
@Component({
...
template: `
@if(isLoading()){
<div> Loading... </div>
}
`
})
export class Child extends Parent implements OnInit {
ngOnInit() {
this.startLoading();
setTimeout(() => {
this.stopLoading();
}, 1000);
}
}
Jak widać w powyższym przykładzie, komponent Child ma dostęp do wszystkich informacji z klasy Parent. W rezultacie Child zachowuje się jak połączenie obu komponentów w jednym, a do jego użycia wystarczy sam selektor komponentu Child (chyba że szablon HTML rodzica musi być jawnie użyty w innym miejscu).
Przepływ danych między komponentami
Komponenty mogą również wchodzić ze sobą w interakcje poprzez przesyłanie danych i zdarzeń. Ten rozdział wyjaśni sposoby przekazywania danych między komponentami.
- Sygnał input() i dekorator @Input
Gdy komponenty przekazują sobie dane, komunikacja typu rodzic-dziecko odbywa się za pomocą sygnału input() lub dekoratora @Input(). Uwaga: Sygnały wejściowe (signal inputs) zostały wprowadzone w Angularze 17, wraz z innymi funkcjonalnościami opartymi na sygnałach (pamiętaj: wielkość liter ma znaczenie!).
Pozwólmy komponentowi aplikacji przekazać dane do komponentu o nazwie CustomSlider:
// custom-slider.component.ts
import { Component, input } from '@angular/core';
@Component({
selector: 'app-custom-slider',
standalone: true,
templateUrl: './custom-slider.component.html'
styles: /*...*/
})
export class CustomSlider {
readonly value = input.required<number>();
}
<!-- custom-slider.component.html -->
<div class="slider-container">
<label>Slider Value: {{ value() }}</label>
<input type="range" min="0" max="100" [value]="value()">
</div>
// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { CustomSlider } from './custom-slider.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [
RouterOutlet,
CustomSlider // import the child component
],
templateUrl: './app.component.html'
styles: /*...*/
})
export class AppComponent {
// Ta właściwość przechowuje wartość, którą chcemy przekazać w dół.
initialSliderValue = 75;
}
<!-- app.component.html -->
<main>
<h1>Parent App Component</h1>
<p>
Przekażemy tę wartość do suwaka:
<strong>{{ initialSliderValue }}</strong>
</p>
<app-custom-slider [value]="initialSliderValue"></app-custom-slider>
</main>
W powyższym przykładzie CustomSlider przyjmuje wartość typu number. Zastosowano tutaj sygnał wejściowy, więc aby uzyskać dostęp do tej wartości w szablonie, należy wywołać ją jak metodę (tj. value()). Alternatywnie można to zrobić za pomocą dekoratora @Input():
@Input({required: true}) value!: number;
<div class="slider-container">
<label>Slider Value: {{ value }}</label>
<input type="range" min="0" max="100" [value]="value">
</div>
Wtedy w szablonie wartość {{ value }} jest dostępna bezpośrednio. Choć obecnie sygnały są preferowane nad dekoratorami ze względów wydajnościowych, wciąż warto znać dekoratory Input/Output.
- output(), dekorator @Output() i EventEmitter
Komunikacja typu dziecko-rodzic odbywa się poprzez wyjścia (outputs) i eventy. Komponenty potomne emitują eventy do swoich rodziców wraz z określoną wartością. Przyjrzyjmy się temu przykładowi:
// vote-button.component.ts
import { Component, output } from '@angular/core';
@Component({
selector: 'app-vote-button',
standalone: true,
templateUrl: './vote-button.component.html'
})
export class VoteButtonComponent {
readonly voted = output<string>();
onClick(){
this.voted.emit('Voted for Angular');
}
}
<!-- vote-button.component.html -->
<button (click)="onClick()"> Vote for Angular! </button>
// app.component.ts
import { Component } from '@angular/core';
import { VoteButtonComponent } from './vote-button.component';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
imports: [VoteButtonComponent],
})
export class AppComponent {
voteStatus: string = 'No one has voted yet.';
handleVote(eventPayload: string) {
this.voteStatus = eventPayload;
}
}
<!-- app.component.html -->
<h1>Parent Component</h1>
<p>{{ voteStatus }}</p>
<app-vote-button (voted)="handleVote($event)"></app-vote-button>
W powyższym przykładzie klikany jest przycisk wewnątrz komponentu dziecka (VoteButton). Poprzez wyjście (output) informacja o zdarzeniu jest wysyłana do rodzica. Output pełni rolę nadajnika, którego rodzic „słucha” poprzez przekazanie $event. W szablonie rodzic wywołuje handleVote przy zdarzeniu dziecka, pobierając wartość tekstową wyemitowaną przez potomka. Zmienna $event przechwytuje tę wartość.
Alternatywnie, używając dekoratora @Output, przykład wyglądałby następująco:
@Output() voted = new EventEmitter<string>();
Warto jednak zauważyć, że @Output to tylko dekorator oznaczający właściwość i sam w sobie nie posiada mechanizmu emitowania zdarzeń – wymaga on utworzenia instancji klasy EventEmitter. W przypadku nowej funkcji output() nie jest to konieczne, ponieważ jest to funkcja fabryka stworzona specjalnie w tym celu.
Wiązanie danych
W Angularze wiązanie danych (binding) tworzy dynamiczne połączenie między logiką komponentu a jego szablonem. Głównym celem bindowania jest automatyczna aktualizacja widoku w odpowiedzi na zmiany w komponencie. Dane można wiązać jednostronnie (one-way) lub dwustronnie (two-way). Wiązanie jednostronne oznacza, że zmiany w danych wpływają tylko na interfejs użytkownika, natomiast wiązanie dwustronne sprawia, że zmiany w UI aktualizują również dane źródłowe i na odwrót.

Powyższy schemat podsumowuje kierunki wiązania danych.
- Interpolacja
Jest to funkcja, która pobiera dynamiczne dane bezpośrednio z logiki komponentu i odzwierciedla je w interfejsie użytkownika:
<label>Slider Value: {{ value }}</label>
- Wiązanie dwustronne (Two-way binding)
Ta funkcja utrzymuje synchronizację między właściwością w klasie komponentu a wartością w jego szablonie.
Składnia:
[(ngModel)]="prop"
Składnia dwustronnego wiązania łączy w sobie wiązanie właściwości (property binding) […] oraz wiązanie zdarzeń (event binding) (…). Zazwyczaj wykorzystuje ona dyrektywę ngModel dostarczaną przez pakiet FormsModule. Powyższy przykład można rozbić na dwa etapy:
// Property binding
[ngModel]="prop"
// Event binding
(ngModelChange)="prop = $event"
Podczas wiązania właściwości, wartość elementu wejściowego (input) jest ustawiana na wartość prop z wnętrza komponentu. Podczas wiązania zdarzeń, gdy wartość elementu wejściowego ulegnie zmianie, emitowane jest zdarzenie ngModelChange. Dzięki $event Angular przechwytuje tę nową wartość i aktualizuje nią właściwość prop w Twoim komponencie.
Wraz z wprowadzeniem sygnałów pojawiła się nowocześniejsza wersja tej metody: sygnał model().
import { Component, model } from '@angular/core';
@Component(...)
export class AppComponent {
prop = model('World');
}
model() tworzy sygnał z możliwością wiązania dwustronnego. W przeciwieństwie do tradycyjnej metody, sygnał model() tworzy właściwość sygnałową przeznaczoną specjalnie do dwustronnego bindowania, której wartość można aktualizować bezpośrednio. W nowszych wersjach Angulara i przy przejściu na detekcję zmian zoneless, korzystanie z narzędzi opartych na sygnałach przynosi więcej korzyści, ponieważ aktualizują one tylko to, co faktycznie uległo zmianie, bez polegania na bibliotece zone.js.
Content projection
W Angularze mechanizm content projection pozwala na tworzenie reużywalnych i elastycznych układów stron w formie komponentów. Oto jak można przekazywać treść do komponentów:
- ng-content
Załóżmy, że w głównym komponencie (rodzicu) implementujesz logikę dla elementu DOM i chcesz mieć do niego bezpośredni dostęp z poziomu rodzica, jednocześnie umieszczając go wizualnie wewnątrz widoku dziecka. Właśnie tutaj z pomocą przychodzi element <ng-content>.
Oto komponent rodzica:
// app.component.ts
@Component({
selector: 'app-root',
template: `
<main>
<app-header>
<input #projectedInput type="text" placeholder="Search...">
</app-header>
<div class="content">
...
</div>
</main>
`,
styles: [`...`],
imports: [HeaderComponent]
})
export class AppComponent {
// some logic
}
A oto komponent dziecka:
// header.component.ts
@Component({
selector: 'app-header',
template: `
<header>
<span>MyApp</span>
<ng-content></ng-content>
</header>
`,
styles: [`...`]
})
export class HeaderComponent {/*...*/}
Blok kodu znajdujący się pomiędzy znacznikami app-header zostanie wyświetlony wewnątrz szablonu dziecka, dokładnie tam, gdzie w HeaderComponent znajduje się znacznik <ng-content>. Jeśli o tym pomyśleć, komponenty z <ng-content> są jak bajgle: pozwalają pokazać coś przez dziurkę w środku! 🙂
Zapytania sygnałowe (Signal Queries)
- viewChild
Oprócz przekazywania danych przez sygnały, istnieje również sposób na przyznanie komponentowi bezpośredniego dostępu do instancji innych komponentów. Pozwala to na wywoływanie ich publicznych metod i korzystanie z publicznych właściwości.
viewChild to zapytanie widoku (view query) oparte na sygnałach. Funkcja ta jest przydatna w sytuacjach, gdy komunikacja przez sygnały wejściowe (input) nie wystarcza, a rodzic musi „wydać polecenie” komponentowi potomnemu. Spójrzmy na przykład:
// alert.component.ts
@Component({
selector: 'app-alert',
template: `
@if (visible) {
<p class="alert">{{ message }}</p>
}
`,
styles: [`.alert { background: #ffc107; padding: 1rem; border-radius: 4px; }`]
})
export class AlertComponent {
visible = false;
message = 'This is a default alert!';
public show(message: string) {
this.message = message;
this.visible = true;
setTimeout(() => this.visible = false, 2000);
}
}
// app.component.ts
@Component({
selector: 'app-root',
template: `
<div class="container">
<div class="section">
<h3>viewChild Example</h3>
<p>This parent directly controls the alert below.</p>
<button (click)="showAlert()">Trigger Direct Child Alert</button>
<app-alert />
</div>
</div>
`,
styleUrl: "./app.component.css",
imports: [AlertComponent],
})
export class AppComponent{
private directAlert = viewChild.required(AlertComponent);
showAlert() {
this.directAlert().show('Alert triggered directly from the Parent!');
}
}
W powyższym przykładzie AlertComponent jest częścią szablonu rodzica. Dzięki viewChild rodzic może wywoływać jego publiczne metody. Za pomocą tej samej techniki można również uzyskać dostęp do elementów DOM. Możesz na przykład manipulować polem input:
// app.component.ts
@Component({
selector: 'app-root',
template: `
<section>
<div class="container">
<h3>@ViewChild Example (DOM Element)</h3>
<p>This parent directly accesses the input element below.</p>
<input #nameInput type="text" placeholder="Your name">
<button (click)="focusInput()">Focus the Input</button>
</div>
</section>
`,
styles: [`...`],
imports: [AlertComponent, CardComponent],
})
export class AppComponent {
private _nameInputElement = viewChild.required<ElementRef<HTMLInputElement>>('nameInput');
focusInput() {
this._nameInputElement().nativeElement.focus();
this._nameInputElement().nativeElement.value = 'Focused!';
}
}
W przypadku elementów DOM należy przekazać nazwę zmiennej referencyjnej (zaczynającej się od #) zdefiniowanej w szablonie.Przed wprowadzeniem zapytań opartych na sygnałach w wersji 17.2, deweloperzy używali dekoratorów. Klasyczny @ViewChild wyglądał tak:
@ViewChild('nameInput')
private _nameInputElement!: ElementRef;
oraz dla zapytania komponentów dzieci:
@ViewChild(AlertComponent)
private _directAlert!: AlertComponent;
Uwaga: Warto dodać, że zapytania oparte na dekoratorach posiadały opcję static
@ViewChild('nameInput', {static: true})
inputEl!: ElementRef<HTMLInputElement>
Pozwalała ona na dostęp do elementu natychmiast, bez czekania na pełną inicjalizację widoku.
W zapytaniach sygnałowych nie ma opcji static, ponieważ nie musisz już sprawdzać, kiedy element będzie gotowy – możesz po prostu zareagować na jego pojawienie się wewnątrz funkcji effect. Możesz dowiedzieć się więcej o effect tutaj.
- contentChild
contentChild to zapytanie oparte na sygnałach, które pozwala komponentowi uzyskać dostęp do komponentu potomnego, elementu lub dyrektywy wstrzykniętej do niego poprzez content projection. Gdy komponent używa <ng-content>, aby wyświetlić treść przekazaną od rodzica, może on odpytać o konkretny element znajdujący się wewnątrz tego wstrzykniętego bloku.
Załóżmy, że AppComponent używa CardComponent w swoim szablonie, a wewnątrz <app-card> umieszcza <app-alert>:
// card.component.ts
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<h4>Card Wrapper</h4>
<p>This card has a slot where content can be placed:</p>
<div class="content-slot">
<!-- Content from a parent will be projected here -->
<ng-content />
</div>
<button (click)="showAlertInside()">Trigger Alert Inside Card</button>
</div>
`,
styles: [`...`]
})
export class CardComponent {
private projectedAlert = contentChild(AlertComponent);
showAlertInside() {
this.projectedAlert()?.show('Alert triggered from INSIDE the Card Wrapper!');
}
}
// app.component.ts
@Component({
selector: 'app-root',
template: `
<div class="section">
<h3>contentChild Example</h3>
<p>This parent places an alert inside the card wrapper.</p>
<!-- The Card wrapper has an <app-alert> projected into it -->
<app-card>
<app-alert />
</app-card>
</div>
`,
styles: [`...`],
imports: [AlertComponent, CardComponent],
})
export class AppComponent {/*...*/}
Treść znajdująca się między znacznikami komponentu dziecka jest wstrzykiwana do jego szablonu za pomocą ng-content. W tym przypadku wstrzykiwany jest AlertComponent, a CardComponent odnajduje dokładnie tę instancję. Należy pamiętać, że wynik zapytania może być undefined, jeśli komponent nie zostanie dostarczony. Gdy zapytanie znajdzie komponent, może uzyskać dostęp do jego publicznych metod – można to porównać do zaglądania w pracę kolegi z zespołu, zamiast zarządzania własnym, wewnętrznym stanem.
W przypadku elementów DOM działa to bardzo podobnie. Jeśli chcesz manipulować elementem z poziomu komponentu dziecka, mimo że jest on zdefiniowany w szablonie rodzica:
// app.component.ts
@Component({
selector: 'app-root',
template: `
<main>
<app-header>
<input #projectedInput type="text" placeholder="Search...">
</app-header>
<div class="content">
...
</div>
</main>
`,
styles: [`...`],
imports: [HeaderComponent]
})
export class AppComponent {/*...*/}
// header.component.ts
@Component({
selector: 'app-header',
template: `
<header>
<span>MyApp</span>
<ng-content />
</header>
`,
styles: [`...`]
})
export class HeaderComponent {
private projectedInputElement = contentChild<ElementRef<HTMLInputElement>>('projectedInput');
/* some logic */
}
contentChild wyszuka element oznaczony identyfikatorem #projectedInput. Ponieważ zapytania oparte na sygnałach zwracają reaktywne obiekty sygnałów, ich początkowa wartość będzie wynosić undefined do momentu zainicjowania treści. Używanie effect() z tymi zapytaniami jest bardzo wydajne, ponieważ kod automatycznie zareaguje, gdy wartość sygnału ulegnie zmianie.
Historycznie, przed wersją Angular 17.2, osiągano to za pomocą dekoratora @ContentChild. Odpowiednik oparty na dekoratorze wygląda następująco:
// for DOM elements
@ContentChild('projectedInput') private _projectedInputElement: ElementRef | undefined;
// for components
@ContentChild(AlertComponent) private _projectedAlert: AlertComponent | undefined;
Podsumowanie
Komponenty stanowią fundament każdego projektu w Angularze. W większych aplikacjach będziesz pracować z wieloma komponentami, które – choć nie zawsze bezpośrednio powiązane – będą od siebie zależne. W tym artykule omówiliśmy różne sposoby ich wzajemnej interakcji. Dowiedziałeś się, jak rozszerzać bazową klasę komponentu, aby dodać nowe funkcje lub współdzielić logikę. Poznałeś również metody przesyłania danych, mechanizm wiązania dwustronnego (two-way binding) oraz technikę content projection za pomocą wbudowanego znacznika ng-content.
Najczęściej spotykanym kierunkiem przepływu danych jest relacja rodzic-dziecko, w której komponenty odbierają dane poprzez wejścia (inputs) i przesyłają powiadomienia o eventach poprzez wyjścia (outputs). Dodatkowo, masz możliwość uzyskania bezpośredniego dostępu do publicznych elementów komponentów potomnych za pomocą zapytań viewChild oraz contentChild.