Checkbox
„Akceptuję regulamin”, „Chcę dostawać powiadomienia”, „Dodatkowy ser”. Pole wyboru odpowiada na pytanie „tak czy nie?”. Z ngModel do sygnału trafia true albo false — i to wystarczy, żeby blokować przyciski, doliczać ceny i budować listy wyborów.
Czego się dziś nauczysz
- Zbudujesz checkbox z etykietą w znacznikach Bootstrapa (
form-check). - Połączysz checkbox z sygnałem logicznym i zablokujesz nim przycisk.
- Policzysz cenę z kilku niezależnych opcji w
computed. - Obsłużysz listę checkboxów z tablicy jednym sygnałem z tablicą wybranych.
- Użyjesz
[checked]i(change)zamiastngModeltam, gdzie wygodniej. - Zbudujesz checkbox „zaznacz wszystkie”.
- Nadasz checkboxom w pętli unikalne
id.
Przygotowanie: lekcje 13, 15 i 17–19 — znasz @for, [(ngModel)], [disabled] i computed. Przewidywany czas: 45 min z zadaniami.
Tak albo nie
Pole tekstowe może zawierać cokolwiek, liczbowe — dowolną liczbę. Checkbox ma tylko dwa stany: zaznaczony albo nie. Dlatego to najprostsza kontrolka do obsłużenia w kodzie: sygnał typu boolean i gotowe. Schody zaczynają się, gdy checkboxów jest kilka i powstają z tablicy — wtedy trzeba zdecydować, gdzie przechowywać informację o tym, co zaznaczono.
<div class="form-check">
<input id="zgoda" name="zgoda" class="form-check-input" type="checkbox" [(ngModel)]="zgoda">
<label class="form-check-label" for="zgoda">Akceptuję regulamin</label>
</div>
| Sytuacja | Rozwiązanie |
|---|---|
| jeden checkbox | readonly zgoda = signal(false); i [(ngModel)]="zgoda" |
| kilka stałych opcji | osobny sygnał boolean dla każdej |
| opcje z tablicy | jeden sygnał z tablicą wybranych; [checked]="wybrane().includes(x)" i (change)="przelacz(x)" |
[checked] i (change) — bez ngModel
[checked] to zwykłe wiązanie właściwości z lekcji 06 (klasa → checkbox), a (change) — zwykłe zdarzenie z lekcji 08 (checkbox → klasa). Razem dają to samo co ngModel, tylko ręcznie — i bez potrzeby importu FormsModule. Przy liście z tablicy to wygodniejsze, bo zaznaczenie nie jest osobnym sygnałem, tylko wynika z tablicy wybranych.
Zgoda blokująca przycisk
Klasyka: przycisk „Zapisz się” jest nieaktywny, dopóki użytkownik nie zaakceptuje regulaminu.
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-demo',
imports: [FormsModule],
templateUrl: './demo.html'
})
export class Demo {
readonly regulamin = signal(false);
readonly zapisano = signal(false);
}
<div class="card m-3 p-3" style="width: 26rem">
<h5>Turniej retro</h5>
<div class="form-check mb-3">
<input id="regulamin" name="regulamin" class="form-check-input" type="checkbox"
[(ngModel)]="regulamin">
<label class="form-check-label" for="regulamin">Akceptuję regulamin turnieju</label>
</div>
<button class="btn btn-success" [disabled]="!regulamin()" (click)="zapisano.set(true)">
Zapisz się
</button>
@if (zapisano()) {
<p class="text-success mt-3 mb-0">Zapisano! Do zobaczenia w sali 12.</p>
}
<p class="small text-secondary mt-3 mb-0">regulamin() = {{ regulamin() }}</p>
</div>
☑ Akceptuję regulamin turnieju
[Zapisz się] (aktywny)
Zapisano! Do zobaczenia w sali 12.
regulamin() = true
Kliknij w tekst etykiety (nie w kwadracik) — checkbox też się zaznacza, bo etykieta ma for. Ostatni wiersz pokazuje, co dokładnie jest w sygnale.
Zatrzymaj się
Po zapisaniu odznacz regulamin. Komunikat o zapisie nie znika. Czy to błąd? Jak byś to zaprojektował?
Kilka niezależnych opcji
Zamówienie pizzy na nocny turniej: cena podstawowa plus dodatki. Każdy dodatek to osobny checkbox i osobny sygnał, a cenę liczy computed.
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 bazowa = 28;
readonly ser = signal(false);
readonly pieczarki = signal(false);
readonly jalapeno = signal(false);
readonly cena = computed(() => {
let suma = this.bazowa;
if (this.ser()) { suma += 4; }
if (this.pieczarki()) { suma += 3; }
if (this.jalapeno()) { suma += 3.5; }
return suma;
});
}
<div class="card m-3 p-3" style="width: 24rem">
<h5>Pizza Margherita — {{ bazowa }} zł</h5>
<p class="mb-2">Dodatki:</p>
<div class="form-check">
<input id="ser" name="ser" class="form-check-input" type="checkbox" [(ngModel)]="ser">
<label class="form-check-label" for="ser">Podwójny ser (+4 zł)</label>
</div>
<div class="form-check">
<input id="pieczarki" name="pieczarki" class="form-check-input" type="checkbox" [(ngModel)]="pieczarki">
<label class="form-check-label" for="pieczarki">Pieczarki (+3 zł)</label>
</div>
<div class="form-check mb-3">
<input id="jalapeno" name="jalapeno" class="form-check-input" type="checkbox" [(ngModel)]="jalapeno">
<label class="form-check-label" for="jalapeno">Jalapeño (+3,50 zł)</label>
</div>
<p class="fs-5 fw-bold mb-0">Do zapłaty: {{ cena().toFixed(2) }} zł</p>
</div>
Do zapłaty: 38.50 zł
Każde zaznaczenie zmienia kwotę od razu. Z polskim przecinkiem pokazałby ją pipe number albo currency z lekcji 16.
Zatrzymaj się
Wewnątrz computed są klamry i return. Kiedy taki dłuższy zapis jest czytelniejszy niż jedno wyrażenie?
Lista checkboxów z tablicy
Zainteresowania ucznia — opcje pochodzą z tablicy. Zamiast sygnału dla każdej opcji trzymamy jeden sygnał z tablicą zaznaczonych nazw. Tu nie ma ngModel, więc nie trzeba też FormsModule.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly opcje = ['Gry retro', 'Robotyka', 'Druk 3D', 'Grafika', 'Sieci'];
readonly wybrane = signal<string[]>([]);
przelacz(nazwa: string): void {
this.wybrane.update(lista =>
lista.includes(nazwa)
? lista.filter(x => x !== nazwa) // był — usuń
: [...lista, nazwa] // nie było — dodaj
);
}
}
<div class="p-3" style="width: 26rem">
<p class="fw-bold">Co cię interesuje?</p>
@for (o of opcje; track o; let i = $index) {
<div class="form-check">
<input [id]="'opcja' + i" class="form-check-input" type="checkbox"
[checked]="wybrane().includes(o)" (change)="przelacz(o)">
<label class="form-check-label" [for]="'opcja' + i">{{ o }}</label>
</div>
}
<p class="mt-3">Wybrano ({{ wybrane().length }}): {{ wybrane().join(', ') || 'nic' }}</p>
</div>
Wybrano (2): Robotyka, Druk 3D
Lista pokazuje wybrane opcje w kolejności klikania. Każdy checkbox dostaje unikalne id zbudowane z numeru: opcja0, opcja1…
Zatrzymaj się
W przelacz operator ?: zwraca jedną z dwóch nowych tablic. Dlaczego żadna gałąź nie używa push ani splice?
„Zaznacz wszystkie”
Rozbudowujemy przykład 3 o checkbox nadrzędny. Jest zaznaczony, gdy zaznaczone są wszystkie opcje; kliknięcie w niego zaznacza albo czyści całą listę.
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly opcje = ['Gry retro', 'Robotyka', 'Druk 3D', 'Grafika', 'Sieci'];
readonly wybrane = signal<string[]>(['Robotyka']);
readonly wszystkie = computed(() => this.wybrane().length === this.opcje.length);
przelacz(nazwa: string): void {
this.wybrane.update(lista =>
lista.includes(nazwa) ? lista.filter(x => x !== nazwa) : [...lista, nazwa]
);
}
przelaczWszystkie(): void {
this.wybrane.set(this.wszystkie() ? [] : [...this.opcje]);
}
}
<div class="p-3" style="width: 26rem">
<div class="form-check border-bottom pb-2 mb-2">
<input id="wszystkie" class="form-check-input" type="checkbox"
[checked]="wszystkie()" (change)="przelaczWszystkie()">
<label class="form-check-label fw-bold" for="wszystkie">Zaznacz wszystkie</label>
</div>
@for (o of opcje; track o; let i = $index) {
<div class="form-check">
<input [id]="'opcja' + i" class="form-check-input" type="checkbox"
[checked]="wybrane().includes(o)" (change)="przelacz(o)">
<label class="form-check-label" [for]="'opcja' + i">{{ o }}</label>
</div>
}
<p class="mt-3">Wybrano {{ wybrane().length }} z {{ opcje.length }}.</p>
</div>
☑ Zaznacz wszystkie
☑ Gry retro ☑ Robotyka ☑ Druk 3D ☑ Grafika ☑ Sieci
Wybrano 5 z 5.
Zaznacz ręcznie każdą opcję po kolei — przy piątej checkbox nadrzędny zaznacza się sam, bo wszystkie() zmienia się na true.
Zatrzymaj się
[...this.opcje] tworzy kopię tablicy opcji. Co mogłoby się stać, gdybyś przypisał do sygnału samą tablicę this.opcje?
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
type="checkbox" + [(ngModel)] | true/false w sygnale | Sygnał: signal(false). |
form-check, form-check-input, form-check-label | checkbox Bootstrapa | Etykieta z for. |
[checked]="warunek" | zaznaczenie z danych | Bez FormsModule. |
(change)="metoda()" | reakcja na kliknięcie i spację | Lepsze niż (click). |
lista.includes(x) | czy element jest w tablicy | true/false. |
[id]="'opcja' + i", [for] | unikalne id w pętli | let i = $index. |
[...tablica] | kopia tablicy | Z lekcji 10. |
border-bottom pb-2 | linia pod elementem | Bootstrap. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
[(ngModel)]="zgoda" przy signal('') | Do sygnału tekstowego trafia true/false i TypeScript zaczyna się gubić. Checkbox — signal(false). |
<label> bez for | Trzeba trafić w malutki kwadracik. Etykieta z for równym id pola jest klikalna. |
Wszystkie checkboxy z pętli mają id="opcja" | Kliknięcie w dowolną etykietę zaznacza pierwszy checkbox. Doklej $index. |
(click) zamiast (change) | Zwykle działa, ale change reaguje też na zaznaczanie spacją. |
this.wybrane().push(nazwa) | Pułapka z lekcji 10: computed (np. wszystkie) się nie przeliczy. |
| 30 opcji = 30 sygnałów | Kod puchnie i trzeba go zmieniać przy każdej nowej opcji. Opcje z tablicy — jeden sygnał z tablicą wybranych. |
Zadania
Checkbox „Chcę dostawać informacje o turniejach”. Gdy jest zaznaczony, pod nim pojawia się pole na e-mail; gdy odznaczony — pole znika.
Kryterium sukcesu: pole e-mail istnieje tylko przy zaznaczonym checkboxie.
Podpowiedź
@if (newsletter()) wokół pola.
Checkbox „Tryb nocny”, który przełącza wygląd karty: ciemne tło i jasny tekst przy zaznaczeniu, zwykły wygląd bez zaznaczenia. Dodaj drugi checkbox „Duża czcionka”, który niezależnie powiększa tekst karty.
Kryterium sukcesu: oba checkboxy działają niezależnie, a każda z czterech kombinacji daje inny wygląd.
Podpowiedź
[class.text-bg-dark]="nocny()" i [class.fs-4]="duza()" na elemencie karty.
Cena bazowa 2500 zł i pięć opcji z cenami (np. dodatkowy RAM, większy dysk, karta Wi-Fi) zapisanych w tablicy obiektów { id, nazwa, cena } i wyświetlonych pętlą. Pokaż sumę w złotych i listę wybranych opcji.
Kryterium sukcesu: suma zgadza się z zaznaczonymi opcjami; dopisanie szóstej opcji do tablicy nie wymaga zmian w szablonie.
Podpowiedź
W sygnale wybrane trzymaj id. Sumę policz w computed: pętla for…of po opcjach i if (this.wybrane().includes(o.id)).
Trzy checkboxy zgód: regulamin (wymagana), przetwarzanie danych (wymagana), newsletter (opcjonalna). Przycisk „Zarejestruj” aktywny tylko przy obu wymaganych. Pod przyciskiem lista brakujących zgód, np. „Brakuje: regulamin”.
Kryterium sukcesu: przycisk aktywuje się dopiero po zaznaczeniu obu wymaganych, a lista braków zmienia się na bieżąco.
Podpowiedź
computed buduje tablicę braków; przycisk: [disabled]="braki().length > 0".
Lista ośmiu uczniów (tablica) z checkboxem „obecny” przy każdym, przyciski „Wszyscy obecni” i „Nikt” oraz licznik „obecnych: X / 8”. Nieobecni mają nazwisko na szaro (text-secondary), a gdy obecnych jest mniej niż połowa — licznik jest czerwony.
Kryterium sukcesu: wszystkie elementy reagują na każde kliknięcie, a przyciski zaznaczają i czyszczą całą listę.
Podpowiedź
Przykład 4 plus [class.text-secondary]="!obecni().includes(u)" na etykiecie.
Ankieta „Wybierz maksymalnie 3 ulubione gry” z ośmioma grami z tablicy. Po wybraniu trzech pozostałe checkboxy stają się nieaktywne (te zaznaczone można dalej odznaczyć). Pod listą: „Zostało wyborów: X”, a po wybraniu trzech — przycisk „Wyślij” z podsumowaniem w ramce alert po kliknięciu.
Kryterium sukcesu: nie da się zaznaczyć czwartej gry, zaznaczone da się odznaczyć, a licznik i przycisk zgadzają się z liczbą wyborów.
Podpowiedź
[disabled]="!wybrane().includes(g) && wybrane().length === 3" na każdym checkboxie.
Co trzeba zapamiętać
- Checkbox z
[(ngModel)]zapisuje w sygnaletruealbofalse. - Wartość checkboxa od razu użyjesz w
[disabled],@if,[class.x]albocomputed. - Listę checkboxów z tablicy obsługujesz jednym sygnałem z tablicą wybranych oraz
[checked]i(change). - Każdy checkbox w pętli potrzebuje unikalnego
id, żeby etykieta działała. - Tablicę wybranych zmieniasz nową tablicą — nigdy
push.
Sprawdź się: masz 30 opcji w tablicy. Dlaczego 30 osobnych sygnałów to zły pomysł i jak wygląda lepsze rozwiązanie?
Dokumentacja: Bootstrap — checkboxy i radio.