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

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.

Angular 22 checkbox form-check boolean [checked] (change) lista z tablicy 45 min
CEL LEKCJI

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) zamiast ngModel tam, 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.

TEORIA

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.

checkbox w Bootstrapie
<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>
SytuacjaRozwiązanie
jeden checkboxreadonly zgoda = signal(false); i [(ngModel)]="zgoda"
kilka stałych opcjiosobny sygnał boolean dla każdej
opcje z tablicyjeden 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.

PRZYKŁAD 1

Zgoda blokująca przycisk

Klasyka: przycisk „Zapisz się” jest nieaktywny, dopóki użytkownik nie zaakceptuje regulaminu.

src/app/nowa-20-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 regulamin = signal(false);
    readonly zapisano = signal(false);
}
src/app/nowa-20-1/demo.html — pełny plik
<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>
wynik po zaznaczeniu i kliknięciu
☑ 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ł?

PRZYKŁAD 2

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.

src/app/nowa-20-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 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;
    });
}
src/app/nowa-20-2/demo.html — pełny plik
<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>
wynik przy wszystkich dodatkach
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?

PRZYKŁAD 3

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.

src/app/nowa-20-3/demo.ts — pełny plik
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
        );
    }
}
src/app/nowa-20-3/demo.html — pełny plik
<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>
wynik po zaznaczeniu Robotyki i Druku 3D
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?

PRZYKŁAD 4

„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ę.

src/app/nowa-20-4/demo.ts — pełny plik
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]);
    }
}
src/app/nowa-20-4/demo.html — pełny plik
<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>
wynik po kliknięciu „Zaznacz wszystkie”
☑ 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?

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
type="checkbox" + [(ngModel)]true/false w sygnaleSygnał: signal(false).
form-check, form-check-input, form-check-labelcheckbox BootstrapaEtykieta z for.
[checked]="warunek"zaznaczenie z danychBez FormsModule.
(change)="metoda()"reakcja na kliknięcie i spacjęLepsze niż (click).
lista.includes(x)czy element jest w tablicytrue/false.
[id]="'opcja' + i", [for]unikalne id w pętlilet i = $index.
[...tablica]kopia tablicyZ lekcji 10.
border-bottom pb-2linia pod elementemBootstrap.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
[(ngModel)]="zgoda" przy signal('')Do sygnału tekstowego trafia true/false i TypeScript zaczyna się gubić. Checkbox — signal(false).
<label> bez forTrzeba 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łówKod puchnie i trzeba go zmieniać przy każdej nowej opcji. Opcje z tablicy — jeden sygnał z tablicą wybranych.
ZADANIA

Zadania

ZAD 1Newsletter★☆☆

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.

ZAD 2Tryb nocny★☆☆

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.

ZAD 3Konfigurator komputera★★☆

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

ZAD 4Zgody w formularzu★★☆

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

ZAD 5Lista obecności★★★

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.

ZAD 6Ankieta z limitem★★★

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.

PODSUMOWANIE

Co trzeba zapamiętać

  • Checkbox z [(ngModel)] zapisuje w sygnale true albo false.
  • Wartość checkboxa od razu użyjesz w [disabled], @if, [class.x] albo computed.
  • 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.

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