Radio — jeden wybór z kilku
Bilet normalny albo ulgowy. Rozmiar koszulki S, M albo L. Odpowiedź A, B, C albo D. Gdy z kilku możliwości wolno wybrać dokładnie jedną, używasz przycisków radio. Wszystkie w grupie łączysz z jednym sygnałem.
Czego się dziś nauczysz
- Zbudujesz grupę przycisków radio o wspólnym
name, połączonych z jednym sygnałem. - Wybierzesz między
valuea[value]w zależności od tego, czy potrzebujesz tekstu, czy liczby. - Ustawisz wartość początkową grupy albo zostawisz ją bez wyboru.
- Zbudujesz przyciski radio z tablicy obiektów i znajdziesz wybrany obiekt metodą
find. - Użyjesz zapisu
@if (wyrazenie; as nazwa). - Umieścisz kilka niezależnych grup na jednym ekranie.
Przygotowanie: lekcje 14 i 20 — znasz @switch, checkbox z ngModel i listę z @for. Przewidywany czas: 45 min z zadaniami.
Jedna z kilku
Checkboxy są niezależne: możesz zaznaczyć wszystkie albo żaden. Przyciski radio tworzą grupę: zaznaczenie jednego automatycznie odznacza poprzedni. Grupa ma wspólną wartość — tę, która jest aktualnie wybrana. W Angularze każdy przycisk w grupie ma [(ngModel)] połączone z tym samym sygnałem i własną wartość; kliknięcie wpisuje tę wartość do sygnału.
| Zasada | Szczegóły |
|---|---|
ten sam name | Przyciski o tym samym name tworzą grupę. Dwie grupy na stronie — dwie różne nazwy. |
| ten sam sygnał | Każdy przycisk grupy: [(ngModel)]="rozmiar". Sygnał trzyma wartość wybranego. |
value czy [value] | value="M" — tekst 'M'. [value]="20" — wynik wyrażenia, tu liczba 20. Ta sama różnica co src i [src] w lekcji 06. |
| wartość początkowa | signal('M') — na starcie zaznaczony M. Wartość spoza grupy ('') — nic nie jest zaznaczone. |
Rozmiar koszulki — wartości tekstowe
Trzy przyciski w jednej linii (form-check-inline), wartości tekstowe zapisane zwykłym value. Na starcie wybrane jest M.
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-demo',
imports: [FormsModule],
templateUrl: './demo.html'
})
export class Demo {
readonly rozmiar = signal('M');
}
<div class="p-3">
<p class="fw-bold mb-2">Rozmiar koszulki drużyny</p>
<div class="form-check form-check-inline">
<input id="r-s" name="rozmiar" class="form-check-input" type="radio" value="S" [(ngModel)]="rozmiar">
<label class="form-check-label" for="r-s">S</label>
</div>
<div class="form-check form-check-inline">
<input id="r-m" name="rozmiar" class="form-check-input" type="radio" value="M" [(ngModel)]="rozmiar">
<label class="form-check-label" for="r-m">M</label>
</div>
<div class="form-check form-check-inline">
<input id="r-l" name="rozmiar" class="form-check-input" type="radio" value="L" [(ngModel)]="rozmiar">
<label class="form-check-label" for="r-l">L</label>
</div>
<p class="mt-3">Zamawiasz koszulkę w rozmiarze <strong>{{ rozmiar() }}</strong>.</p>
</div>
○ S ○ M ● L
Zamawiasz koszulkę w rozmiarze L.
Każde kliknięcie odznacza poprzedni wybór. Nie da się odznaczyć wszystkich — w grupie radio zawsze coś zostaje wybrane, jeśli raz wybrano.
Zatrzymaj się
Zmień wartość początkową sygnału na 'XL'. Co będzie zaznaczone i co pokaże napis?
Wartości liczbowe: [value]
Rodzaj biletu decyduje o cenie. Wartością przycisku jest od razu cena — liczba — więc używamy [value]. Liczba biletów pochodzi z pola z lekcji 19.
import { Component, computed, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-demo',
imports: [FormsModule],
templateUrl: './demo.html'
})
export class Demo {
readonly cenaBiletu = signal(20);
readonly ile = signal<number | null>(2);
readonly razem = computed(() => this.cenaBiletu() * (this.ile() ?? 0));
}
<div class="card m-3 p-3" style="width: 24rem">
<div class="form-check">
<input id="normalny" name="bilet" class="form-check-input" type="radio"
[value]="20" [(ngModel)]="cenaBiletu">
<label class="form-check-label" for="normalny">Normalny — 20 zł</label>
</div>
<div class="form-check">
<input id="ulgowy" name="bilet" class="form-check-input" type="radio"
[value]="12" [(ngModel)]="cenaBiletu">
<label class="form-check-label" for="ulgowy">Ulgowy (uczeń) — 12 zł</label>
</div>
<div class="form-check mb-3">
<input id="rodzinny" name="bilet" class="form-check-input" type="radio"
[value]="15" [(ngModel)]="cenaBiletu">
<label class="form-check-label" for="rodzinny">Rodzinny — 15 zł</label>
</div>
<label class="form-label" for="ile">Liczba biletów</label>
<input id="ile" name="ile" type="number" min="1" class="form-control mb-3" [(ngModel)]="ile">
<p class="fs-5 fw-bold mb-0">Razem: {{ razem() }} zł</p>
</div>
na starcie (normalny, 2 szt.): Razem: 40 zł
po wybraniu ulgowego: Razem: 24 zł
value zamiast [value]
Zamień w jednym przycisku [value]="12" na value="12" i wybierz go. Mnożenie nadal działa (JavaScript zamienia tekst na liczbę przy *), ale przy dodawaniu dostałbyś doklejanie z lekcji 19, a warunek cenaBiletu() === 12 przestałby działać — '12' to nie 12.
Zatrzymaj się
Trzymamy w sygnale cenę, a nie nazwę biletu. Co by było, gdyby trzeba było pokazać na paragonie słowo „ulgowy”?
Przyciski z tablicy obiektów
Sposób dostawy wybierany z listy. Opcje są w tablicy, przyciski powstają w pętli, a wartością każdego jest id. Pełne dane wybranej opcji wyszukuje computed metodą find — zwraca pierwszy element, dla którego warunek jest prawdziwy, albo undefined.
import { Component, computed, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
interface Dostawa {
id: number;
nazwa: string;
cena: number;
dni: number;
}
@Component({
selector: 'app-demo',
imports: [FormsModule],
templateUrl: './demo.html'
})
export class Demo {
readonly dostawy: Dostawa[] = [
{ id: 1, nazwa: 'Odbiór w sekretariacie', cena: 0, dni: 1 },
{ id: 2, nazwa: 'Paczkomat', cena: 12.99, dni: 2 },
{ id: 3, nazwa: 'Kurier', cena: 17.5, dni: 1 },
];
readonly wybranaId = signal(1);
readonly wybrana = computed(() => this.dostawy.find(d => d.id === this.wybranaId()));
}
<div class="p-3" style="width: 28rem">
<p class="fw-bold">Jak dostarczyć koszulki?</p>
@for (d of dostawy; track d.id) {
<div class="form-check">
<input [id]="'dost' + d.id" name="dostawa" class="form-check-input" type="radio"
[value]="d.id" [(ngModel)]="wybranaId">
<label class="form-check-label" [for]="'dost' + d.id">{{ d.nazwa }}</label>
</div>
}
@if (wybrana(); as w) {
<div class="alert alert-info mt-3 mb-0">
{{ w.nazwa }}: {{ w.cena === 0 ? 'bezpłatnie' : w.cena.toFixed(2) + ' zł' }},
czas: {{ w.dni }} dni
</div>
}
</div>
Paczkomat: 12.99 zł, czas: 2 dni
@if (wybrana(); as w) robi dwie rzeczy: sprawdza, czy find coś znalazł, i nadaje wynikowi krótką nazwę w. Wewnątrz bloku TypeScript wie już, że w na pewno istnieje. alert-info to niebieska ramka.
Zatrzymaj się
Dlaczego wartością przycisku jest d.id, a nie cały obiekt d? Wskazówka: pomyśl o wartości początkowej sygnału.
Dwie grupy na jednym ekranie — mini quiz
Dwa pytania, każde z własną grupą odpowiedzi. Grupy różnią się atrybutem name i mają osobne sygnały. Przycisk „Sprawdź” liczy punkty.
import { Component, computed, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-demo',
imports: [FormsModule],
templateUrl: './demo.html'
})
export class Demo {
readonly odp1 = signal('');
readonly odp2 = signal('');
readonly sprawdzone = signal(false);
readonly kompletne = computed(() => this.odp1() !== '' && this.odp2() !== '');
readonly punkty = computed(() =>
(this.odp1() === 'b' ? 1 : 0) + (this.odp2() === 'a' ? 1 : 0)
);
}
<div class="p-3" style="width: 30rem">
<p class="fw-bold mb-1">1. Który znacznik HTML tworzy pole wielowierszowe?</p>
<div class="form-check">
<input id="p1a" name="pytanie1" class="form-check-input" type="radio" value="a" [(ngModel)]="odp1">
<label class="form-check-label" for="p1a"><input type="multi"></label>
</div>
<div class="form-check">
<input id="p1b" name="pytanie1" class="form-check-input" type="radio" value="b" [(ngModel)]="odp1">
<label class="form-check-label" for="p1b"><textarea></label>
</div>
<div class="form-check mb-3">
<input id="p1c" name="pytanie1" class="form-check-input" type="radio" value="c" [(ngModel)]="odp1">
<label class="form-check-label" for="p1c"><text></label>
</div>
<p class="fw-bold mb-1">2. Co zwraca <code>pole.value</code> z pola <code>type="number"</code>?</p>
<div class="form-check">
<input id="p2a" name="pytanie2" class="form-check-input" type="radio" value="a" [(ngModel)]="odp2">
<label class="form-check-label" for="p2a">tekst</label>
</div>
<div class="form-check mb-3">
<input id="p2b" name="pytanie2" class="form-check-input" type="radio" value="b" [(ngModel)]="odp2">
<label class="form-check-label" for="p2b">liczbę</label>
</div>
<button class="btn btn-primary" [disabled]="!kompletne()" (click)="sprawdzone.set(true)">Sprawdź</button>
@if (sprawdzone()) {
<p class="mt-3 fs-5" [class.text-success]="punkty() === 2">Wynik: {{ punkty() }} / 2</p>
}
</div>
Wynik: 2 / 2 (zielony)
Przycisk ożywa dopiero po odpowiedzi na oba pytania. Wybór w drugim pytaniu nie rusza pierwszego, bo grupy mają różne name.
Zatrzymaj się
Zmień w drugim pytaniu name="pytanie2" na name="pytanie1". Co się dzieje przy klikaniu? Spróbuj to wyjaśnić, zanim przeczytasz tabelę błędów.
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
type="radio" + wspólny name | grupa jednego wyboru | Jeden sygnał na grupę. |
value="M" | wartość tekstowa | Do sygnału trafia 'M'. |
[value]="20", [value]="d.id" | wartość z wyrażenia | Liczby, pola obiektów. |
form-check-inline | przyciski w jednej linii | Bootstrap. |
tablica.find(x => warunek) | pierwszy pasujący element | Albo undefined. |
@if (wyr; as nazwa) | warunek i krótka nazwa wyniku | Dla undefined/null. |
alert-info | niebieska ramka | Bootstrap. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
Dwie grupy z tym samym name | Wybór w jednej grupie odznacza przycisk w drugiej, a sygnały zaczynają się „kłócić”. Każda grupa — własna nazwa. |
value="20", a potem cenaBiletu() + 5 | Wynik 205 zamiast 25. Do liczb używaj [value]="20". |
[value]="S" zamiast value="S" | Angular szuka w klasie pola S i zgłasza błąd. Tekst w nawiasach kwadratowych — w apostrofach: [value]="'S'". |
| Każdy przycisk połączony z innym sygnałem | Grupa nie ma jednej wspólnej wartości. Grupa = jeden sygnał. |
Brak name przy przyciskach radio | Przeglądarka nie wie, że przyciski należą do jednej grupy. Atrybut name jest tu obowiązkowy. |
{{ wybrana().nazwa }} bez @if … as | Błąd: Object is possibly 'undefined’ — find mógł nic nie znaleźć. |
Zadania
Grupa trzech przycisków: rano, po południu, wieczorem. Pod spodem odpowiednie powitanie: „Dzień dobry”, „Witaj”, „Dobry wieczór”.
Kryterium sukcesu: wybór zmienia powitanie; na starcie nic nie jest zaznaczone, a zamiast powitania jest prośba o wybór.
Podpowiedź
signal('') i @switch z lekcji 14 z @default dla prośby.
Grupa sześciu przycisków w jednej linii z ocenami 1–6 (wartości liczbowe). Pod spodem tyle gwiazdek ★, ile wynosi ocena, i komentarz: 1–2 „Przykro nam”, 3–4 „Dziękujemy”, 5–6 „Super!”.
Kryterium sukcesu: gwiazdek jest tyle, ile wybrana ocena, a komentarz pasuje do zakresu.
Podpowiedź
[value]="1" … [value]="6", gwiazdki: '★'.repeat(ocena()).
Trzy pakiety internetu z tablicy obiektów { id, nazwa, predkosc, cena }. Przyciski radio powstają w pętli, a wybrany pakiet jest pokazany jako karta z wyróżnioną ceną.
Kryterium sukcesu: dopisanie czwartego pakietu do tablicy dodaje przycisk bez zmian w szablonie; karta zawsze pokazuje wybrany pakiet.
Podpowiedź
Przykład 3 — wartością przycisku jest id, kartę zbuduj w @if (wybrany(); as p).
Dwie grupy na jednym ekranie: dostawa (odbiór osobisty 0 zł, paczkomat 12 zł, kurier 18 zł) i płatność (przelew 0 zł, za pobraniem +5 zł). Wartość zamówienia 80 zł. Pokaż koszt dostawy, dopłatę i sumę.
Kryterium sukcesu: obie grupy działają niezależnie, suma zgadza się z rachunkiem dla każdej kombinacji.
Podpowiedź
Dwie grupy z różnym name, wartości liczbowe przez [value], suma w computed.
Pytania quizu zapisz w tablicy obiektów: treść, tablica odpowiedzi i numer poprawnej. Wyświetl wszystkie pytania pętlą, każde z własną grupą radio. Po kliknięciu „Sprawdź” pokaż wynik, a poprawne odpowiedzi podświetl na zielono.
Kryterium sukcesu: quiz działa dla dowolnej liczby pytań; grupy się nie mieszają.
Podpowiedź
Odpowiedzi trzymaj w jednym sygnale z tablicą numerów, np. signal<number[]>([-1, -1, -1]), i obsługuj jak listę checkboxów w lekcji 20: [checked] i (change). Atrybut name zbuduj z numeru pytania: [name]="'p' + i".
Kino szkolne: grupa „rodzaj seansu” (2D 15 zł, 3D 20 zł), grupa „bilet” (normalny 100%, ulgowy 70%, senior 50% — wartości to ułamki) i pole liczbowe „liczba osób”. Pokaż cenę jednego biletu, sumę z dwoma miejscami po przecinku oraz — przy co najmniej 10 osobach — rabat grupowy 10% i kwotę po rabacie.
Kryterium sukcesu: każda zmiana w dowolnej grupie albo w polu przelicza wszystkie kwoty; rabat pojawia się dopiero od 10 osób.
Podpowiedź
[value]="0.7" dla ulgowego. Kwoty — computed i pipe currency:'PLN' z lekcji 16.
Co trzeba zapamiętać
- Grupa radio to przyciski z tym samym
namei tym samym sygnałem w[(ngModel)]. - Wybrany przycisk wpisuje do sygnału swoją wartość:
value="…"daje tekst,[value]="…"— wynik wyrażenia, np. liczbę. - Wartość początkowa sygnału decyduje, który przycisk jest zaznaczony na starcie.
- Opcje z tablicy: przyciski z pętli, wartością jest
id, a pełne dane wyszukujecomputedzfind. @if (x; as nazwa)sprawdza, czy wynik istnieje, i nadaje mu nazwę.
Sprawdź się: kiedy wybierzesz grupę radio, a kiedy checkboxy? Podaj po jednym przykładzie z formularza zapisu na wycieczkę.
Dokumentacja: Bootstrap — przyciski radio.