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

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.

Angular 22 radio name value [value] @if … as find 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Zbudujesz grupę przycisków radio o wspólnym name, połączonych z jednym sygnałem.
  • Wybierzesz między value a [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.

TEORIA

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.

ZasadaSzczegóły
ten sam namePrzyciski 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ątkowasignal('M') — na starcie zaznaczony M. Wartość spoza grupy ('') — nic nie jest zaznaczone.
PRZYKŁAD 1

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.

src/app/nowa-21-1/demo.ts — pełny plik
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');
}
src/app/nowa-21-1/demo.html — pełny plik
<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>
wynik po kliknięciu L
○ 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?

PRZYKŁAD 2

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.

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

PRZYKŁAD 3

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.

src/app/nowa-21-3/demo.ts — pełny plik
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()));
}
src/app/nowa-21-3/demo.html — pełny plik
<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>
wynik po wybraniu Paczkomatu
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.

PRZYKŁAD 4

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.

src/app/nowa-21-4/demo.ts — pełny plik
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)
    );
}
src/app/nowa-21-4/demo.html — pełny plik
<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">&lt;input type="multi"&gt;</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">&lt;textarea&gt;</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">&lt;text&gt;</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 po poprawnych odpowiedziach
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.

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
type="radio" + wspólny namegrupa jednego wyboruJeden sygnał na grupę.
value="M"wartość tekstowaDo sygnału trafia 'M'.
[value]="20", [value]="d.id"wartość z wyrażeniaLiczby, pola obiektów.
form-check-inlineprzyciski w jednej liniiBootstrap.
tablica.find(x => warunek)pierwszy pasujący elementAlbo undefined.
@if (wyr; as nazwa)warunek i krótka nazwa wynikuDla undefined/null.
alert-infoniebieska ramkaBootstrap.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
Dwie grupy z tym samym nameWybó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() + 5Wynik 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łemGrupa nie ma jednej wspólnej wartości. Grupa = jeden sygnał.
Brak name przy przyciskach radioPrzeglądarka nie wie, że przyciski należą do jednej grupy. Atrybut name jest tu obowiązkowy.
{{ wybrana().nazwa }} bez @if … asBłąd: Object is possibly 'undefined’ — find mógł nic nie znaleźć.
ZADANIA

Zadania

ZAD 1Pora dnia★☆☆

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.

ZAD 2Ocena zajęć★☆☆

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

ZAD 3Wybór pakietu★★☆

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

ZAD 4Dostawa i płatność★★☆

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.

ZAD 5Quiz z tablicy★★★

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

ZAD 6Taryfa kina szkolnego★★★

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.

PODSUMOWANIE

Co trzeba zapamiętać

  • Grupa radio to przyciski z tym samym name i 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 wyszukuje computed z find.
  • @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.

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