input.required, obiekt jako wejście i booleanAttribute
Wejście z wartością domyślną jest wygodne, ale zdradliwe: gdy rodzic zapomni podać tytuł, karta po cichu pokaże pusty nagłówek. Dziś nauczysz się oznaczać wejścia obowiązkowe, przekazywać cały obiekt jednym wejściem i pisać wejścia logiczne jak zwykłe atrybuty HTML.
Czego się dziś nauczysz
- Wybierzesz między wejściem z wartością domyślną a wejściem obowiązkowym
input.required. - Odczytasz komunikat kompilatora o brakującym wejściu obowiązkowym.
- Opiszesz kształt danych interfejsem w osobnym pliku i zaimportujesz go w dwóch komponentach.
- Przekażesz do dziecka cały obiekt jednym wejściem.
- Użyjesz
booleanAttribute, żeby wejście logiczne działało jak<input disabled>.
Przygotowanie: lekcja 29 — input() i przekazywanie danych z rodzica. Przewidywany czas: 45 min z zadaniami.
Domyślne czy obowiązkowe
Niektóre dane mają sensowną wartość domyślną: limit miejsc 10, kolor szary, poziom 1. Innych — tytułu, identyfikatora, całego obiektu wydarzenia — nie da się „wymyślić”. Takie wejścia oznaczasz jako obowiązkowe: rodzic, który ich nie poda, nie skompiluje się.
| Zapis w dziecku | Znaczenie |
|---|---|
readonly limit = input(10); | rodzic może podać inną wartość, ale nie musi |
readonly tytul = input.required<string>(); | rodzic musi podać wartość; nie ma wartości domyślnej, więc typ piszesz w nawiasach ostrych |
readonly wydarzenie = input.required<Wydarzenie>(); | cały obiekt jednym wejściem; typ opisuje interfejs |
readonly wyroznione = input(false, { transform: booleanAttribute }); | wystarczy napisać samo słowo wyroznione w znaczniku |
Interfejs — opis kształtu danych
Interfejs to lista pól z typami — bez kodu, tylko opis. Trzymasz go w osobnym pliku, np. wydarzenie.ts, z export przed słowem interface. Importują go i rodzic (tablica signal<Wydarzenie[]>), i dziecko (input.required<Wydarzenie>) — wtedy obie strony mówią o tym samym kształcie danych.
Kiedy wartość domyślna ma sens
Karta ma tytuł i kolor nagłówka. Kolor ma sensowną wartość domyślną (secondary), więc rodzic podaje go tylko wtedy, gdy chce innego. Tytuł też ma tu wartość domyślną — zobacz, co z tego wynika.
import { Component, input } from '@angular/core';
@Component({
selector: 'app-karta',
imports: [],
templateUrl: './karta.html'
})
export class Karta {
readonly tytul = input('');
readonly kolor = input('secondary');
}
<div class="card mb-2" style="width: 20rem">
<div class="card-header" [class]="'text-bg-' + kolor()">Wydarzenie</div>
<div class="card-body"><h5 class="mb-0">{{ tytul() }}</h5></div>
</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">
<app-karta tytul="Turniej retro" />
<app-karta tytul="Noc robotów" kolor="success" />
<app-karta kolor="danger" />
</div>
[Wydarzenie] (szary) Turniej retro
[Wydarzenie] (zielony) Noc robotów
[Wydarzenie] (czerwony) (pusty tytuł)
[class]="'text-bg-' + kolor()" skleja nazwę klasy z dwóch części, np. text-bg-success. Klasa card-header zapisana zwykłym atrybutem zostaje — Angular tylko dokłada wyliczoną. Trzecia karta ma pusty tytuł: rodzic go zapomniał, a kompilator milczy, bo przecież jest wartość domyślna ''.
Zatrzymaj się
Dla którego z dwóch wejść wartość domyślna jest dobrym pomysłem, a dla którego — pułapką?
Wejście obowiązkowe: input.required
Ta sama karta, ale tytuł jest obowiązkowy. Trzecia karta w szablonie rodzica jest zakomentowana — odkomentuj ją i zobacz, co powie kompilator.
import { Component, input } from '@angular/core';
@Component({
selector: 'app-karta',
imports: [],
templateUrl: './karta.html'
})
export class Karta {
readonly tytul = input.required<string>();
readonly kolor = input('secondary');
}
<div class="card mb-2" style="width: 20rem">
<div class="card-header" [class]="'text-bg-' + kolor()">Wydarzenie <small>(tytuł obowiązkowy)</small></div>
<div class="card-body"><h5 class="mb-0">{{ tytul() }}</h5></div>
</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">
<app-karta tytul="Turniej retro" />
<app-karta tytul="Noc robotów" kolor="success" />
<!-- <app-karta kolor="danger" /> -->
</div>
✘ [ERROR] NG8008: Required input 'tytul' from component Karta must be specified.
Dwie karty działają normalnie. Po odkomentowaniu trzeciej ng serve nie zbuduje aplikacji. Problem zostaje złapany, zanim ktokolwiek zobaczy pustą kartę.
Zatrzymaj się
Dlaczego przy input.required trzeba podać typ <string>, a przy input('') nie trzeba?
Cały obiekt jako jedno wejście
Zamiast przekazywać pole po polu, karta dostaje cały obiekt. Interfejs Wydarzenie jest w osobnym pliku wydarzenie.ts — importują go oba komponenty. Plik utworzysz ręcznie (New File) albo poleceniem ng generate interface nowa-30-3/wydarzenie.
export interface Wydarzenie {
id: number;
tytul: string;
sala: string;
miejsca: number;
}
import { Component, input } from '@angular/core';
import { Wydarzenie } from './wydarzenie';
@Component({
selector: 'app-karta',
imports: [],
templateUrl: './karta.html'
})
export class Karta {
readonly wydarzenie = input.required<Wydarzenie>();
}
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{{ wydarzenie().tytul }}</h5>
<p class="card-text mb-1">Sala {{ wydarzenie().sala }}</p>
<p class="card-text small text-secondary">Wolnych miejsc: {{ wydarzenie().miejsca }}</p>
</div>
</div>
import { Component, signal } from '@angular/core';
import { Karta } from './karta';
import { Wydarzenie } from './wydarzenie';
@Component({
selector: 'app-demo',
imports: [Karta],
templateUrl: './demo.html'
})
export class Demo {
readonly wydarzenia = signal<Wydarzenie[]>([
{ 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 },
]);
}
<div class="p-3">
<div class="row g-3" style="max-width: 50rem">
@for (w of wydarzenia(); track w.id) {
<div class="col-md-4">
<app-karta [wydarzenie]="w" />
</div>
}
</div>
</div>
Turniej retro Noc robotów Warsztat druku 3D
Sala 12 Sala 7 Sala 14
Wolnych miejsc: 16 Wolnych miejsc: 0 Wolnych miejsc: 3
Ten sam efekt co w przykładzie 3 lekcji 29, ale wywołanie karty jest krótkie: [wydarzenie]="w". W szablonie karty najpierw odczytujesz sygnał wydarzenie(), a potem jego pole .tytul. Dopisz do interfejsu pole godzina: string — kompilator od razu pokaże w demo.ts wszystkie obiekty, w których go brakuje.
Zatrzymaj się
W szablonie karty wydarzenie() pojawia się trzy razy. Jak zapisałbyś to krócej przy pomocy @if (…; as w) z lekcji 21?
Atrybut logiczny: booleanAttribute
Karta polecana ma mieć złotą ramkę. Wejście logiczne z przekształceniem booleanAttribute pozwala zapisać to krótko: samo słowo wyroznione, bez ="true" i bez nawiasów. Ten sam komponent przyjmuje też wartość z sygnału przez [wyroznione].
import { Component, booleanAttribute, input } from '@angular/core';
@Component({
selector: 'app-karta',
imports: [],
templateUrl: './karta.html'
})
export class Karta {
readonly tytul = input.required<string>();
readonly wyroznione = input(false, { transform: booleanAttribute });
}
<div class="card p-3 mb-2" style="width: 22rem"
[class.border-warning]="wyroznione()" [class.border-3]="wyroznione()">
<h5 class="mb-0">
{{ tytul() }}
@if (wyroznione()) { <span class="badge text-bg-warning">polecamy</span> }
</h5>
</div>
import { Component, signal } from '@angular/core';
import { Karta } from './karta';
@Component({
selector: 'app-demo',
imports: [Karta],
templateUrl: './demo.html'
})
export class Demo {
readonly promocja = signal(false);
}
<div class="p-3">
<app-karta tytul="Turniej retro" />
<app-karta tytul="Noc robotów" wyroznione />
<app-karta tytul="Warsztat druku 3D" [wyroznione]="promocja()" />
<button class="btn btn-outline-warning" (click)="promocja.set(!promocja())">
Przełącz promocję druku 3D
</button>
</div>
Turniej retro
Noc robotów [polecamy] (złota ramka)
Warsztat druku 3D [polecamy] (złota ramka — przełączana przyciskiem)
Bez booleanAttribute zapis <app-karta wyroznione /> przekazałby pusty tekst '', a kompilator zgłosiłby TS2322: Type 'string’ is not assignable to type 'boolean’. Przekształcenie zamienia „atrybut jest obecny” na true. Podobnie działa numberAttribute — zamienia tekst na liczbę, więc miejsca="16" zadziałałoby bez nawiasów.
Zatrzymaj się
Jaki zapis wybierzesz, gdy karta ma być wyróżniona zawsze, a jaki — gdy zależy to od danych?
Zestawienie elementów
| Element | Znaczenie |
|---|---|
input(wartosc) | wejście opcjonalne z wartością domyślną |
input.required<Typ>() | wejście obowiązkowe; brak u rodzica = błąd NG8008 |
export interface Wydarzenie { … } | opis kształtu obiektu w osobnym pliku |
import { Wydarzenie } from './wydarzenie'; | import interfejsu w rodzicu i w dziecku |
signal<Wydarzenie[]>([…]) | tablica obiektów zgodnych z interfejsem |
{{ wydarzenie().tytul }} | odczyt pola obiektu przekazanego wejściem |
input(false, { transform: booleanAttribute }) | wejście logiczne; <app-x flaga /> daje true |
input(0, { transform: numberAttribute }) | wejście liczbowe przyjmujące tekst z atrybutu |
[class]="'text-bg-' + kolor()" | klasa sklejona z tekstu i wartości wejścia (lekcja 15) |
Zanim utkniesz
| Zapis | Problem |
|---|---|
| Brak wejścia obowiązkowego u rodzica | NG8008: Required input 'tytul’ from component Karta must be specified. Dopisz atrybut albo wiązanie. |
input.required() bez typu | Typ wejścia to unknown; przy wydarzenie().tytul błąd TS2571: Object is of type 'unknown’. Pisz input.required<Wydarzenie>(). |
interface Wydarzenie bez export | Przy imporcie TS2306: File '…/wydarzenie.ts’ is not a module. Dopisz export. |
{{ wydarzenie.tytul }} bez nawiasów | TS2339: Property 'tytul’ does not exist on type 'InputSignal<Wydarzenie>’. Najpierw odczyt sygnału: wydarzenie().tytul. |
<app-karta wyroznione /> bez booleanAttribute | TS2322: Type 'string’ is not assignable to type 'boolean’. Dodaj przekształcenie albo pisz [wyroznione]="true". |
| Ten sam interfejs przepisany osobno w dwóch plikach | Działa, dopóki są identyczne. Po zmianie jednego zaczynają się niezrozumiałe błędy typów. Jeden plik, dwa importy. |
Zadania
Komponent app-gracz z obowiązkowym wejściem nick i opcjonalnym poziom (domyślnie 1). Użyj go trzy razy, raz bez poziomu. Na koniec usuń nick z jednego użycia i przeczytaj komunikat kompilatora.
Kryterium sukcesu: brak poziomu wyświetla 1; brak nicku powoduje błąd NG8008.
Podpowiedź
input.required<string>() i input(1); poziom u rodzica przez [poziom]="5".
Komponent app-przycisk z obowiązkowym wejściem napis i wejściem kolor (domyślnie 'primary'). Rodzic wyświetla cztery przyciski: „Zapisz” (success), „Usuń” (danger), „Anuluj” (secondary) i „OK” bez koloru.
Kryterium sukcesu: kolory się zgadzają; „OK” jest niebieski; w komponencie nie ma żadnego napisu na sztywno.
Podpowiedź
[class]="'btn btn-' + kolor()" na elemencie button.
Interfejs Uczen (imie, nazwisko, klasa, srednia) w osobnym pliku. Komponent app-karta-ucznia z obowiązkowym wejściem typu Uczen. Rodzic wyświetla pętlą tablicę pięciu uczniów. Przy średniej od 4,75 w górę karta pokazuje etykietę „pasek”.
Kryterium sukcesu: każdy uczeń ma kartę z kompletem danych; etykieta pojawia się tylko przy wysokiej średniej; interfejs jest zaimportowany w dwóch plikach.
Podpowiedź
Przykład 3 plus @if (uczen().srednia >= 4.75) w karcie.
Interfejs Produkt (id, nazwa, cena). Komponent app-produkt z wejściem produkt (obowiązkowe) i logicznym promocja z booleanAttribute. W promocji karta pokazuje cenę przekreśloną i obok cenę o 20% niższą. Rodzic pokazuje trzy produkty, w tym jeden zapisany jako <app-produkt [produkt]="…" promocja />.
Kryterium sukcesu: tylko karta z atrybutem promocja ma dwie ceny; ceny mają dwa miejsca po przecinku.
Podpowiedź
<s> przekreśla tekst; cena promocyjna to produkt().cena * 0.8, formatowanie — toFixed(2) albo pipe number z lekcji 16.
Rozbuduj zadanie 3: dodaj do karty wejście logiczne kompaktowa z booleanAttribute. W trybie kompaktowym karta pokazuje tylko imię i nazwisko w jednej linii. Rodzic ma checkbox z [(ngModel)], który przełącza wszystkie karty naraz.
Kryterium sukcesu: checkbox przełącza widok wszystkich kart; w kodzie rodzica jest jeden sygnał sterujący widokiem.
Podpowiedź
[kompaktowa]="kompakt()" na każdej karcie w pętli, a w karcie @if (kompaktowa()) { … } @else { … }.
Interfejs Kurs (linia, kierunek, odjazd — tekst „HH:MM”, opoznienie — liczba minut). Komponent app-kurs z wejściem obowiązkowym kurs i opcjonalnym teraz (tekst godziny, domyślnie '12:00'). Kurs, który już odjechał (odjazd wcześniejszy niż teraz), jest wyszarzony; opóźnienie większe od 0 wyświetla czerwoną etykietę „+5 min”. Rodzic ma tablicę 6 kursów i pole type="time" z [(ngModel)] ustawiające teraz.
Kryterium sukcesu: zmiana godziny w polu wyszarza kolejne kursy; etykieta opóźnienia pojawia się tylko tam, gdzie trzeba.
Podpowiedź
Godziny „HH:MM” w tym samym formacie można porównywać jak teksty: '08:15' < '12:00' daje true. Wynik porównania trzymaj w computed karty.
Co trzeba zapamiętać
input(wartosc)— wejście opcjonalne;input.required<Typ>()— wejście obowiązkowe.- Brak wymaganego wejścia to błąd kompilacji NG8008, a nie pusta karta na stronie.
- Cały obiekt przekazujesz jednym wejściem; jego interfejs trzymaj w osobnym pliku z
export. - W dziecku najpierw odczyt sygnału, potem pole:
wydarzenie().tytul. booleanAttributepozwala pisać wejścia logiczne jak atrybuty HTML:<app-karta wyroznione />.
Sprawdź się: dla wejść: tytuł, kolor ramki, identyfikator ucznia, limit znaków — zdecyduj, które mają być obowiązkowe, a które z wartością domyślną, i uzasadnij.
Dokumentacja: angular.dev — Accepting data with input properties.