Select — lista rozwijana
Klasa, województwo, kategoria wydarzenia. Gdy możliwości jest za dużo na przyciski radio, a wybrać trzeba jedną, używasz listy rozwijanej <select>. Z ngModel działa jak grupa radio, tylko zajmuje jedną linijkę — i potrafi przechować cały obiekt.
Czego się dziś nauczysz
- Połączysz listę rozwijaną z sygnałem i zbudujesz opcje pętlą
@for. - Odróżnisz
value(tekst) od[ngValue](dowolna wartość, także obiekt). - Ustawisz wartość początkową listy z obiektami tak, żeby nie była pusta.
- Dodasz opcję „— wybierz —” z wartością
null. - Zbudujesz dwie listy zależne od siebie i wyczyścisz drugą przy zmianie pierwszej.
- Użyjesz typu
Record<string, string[]>iObject.keys.
Przygotowanie: lekcja 21 — znasz różnicę między value a [value] i metodę find. Przewidywany czas: 45 min z zadaniami.
Dużo możliwości, mało miejsca
Szesnaście województw jako przyciski radio zajęłoby pół ekranu. Lista rozwijana pokazuje tylko wybraną wartość, a resztę chowa do kliknięcia. Zasada jest ta sama co przy radio: jeden sygnał przechowuje wartość wybranej opcji.
<select id="klasa" name="klasa" class="form-select" [(ngModel)]="klasa">
<option value="1TP">1TP</option>
<option value="2TP">2TP</option>
</select>
| Zapis opcji | Co trafia do sygnału |
|---|---|
<option value="1TP"> | tekst '1TP' |
<option [ngValue]="3"> | liczba 3 |
<option [ngValue]="w"> | cały obiekt w |
<option [ngValue]="null" disabled> | null — opcja „— wybierz —”, której nie da się wybrać ponownie |
W select: [ngValue], nie [value]
Przy radio używałeś [value]. W liście rozwijanej do wartości innych niż tekst używaj [ngValue] — tylko ono poprawnie przenosi liczby, obiekty i null. [value] zamieniłoby obiekt na napis [object Object].
Pierwsza lista rozwijana
Wybór klasy z czterech wpisanych ręcznie opcji. Wartości są tekstowe, na starcie wybrana jest druga klasa. form-select to wygląd z Bootstrapa.
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-demo',
imports: [FormsModule],
templateUrl: './demo.html'
})
export class Demo {
readonly klasa = signal('2TP');
}
<div class="p-3" style="width: 20rem">
<label class="form-label" for="klasa">Twoja klasa</label>
<select id="klasa" name="klasa" class="form-select" [(ngModel)]="klasa">
<option value="1TP">1TP</option>
<option value="2TP">2TP</option>
<option value="3TP">3TP</option>
<option value="4TP">4TP</option>
</select>
<p class="mt-3">Wybrano: <strong>{{ klasa() }}</strong></p>
</div>
[3TP ▾]
Wybrano: 3TP
Na starcie lista pokazuje 2TP, bo taką wartość ma sygnał. Zamiast myszki możesz użyć strzałek na klawiaturze, gdy lista ma fokus.
Zatrzymaj się
Co pokaże lista, gdy wartość początkowa sygnału będzie '5TP', której nie ma wśród opcji?
Opcje z tablicy obiektów i [ngValue]
Lista wydarzeń z tablicy. Dzięki [ngValue] sygnał przechowuje cały obiekt wybranego wydarzenia — więc od razu możemy pokazać salę i godzinę, bez szukania find.
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
interface Wydarzenie {
id: number;
tytul: string;
sala: string;
godzina: string;
}
@Component({
selector: 'app-demo',
imports: [FormsModule],
templateUrl: './demo.html'
})
export class Demo {
readonly wydarzenia: Wydarzenie[] = [
{ id: 1, tytul: 'Turniej retro', sala: '12', godzina: '16:00' },
{ id: 2, tytul: 'Noc robotów', sala: '7', godzina: '18:30' },
{ id: 3, tytul: 'Warsztat druku 3D', sala: '14', godzina: '15:15' },
];
readonly wybrane = signal<Wydarzenie>(this.wydarzenia[0]);
}
<div class="p-3" style="width: 26rem">
<label class="form-label" for="wyd">Wydarzenie</label>
<select id="wyd" name="wyd" class="form-select" [(ngModel)]="wybrane">
@for (w of wydarzenia; track w.id) {
<option [ngValue]="w">{{ w.tytul }}</option>
}
</select>
<div class="card mt-3">
<div class="card-body">
<h5 class="card-title">{{ wybrane().tytul }}</h5>
<p class="card-text mb-0">Sala {{ wybrane().sala }}, godz. {{ wybrane().godzina }}</p>
</div>
</div>
</div>
Noc robotów
Sala 7, godz. 18:30
Wartość początkowa — z tej samej tablicy
Gdybyś napisał signal({ id: 1, tytul: 'Turniej retro', … }), lista na starcie byłaby pusta. To obiekt o tych samych danych, ale inny obiekt w pamięci, a lista porównuje opcje przez ===. Wartość początkowa to zawsze element tej samej tablicy: this.wydarzenia[0].
Zatrzymaj się
Porównaj z przykładem 3 lekcji 21, w którym wartością było id, a obiekt wyszukiwał find. Która wersja jest krótsza, a która bezpieczniejsza, gdy dane przyjdą z serwera?
Opcja „— wybierz —” i wartość null
W formularzu nie chcemy, żeby lista sama wybierała pierwszą wartość — użytkownik ma świadomie coś wybrać. Dodajemy opcję zastępczą z wartością null, a sygnał ma typ string | null.
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-demo',
imports: [FormsModule],
templateUrl: './demo.html'
})
export class Demo {
readonly jezyki = ['C#', 'TypeScript', 'Kotlin', 'Python', 'Java'];
readonly jezyk = signal<string | null>(null);
}
<div class="p-3" style="width: 24rem">
<label class="form-label" for="jezyk">Ulubiony język programowania</label>
<select id="jezyk" name="jezyk" class="form-select" [(ngModel)]="jezyk">
<option [ngValue]="null" disabled>— wybierz —</option>
@for (j of jezyki; track j) {
<option [ngValue]="j">{{ j }}</option>
}
</select>
@if (jezyk() === null) {
<p class="form-text">Nic jeszcze nie wybrano.</p>
} @else {
<p class="mt-3">Świetny wybór: <strong>{{ jezyk() }}</strong>!</p>
<button class="btn btn-sm btn-outline-secondary" (click)="jezyk.set(null)">Wyczyść wybór</button>
}
</div>
na starcie: [— wybierz — ▾] Nic jeszcze nie wybrano.
po wybraniu Kotlin: Świetny wybór: Kotlin! [Wyczyść wybór]
Opcji „— wybierz —” nie da się kliknąć (disabled). Przycisk „Wyczyść wybór” ustawia sygnał na null — i lista wraca do napisu zastępczego.
Zatrzymaj się
Co zgłosi TypeScript, jeśli zostawisz samo signal(null), bez typu, a potem spróbujesz wybrać z listy „Kotlin”?
Dwie listy zależne od siebie
Najpierw kategoria, potem wydarzenie z tej kategorii. Opcje drugiej listy liczy computed. Gdy zmienia się kategoria, wybór w drugiej liście trzeba wyczyścić — robimy to zdarzeniem (change).
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 katalog: Record<string, string[]> = {
'Gry': ['Turniej retro', 'Liga e-sportowa', 'Planszówki'],
'Technika': ['Noc robotów', 'Warsztat druku 3D', 'Lutowanie dla początkujących'],
'Sztuka': ['Grafika komputerowa', 'Fotografia'],
};
readonly kategorie = Object.keys(this.katalog);
readonly kategoria = signal<string | null>(null);
readonly wydarzenie = signal<string | null>(null);
readonly wydarzenia = computed(() => {
const k = this.kategoria();
return k === null ? [] : this.katalog[k];
});
}
<div class="p-3" style="width: 26rem">
<label class="form-label" for="kat">Kategoria</label>
<select id="kat" name="kat" class="form-select mb-3"
[(ngModel)]="kategoria" (change)="wydarzenie.set(null)">
<option [ngValue]="null" disabled>— wybierz kategorię —</option>
@for (k of kategorie; track k) {
<option [ngValue]="k">{{ k }}</option>
}
</select>
<label class="form-label" for="wyd">Wydarzenie</label>
<select id="wyd" name="wyd" class="form-select" [(ngModel)]="wydarzenie"
[disabled]="kategoria() === null">
<option [ngValue]="null" disabled>— wybierz wydarzenie —</option>
@for (w of wydarzenia(); track $index) {
<option [ngValue]="w">{{ w }}</option>
}
</select>
<p class="mt-3">Zapis: {{ kategoria() ?? '?' }} → {{ wydarzenie() ?? '?' }}</p>
</div>
Zapis: Sztuka → ?
Record<string, string[]> to typ obiektu, w którym kluczami są teksty (nazwy kategorii), a wartościami — tablice tekstów. Object.keys(…) zwraca tablicę samych kluczy. Operator ?? z lekcji 19 zamienia null na znak zapytania.
Zatrzymaj się
Usuń (change)="wydarzenie.set(null)" i powtórz test: wybierz wydarzenie z Gier, potem przełącz na Sztukę. Co pokazuje napis na dole i dlaczego to błąd?
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
<select [(ngModel)]>, form-select | lista rozwijana | Jeden sygnał. |
<option value="…"> | opcja tekstowa | Tekst w sygnale. |
<option [ngValue]="x"> | opcja z dowolną wartością | Liczby, obiekty, null. |
[ngValue]="null" disabled | „— wybierz —” | Sygnał startuje od null. |
signal<string | null>(null) | tekst albo brak wyboru | Typ obowiązkowy. |
Record<string, string[]> | słownik: klucz → tablica | TypeScript. |
Object.keys(obiekt) | tablica kluczy | Opcje pierwszej listy. |
(change) na select | reakcja na zmianę wyboru | Czyszczenie listy zależnej. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
<option [value]="w"> z obiektem | W sygnale ląduje napis [object Object]. Dla obiektów zawsze [ngValue]. |
| Wartość początkowa jako nowy obiekt z tymi samymi danymi | Lista na starcie jest pusta, bo porównuje obiekty przez ===. Bierz obiekt z tej samej tablicy. |
signal(null) bez typu | TypeScript uznaje, że w sygnale może być tylko null. Pisz signal<string | null>(null). |
Opcja „— wybierz —” z value="" przy sygnale null | Na starcie lista jest pusta zamiast pokazywać napis zastępczy: '' to nie null. |
| Druga lista nie czyści się po zmianie pierwszej | Sygnał trzyma wydarzenie z poprzedniej kategorii. Wyczyść go przy zmianie pierwszej listy. |
Brak track w @for opcji | NG5002 — tak samo jak w każdej pętli. |
Zadania
Lista rozwijana z dniami tygodnia zbudowana pętlą z tablicy. Pod nią plan: w poniedziałek i środę „zajęcia koła”, w pozostałe dni — „wolne”.
Kryterium sukcesu: opcje pochodzą z tablicy; komunikat zgadza się z wybranym dniem.
Podpowiedź
@if (dzien() === 'poniedziałek' || dzien() === 'środa').
Lista rozwijana z liczbami 1–8 (wartości liczbowe przez [ngValue]) i cena biletu 15 zł. Pod listą kwota do zapłaty. Sprawdź, że kwota jest liczona, a nie doklejana.
Kryterium sukcesu: dla 3 biletów widać 45 zł, a nie „315”.
Podpowiedź
Tablicę liczb zrób jak w lekcji 13: Array.from({ length: 8 }, (_, i) => i + 1).
Pole z kwotą w złotych (number) i lista walut — obiekty { kod, nazwa, kurs } (EUR, USD, GBP, CHF). Pokaż kwotę po przeliczeniu z dwoma miejscami po przecinku i kodem waluty.
Kryterium sukcesu: wynik zmienia się przy zmianie kwoty i waluty; wartość początkowa listy działa.
Podpowiedź
Lista z [ngValue]="w", sygnał startuje od this.waluty[0]. Kursy wpisz na stałe.
Lista rozwijana z salami (obiekty { nr, nazwa, miejsc, rzutnik }) i opcją „— wybierz salę —”. Po wyborze pokaż kartę sali: nazwę, liczbę miejsc i informację o rzutniku (zielona odznaka „rzutnik” albo szara „brak rzutnika”). Przycisk „Wyczyść” wraca do opcji zastępczej.
Kryterium sukcesu: na starcie lista pokazuje „— wybierz salę —”, karta pojawia się po wyborze i znika po wyczyszczeniu.
Podpowiedź
Sygnał typu Sala | null i @if (sala(); as s).
Dwie listy zależne: trzy województwa i po trzy powiaty w każdym (np. pomorskie: lęborski, słupski, wejherowski). Pod listami tekst „Mieszkasz w powiecie … (woj. …)”, widoczny dopiero po wybraniu obu.
Kryterium sukcesu: druga lista pokazuje tylko powiaty z wybranego województwa, jest nieaktywna bez województwa i czyści się przy jego zmianie.
Podpowiedź
Przykład 4 to gotowy szkielet — zmień dane i komunikat.
Trzy listy: zupa, drugie danie, napój — każda z obiektami { nazwa, cena } i opcją „— brak —” (null). Pokaż zamówienie jako listę wybranych pozycji, sumę oraz — gdy wybrano wszystkie trzy — dopisek „Zestaw dnia: −10%” i kwotę po rabacie.
Kryterium sukcesu: suma uwzględnia tylko wybrane pozycje, a rabat pojawia się wyłącznie przy komplecie trzech.
Podpowiedź
Trzy sygnały typu Danie | null; sumę policz w computed z ?.cena ?? 0, np. (this.zupa()?.cena ?? 0).
Co trzeba zapamiętać
<select [(ngModel)]="sygnal">działa jak grupa radio: sygnał trzyma wartość wybranej opcji.valuedaje tekst;[ngValue]— dowolną wartość, także obiekt inull.- Wartość początkowa przy obiektach musi być tym samym obiektem co w tablicy opcji.
- Opcja
[ngValue]="null" disabledi sygnał startujący odnullwymuszają świadomy wybór. - Przy listach zależnych czyść drugą listę po zmianie pierwszej.
Sprawdź się: lista rozwijana na starcie jest pusta, choć sygnał ma wartość. Wymień dwie najczęstsze przyczyny.
Dokumentacja: angular.dev — opcje listy (ngValue).