input() — rodzic przekazuje dane
Karta wydarzenia z lekcji 04 zawsze pokazywała ten sam tekst. Prawdziwy komponent działa jak funkcja z parametrami: ten sam szablon, za każdym razem inne dane. Te „parametry” to wejścia — input().
Czego się dziś nauczysz
- Zadeklarujesz w komponencie wejście
input()z wartością domyślną. - Odczytasz wejście w szablonie dziecka jak sygnał:
{{ tytul() }}. - Przekażesz dane z rodzica stałym tekstem (
tytul="…") albo wyrażeniem ([miejsca]="16"). - Zbudujesz listę kart pętlą
@for, w której każda karta dostaje inne dane. - Użyjesz wejścia w
computeddziecka i zobaczysz, że zmiana u rodzica od razu odświeża dziecko.
Przygotowanie: lekcja 04 (własny komponent, imports), lekcja 06 (wiązanie [ ]), lekcje 09–13 (sygnały, computed, @if, @for). Przewidywany czas: 45 min z zadaniami.
Jedna karta, wiele treści
Funkcja powitaj(imie) wita każdego, kogo jej podasz. Funkcja powitajAle() wita tylko Alę. Komponent z lekcji 04 był jak powitajAle() — tekst wpisany na sztywno w szablonie. Żeby był naprawdę użyteczny, musi przyjmować dane z zewnątrz.
Komponent, który w swoim szablonie używa innego komponentu, to rodzic. Komponent użyty w ten sposób to dziecko. W naszych przykładach rodzicem jest Demo, a dzieckiem — Karta (<app-karta /> w demo.html).
| Krok | Kod | Gdzie |
|---|---|---|
| 1. Dziecko deklaruje wejście | readonly tytul = input(''); | karta.ts; w nawiasie wartość domyślna, import input z @angular/core |
| 2. Dziecko odczytuje wejście | {{ tytul() }} | karta.html — z nawiasami, bo wejście jest sygnałem |
| 3a. Rodzic podaje tekst | <app-karta tytul="Turniej retro" /> | demo.html — jak zwykły atrybut HTML |
| 3b. Rodzic podaje wyrażenie | <app-karta [miejsca]="16" /> | demo.html — liczba, wartość z sygnału, pole obiektu |
Dane płyną w dół
Rodzic podaje wartość, dziecko ją pokazuje. Dziecko nie wie, skąd rodzic ją wziął, i nie może jej zmienić — wejście to sygnał tylko do odczytu (nie ma set ani update). Dzięki temu karta jest prosta i działa w każdym miejscu aplikacji. Jak dziecko może coś powiedzieć rodzicowi — w lekcji 31.
Pierwsze wejście: stały tekst
Karta ma dwa wejścia: tytul i sala. Rodzic używa jej trzy razy, za każdym razem z innym tekstem. Pliki karty leżą w tym samym folderze co demo, dlatego import to './karta'.
import { Component, input } from '@angular/core';
@Component({
selector: 'app-karta',
imports: [],
templateUrl: './karta.html'
})
export class Karta {
readonly tytul = input('');
readonly sala = input('');
}
<div class="card p-3 mb-2" style="width: 20rem">
<h5>{{ tytul() }}</h5>
<p class="mb-0">Sala {{ sala() }}</p>
</div>
import { Component } from '@angular/core';
import { Karta } from './karta';
@Component({
selector: 'app-demo',
imports: [Karta],
templateUrl: './demo.html'
})
export class Demo {}
<div class="p-3">
<h4>Nasze wydarzenia</h4>
<app-karta tytul="Turniej retro" sala="12" />
<app-karta tytul="Noc robotów" sala="7" />
<app-karta tytul="Warsztat druku 3D" sala="14" />
</div>
Nasze wydarzenia
┌ Turniej retro ┐
│ Sala 12 │
┌ Noc robotów ┐
│ Sala 7 │
┌ Warsztat druku 3D ┐
│ Sala 14 │
Trzy karty, jeden komponent, trzy różne treści. Klasa Demo jest pusta — wszystkie dane są wpisane w szablonie rodzica jako atrybuty. Nazwa atrybutu to nazwa pola w klasie karty.
Zatrzymaj się
Dopisz czwartą kartę <app-karta /> bez żadnych atrybutów. Co się wyświetli i skąd się to wzięło?
Wartość z wyrażenia: [wejście]
Tekst w atrybucie jest zawsze tekstem. Gdy chcesz przekazać liczbę albo wartość z klasy rodzica, używasz nawiasów kwadratowych — dokładnie jak [src] czy [disabled] z lekcji 06. Karta ma teraz wejście liczbowe miejsca i sama decyduje, jaką etykietę pokazać.
import { Component, computed, input } from '@angular/core';
@Component({
selector: 'app-karta',
imports: [],
templateUrl: './karta.html'
})
export class Karta {
readonly tytul = input('');
readonly miejsca = input(0);
readonly pelne = computed(() => this.miejsca() === 0);
}
<div class="card p-3 mb-2" style="width: 20rem">
<h5>{{ tytul() }}</h5>
@if (pelne()) {
<span class="badge text-bg-danger align-self-start">Brak miejsc</span>
} @else {
<span class="badge text-bg-success align-self-start">Wolnych miejsc: {{ miejsca() }}</span>
}
</div>
import { Component, signal } from '@angular/core';
import { Karta } from './karta';
@Component({
selector: 'app-demo',
imports: [Karta],
templateUrl: './demo.html'
})
export class Demo {
readonly druk = signal('Warsztat druku 3D');
}
<div class="p-3">
<app-karta tytul="Turniej retro" [miejsca]="16" />
<app-karta tytul="Noc robotów" [miejsca]="0" />
<app-karta [tytul]="druk()" [miejsca]="3" />
</div>
Turniej retro [Wolnych miejsc: 16] (zielona)
Noc robotów [Brak miejsc] (czerwona)
Warsztat druku 3D [Wolnych miejsc: 3] (zielona)
Tytuł trzeciej karty pochodzi z sygnału druk w klasie rodzica. Wejście jest sygnałem, więc może być źródłem dla computed w dziecku — tak jak pelne.
Liczba bez nawiasów
Zapis miejsca="16" (bez [ ]) nie skompiluje się: TS2322: Type 'string’ is not assignable to type 'number’. Tekst w atrybucie to zawsze tekst, a wejście oczekuje liczby — ten sam problem co z value i [value] w lekcji 21.
Zatrzymaj się
Typu wejścia miejsca nigdzie nie napisaliśmy, a kompilator i tak wie, że to liczba. Skąd?
Lista kart z tablicy
Dane leżą w tablicy u rodzica, a pętla @for tworzy kartę dla każdego elementu i przekazuje jej pola. Karta nie wie nic o tablicy — dostaje tylko swoje dane.
import { Component, input } from '@angular/core';
@Component({
selector: 'app-karta',
imports: [],
templateUrl: './karta.html'
})
export class Karta {
readonly tytul = input('');
readonly sala = input('');
readonly miejsca = input(0);
}
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{{ tytul() }}</h5>
<p class="card-text">Sala {{ sala() }}</p>
<span class="badge" [class.text-bg-success]="miejsca() > 0" [class.text-bg-danger]="miejsca() === 0">
{{ miejsca() > 0 ? 'wolne: ' + miejsca() : 'brak miejsc' }}
</span>
</div>
</div>
import { Component, signal } from '@angular/core';
import { Karta } from './karta';
@Component({
selector: 'app-demo',
imports: [Karta],
templateUrl: './demo.html'
})
export class Demo {
readonly wydarzenia = signal([
{ id: 1, tytul: 'Turniej retro', sala: '12', miejsca: 16 },
{ id: 2, tytul: 'Noc robotów', sala: '7', miejsca: 0 },
{ id: 3, tytul: 'Warsztat druku 3D', sala: '14', miejsca: 3 },
{ id: 4, tytul: 'Hackathon', sala: 'Aula', miejsca: 40 },
]);
}
<div class="p-3">
<div class="row g-3" style="max-width: 60rem">
@for (w of wydarzenia(); track w.id) {
<div class="col-md-3">
<app-karta [tytul]="w.tytul" [sala]="w.sala" [miejsca]="w.miejsca" />
</div>
}
</div>
</div>
Turniej retro Noc robotów Warsztat druku 3D Hackathon
Sala 12 Sala 7 Sala 14 Sala Aula
[wolne: 16] [brak miejsc] [wolne: 3] [wolne: 40]
Dopisz piąte wydarzenie w tablicy — pojawi się piąta karta bez żadnej zmiany w szablonie. Wygląd karty zmieniasz w jednym miejscu (karta.html), a zmienia się na wszystkich kartach naraz. Kolor etykiety ustawiają [class.text-bg-success] i [class.text-bg-danger] z lekcji 15.
Zatrzymaj się
Karta dostaje trzy osobne wejścia. Gdyby obiekt miał dziesięć pól, wywołanie karty byłoby bardzo długie. Jak można by to uprościć? (Odpowiedź w lekcji 30.)
Rodzic zmienia dane — karta się odświeża
Rodzic ma przycisk zapisu, który zmniejsza liczbę wolnych miejsc w swoim sygnale. Karta dostaje tę liczbę wejściem i sama przelicza procent zapełnienia w computed.
import { Component, computed, input } from '@angular/core';
@Component({
selector: 'app-karta',
imports: [],
templateUrl: './karta.html'
})
export class Karta {
readonly tytul = input('');
readonly miejsca = input(0);
readonly limit = input(10);
readonly procent = computed(() => Math.round((this.limit() - this.miejsca()) / this.limit() * 100));
}
<div class="card p-3" style="width: 22rem">
<h5>{{ tytul() }}</h5>
<p class="mb-1">Wolnych miejsc: {{ miejsca() }} z {{ limit() }}</p>
<div class="progress">
<div class="progress-bar" [class.bg-danger]="miejsca() === 0" [style.width.%]="procent()"></div>
</div>
</div>
import { Component, signal } from '@angular/core';
import { Karta } from './karta';
@Component({
selector: 'app-demo',
imports: [Karta],
templateUrl: './demo.html'
})
export class Demo {
readonly wolne = signal(4);
zapisz(): void {
this.wolne.update(n => (n > 0 ? n - 1 : 0));
}
}
<div class="p-3">
<app-karta tytul="Warsztat druku 3D" [miejsca]="wolne()" [limit]="8" />
<button class="btn btn-primary mt-3" (click)="zapisz()">Zapisz osobę (u rodzica)</button>
</div>
Warsztat druku 3D
Wolnych miejsc: 0 z 8
[████████████████████] (pasek czerwony, 100%)
Każde kliknięcie u rodzica zmienia sygnał wolne, a karta od razu pokazuje nową liczbę i dłuższy pasek. Kod karty nie ma pojęcia, że gdzieś jest przycisk — reaguje na zmianę wejścia. [style.width.%] to wiązanie stylu z jednostką z lekcji 15.
Zatrzymaj się
Dopisz w karcie metodę, która robi this.miejsca.set(5). Co zgłosi kompilator i dlaczego to dobra wiadomość?
Zestawienie elementów
| Element | Znaczenie |
|---|---|
input z @angular/core | funkcja tworząca wejście komponentu |
readonly x = input('') | wejście tekstowe z wartością domyślną ''; typ wynika z wartości |
readonly x = input(0) | wejście liczbowe z wartością domyślną 0 |
{{ x() }}, this.x() | odczyt wejścia w szablonie i w klasie dziecka |
computed(() => … this.x() …) | wartość wyliczana z wejścia — przelicza się po każdej zmianie u rodzica |
<app-karta x="tekst" /> | rodzic podaje stały tekst |
<app-karta [x]="wyrazenie" /> | rodzic podaje wynik wyrażenia: liczbę, sygnał s(), pole w.tytul |
[x]="'tekst'" | tekst w nawiasach kwadratowych — w apostrofach (to wyrażenie) |
Zanim utkniesz
| Zapis | Problem |
|---|---|
{{ tytul }} w szablonie karty | Na stronie napis [Input Signal: Turniej retro], w terminalu ostrzeżenie NG8109 tytul is a function and should be invoked: tytul(). Wejście to sygnał — odczyt z nawiasami. |
miejsca="16" przy wejściu liczbowym | TS2322: Type 'string’ is not assignable to type 'number’. Liczby przekazuj przez [miejsca]="16". |
[tytul]="Turniej retro" | NG5002: Parser Error: Unexpected token 'retro’. W [ ] jest wyrażenie, więc tekst trzeba ująć w apostrofy: [tytul]="'Turniej retro'" — albo pisać bez nawiasów. |
Literówka bez nawiasów: tytl="…" | Brak błędu — to zwykły atrybut HTML, karta go nie zna i pokazuje wartość domyślną. |
Literówka z nawiasami: [tytl]="…" | NG8002: Can’t bind to 'tytl’ since it isn’t a known property of 'app-karta’. |
this.tytul.set('…') w karcie | TS2339: Property 'set’ does not exist on type 'InputSignal<string>’. Dane zmienia rodzic. |
Zadania
Komponent app-wizytowka z wejściami imie i klasa. Użyj go trzy razy dla trzech osób.
Kryterium sukcesu: trzy wizytówki z różnymi danymi; w plikach wizytówki nie ma żadnego imienia wpisanego na sztywno.
Podpowiedź
Przykład 1, tylko z innymi nazwami. Nie zapomnij dodać Wizytowka do imports rodzica.
Komponent app-odznaka z wejściami nazwa (tekst) i punkty (liczba, domyślnie 0). Odznaka pokazuje nazwę i punkty w formacie „Pixel — 120 pkt”. Rodzic używa jej trzy razy, w tym raz bez punktów.
Kryterium sukcesu: punkty przekazane jako liczba ([punkty]); odznaka bez punktów pokazuje „0 pkt”.
Podpowiedź
readonly punkty = input(0); — i pamiętaj o nawiasach kwadratowych u rodzica.
Komponent app-pogoda z wejściami miasto (tekst) i temperatura (liczba). Kafelek jest niebieski poniżej 0°C, zielony od 0 do 20°C i pomarańczowy (żółty) powyżej. Rodzic wyświetla pętlą tablicę pięciu miast.
Kryterium sukcesu: kolory kafelków zgadzają się z temperaturami; dopisanie miasta do tablicy dodaje kafelek.
Podpowiedź
W karcie computed zwracający nazwę klasy, np. 'text-bg-primary', i [class]="kolor()" na kafelku.
Komponent app-zbiorka z wejściami cel (np. 500) i zebrano. Pokazuje „Zebrano 120 z 500 zł” i pasek progress. Rodzic ma przyciski „+10 zł” i „+50 zł”, które zmieniają jego sygnał.
Kryterium sukcesu: przyciski u rodzica wydłużają pasek dziecka; procent liczy dziecko w computed; po przekroczeniu celu pasek ma 100% i jest zielony.
Podpowiedź
Math.min(100, …) nie pozwoli wyjść poza 100%. Wzór na podstawie przykładu 4.
Komponent app-wiersz-wyniku z wejściami miejsce, nick, punkty i maks (najlepszy wynik). Wiersz pokazuje pasek o długości proporcjonalnej do punkty / maks. Rodzic ma tablicę graczy, wyświetla ją posortowaną malejąco i przekazuje każdemu wierszowi wynik lidera jako maks.
Kryterium sukcesu: lider ma pełny pasek, pozostali — krótsze w odpowiedniej proporcji; numer miejsca pochodzi z $index.
Podpowiedź
U rodzica dwa computed: posortowana kopia [...gracze()].sort((a, b) => b.punkty - a.punkty) i maksimum = punkty pierwszego elementu tej kopii.
Komponent app-lekcja z wejściami nr, przedmiot, sala i teraz (liczba — numer trwającej lekcji). Kafelek trwającej lekcji jest wyróżniony, lekcje, które już się odbyły, są wyszarzone. Rodzic ma tablicę 7 lekcji, sygnał teraz i przyciski „poprzednia” / „następna”.
Kryterium sukcesu: przyciski u rodzica przesuwają wyróżnienie po kafelkach; kafelek sam decyduje o swoim wyglądzie na podstawie nr() i teraz().
Podpowiedź
W kafelku dwa computed: trwa (nr() === teraz()) i minela (nr() < teraz()), a w szablonie [class.border-primary] i [class.opacity-50].
Co trzeba zapamiętać
readonly x = input(wartoscDomyslna)deklaruje wejście komponentu; import z@angular/core.- Wejście jest sygnałem tylko do odczytu: w dziecku czytasz
x()i możesz go użyć wcomputed. - Rodzic podaje stały tekst atrybutem
x="…", a każdą inną wartość wiązaniem[x]="…". - Dane płyną w dół: zmiana u rodzica od razu zmienia dziecko; dziecko nie może zmienić wejścia.
@foru rodzica + wejścia w dziecku = lista kart z jednego szablonu.
Sprawdź się: czym różni się <app-karta miejsca="5" /> od <app-karta [miejsca]="5" /> i który zapis zadziała przy wejściu liczbowym?
Dokumentacja: angular.dev — Accepting data with input properties.