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

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.

Angular 22 input.required interfejs w osobnym pliku export booleanAttribute 45 min
CEL LEKCJI

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.

TEORIA

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

PRZYKŁAD 1

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.

src/app/nowa-30-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 kolor = input('secondary');
}
src/app/nowa-30-1/karta.html — pełny plik
<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>
src/app/nowa-30-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-30-1/demo.html — pełny plik
<div class="p-3">
  <app-karta tytul="Turniej retro" />
  <app-karta tytul="Noc robotów" kolor="success" />
  <app-karta kolor="danger" />
</div>
wynik
[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ą?

PRZYKŁAD 2

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.

src/app/nowa-30-2/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.required<string>();
    readonly kolor = input('secondary');
}
src/app/nowa-30-2/karta.html — pełny plik
<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>
src/app/nowa-30-2/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-30-2/demo.html — pełny plik
<div class="p-3">
  <app-karta tytul="Turniej retro" />
  <app-karta tytul="Noc robotów" kolor="success" />
  <!-- <app-karta kolor="danger" /> -->
</div>
terminal po odkomentowaniu trzeciej karty
✘ [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?

PRZYKŁAD 3

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.

src/app/nowa-30-3/wydarzenie.ts — pełny plik
export interface Wydarzenie {
    id: number;
    tytul: string;
    sala: string;
    miejsca: number;
}
src/app/nowa-30-3/karta.ts — pełny plik
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>();
}
src/app/nowa-30-3/karta.html — pełny plik
<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>
src/app/nowa-30-3/demo.ts — pełny plik
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 },
    ]);
}
src/app/nowa-30-3/demo.html — pełny plik
<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>
wynik (trzy karty w rzędzie)
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?

PRZYKŁAD 4

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

src/app/nowa-30-4/karta.ts — pełny plik
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 });
}
src/app/nowa-30-4/karta.html — pełny plik
<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>
src/app/nowa-30-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 promocja = signal(false);
}
src/app/nowa-30-4/demo.html — pełny plik
<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>
wynik po kliknięciu przycisku
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?

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenie
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)
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
Brak wejścia obowiązkowego u rodzicaNG8008: Required input 'tytul’ from component Karta must be specified. Dopisz atrybut albo wiązanie.
input.required() bez typuTyp wejścia to unknown; przy wydarzenie().tytul błąd TS2571: Object is of type 'unknown’. Pisz input.required<Wydarzenie>().
interface Wydarzenie bez exportPrzy imporcie TS2306: File '…/wydarzenie.ts’ is not a module. Dopisz export.
{{ wydarzenie.tytul }} bez nawiasówTS2339: Property 'tytul’ does not exist on type 'InputSignal<Wydarzenie>’. Najpierw odczyt sygnału: wydarzenie().tytul.
<app-karta wyroznione /> bez booleanAttributeTS2322: Type 'string’ is not assignable to type 'boolean’. Dodaj przekształcenie albo pisz [wyroznione]="true".
Ten sam interfejs przepisany osobno w dwóch plikachDziała, dopóki są identyczne. Po zmianie jednego zaczynają się niezrozumiałe błędy typów. Jeden plik, dwa importy.
ZADANIA

Zadania

ZAD 1Obowiązkowy nick★☆☆

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

ZAD 2Przycisk w kolorze★☆☆

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.

ZAD 3Karta ucznia★★☆

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.

ZAD 4Produkt w promocji★★☆

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.

ZAD 5Kompaktowa lista★★★

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 { … }.

ZAD 6Rozkład jazdy★★★

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.

PODSUMOWANIE

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.
  • booleanAttribute pozwala 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.

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