Przejdź do treści
School IT / Przestrzeń do nauki

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().

Angular 22 input() wiązanie [wejście] komponent rodzic i dziecko @for z komponentem 45 min
CEL LEKCJI

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 computed dziecka 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.

TEORIA

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).

KrokKodGdzie
1. Dziecko deklaruje wejściereadonly 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.

PRZYKŁAD 1

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'.

src/app/nowa-29-1/karta.ts — pełny plik
import { Component, input } from '@angular/core';

@Component({
    selector: 'app-karta',
    imports: [],
    templateUrl: './karta.html'
})
export class Karta {
    readonly tytul = input('');
    readonly sala = input('');
}
src/app/nowa-29-1/karta.html — pełny plik
<div class="card p-3 mb-2" style="width: 20rem">
  <h5>{{ tytul() }}</h5>
  <p class="mb-0">Sala {{ sala() }}</p>
</div>
src/app/nowa-29-1/demo.ts — pełny plik
import { Component } from '@angular/core';
import { Karta } from './karta';

@Component({
    selector: 'app-demo',
    imports: [Karta],
    templateUrl: './demo.html'
})
export class Demo {}
src/app/nowa-29-1/demo.html — pełny plik
<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>
wynik
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?

PRZYKŁAD 2

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ć.

src/app/nowa-29-2/karta.ts — pełny plik
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);
}
src/app/nowa-29-2/karta.html — pełny plik
<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>
src/app/nowa-29-2/demo.ts — pełny plik
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');
}
src/app/nowa-29-2/demo.html — pełny plik
<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>
wynik
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?

PRZYKŁAD 3

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.

src/app/nowa-29-3/karta.ts — pełny plik
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);
}
src/app/nowa-29-3/karta.html — pełny plik
<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>
src/app/nowa-29-3/demo.ts — pełny plik
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 },
    ]);
}
src/app/nowa-29-3/demo.html — pełny plik
<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>
wynik (cztery karty w rzędzie)
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.)

PRZYKŁAD 4

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.

src/app/nowa-29-4/karta.ts — pełny plik
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));
}
src/app/nowa-29-4/karta.html — pełny plik
<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>
src/app/nowa-29-4/demo.ts — pełny plik
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));
    }
}
src/app/nowa-29-4/demo.html — pełny plik
<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>
wynik po czterech kliknięciach
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ść?

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenie
input z @angular/corefunkcja 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)
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
{{ tytul }} w szablonie kartyNa 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 liczbowymTS2322: 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 karcieTS2339: Property 'set’ does not exist on type 'InputSignal<string>’. Dane zmienia rodzic.
ZADANIA

Zadania

ZAD 1Wizytówka★☆☆

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.

ZAD 2Odznaka z punktami★☆☆

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.

ZAD 3Kafelek pogody★★☆

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.

ZAD 4Pasek postępu zbiórki★★☆

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.

ZAD 5Tablica wyników★★★

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.

ZAD 6Plan lekcji z kafelków★★★

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].

PODSUMOWANIE

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ć w computed.
  • 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.
  • @for u 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.

Postęp zapisuje się w tej przeglądarce.