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

Klasy i style zależne od danych: [class], [style], NgClass, NgStyle

W lekcji 06 włączałeś pojedynczą klasę zapisem [class.nazwa]. Teraz dane siedzą w sygnałach i pętlach, a klas do przełączania jest kilka naraz. Poznasz zapis obiektowy [class]="{ … }", style liczone z sygnałów oraz starsze dyrektywy NgClass i NgStyle, które spotkasz w arkuszach egzaminacyjnych.

Angular 22 [class.x] [class]=”{…}” [style.x] NgClass NgStyle 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Przełączysz wygląd jednego elementu zamiast pisać dwa elementy w @if.
  • Sterujesz kilkoma klasami naraz obiektem w [class]="{ 'klasa': warunek }".
  • Ustawisz styl z liczby w sygnale z jednostką (.px, .%) i bez niej (transform).
  • Pokolorujesz wiersze tabeli zależnie od danych w pętli @for.
  • Zapiszesz to samo dyrektywami NgClass i NgStyle i zaimportujesz je z @angular/common.
  • Wybierzesz między klasą Bootstrapa a stylem CSS.

Przygotowanie: lekcja 06 (wiązanie właściwości) i lekcje 09–14 — dane trzymasz w sygnałach i umiesz używać @for. Przewidywany czas: 45 min z zadaniami.

TEORIA

Ten sam element, inny wygląd

Z @if mógłbyś napisać dwie etykiety — zieloną i czerwoną — i pokazywać jedną z nich. Przy dwóch kolorach to jeszcze ujdzie. Przy pięciu stanach i trzech miejscach na stronie kod robi się nie do czytania, a przecież element jest ten sam — zmienia się tylko jego klasa.

ZapisKiedy
[class.nazwa]="warunek"jedna klasa (lekcja 06)
[class]="{ 'a': warunekA, 'b': warunekB }"kilka klas, każda z własnym warunkiem
[style.font-size.px]="rozmiar()"jeden styl z liczby z jednostką
[style.transform]="'rotate(' + kat() + 'deg)'"styl, który trzeba zbudować z tekstu
[ngClass]="{ … }", [ngStyle]="{ … }"starszy zapis tego samego; wymaga importu

W obiekcie nazwy klas piszesz w apostrofach — 'text-bg-success' bez apostrofów Angular odczytałby jako działanie text - bg - success. Zwykły atrybut class="badge" może stać obok wiązania — Angular łączy oba.

Klasa czy styl?

W Bootstrapie kolory to klasy: text-bg-success, text-danger, btn-primary, table-warning. Dlatego najczęściej przełączasz klasy, a styl ustawiasz tylko wtedy, gdy wartość jest liczbą (szerokość, rozmiar, kąt) albo kolorem wybranym przez użytkownika.

PRZYKŁAD 1

Przełącznik bez drugiego przycisku

Przycisk „Ulubione” zmienia wygląd po kliknięciu. Klasa btn jest zawsze, a btn-warning i btn-outline-warning włączają się na zmianę — jeden element zamiast dwóch w @if.

src/app/nowa-15-1/demo.ts — pełny plik
import { Component, signal } from '@angular/core';

@Component({
    selector: 'app-demo',
    imports: [],
    templateUrl: './demo.html'
})
export class Demo {
    readonly ulubione = signal(false);
}
src/app/nowa-15-1/demo.html — pełny plik
<div class="card m-3 p-3" style="width: 22rem">
  <h5 [class.text-warning]="ulubione()">Turniej retro</h5>
  <button class="btn"
          [class.btn-warning]="ulubione()"
          [class.btn-outline-warning]="!ulubione()"
          (click)="ulubione.set(!ulubione())">
    ★ {{ ulubione() ? 'W ulubionych' : 'Dodaj do ulubionych' }}
  </button>
</div>
wynik po kliknięciu
Turniej retro                (tytuł żółty)
[★ W ulubionych]             (przycisk wypełniony na żółto)

Zajrzyj w F12: klasa btn jest stale na miejscu, zmieniają się tylko dwie pozostałe.

Zatrzymaj się

Ile elementów <button> byłoby w szablonie, gdybyś zrobił to przez @if/@else? Który zapis łatwiej zmienić, gdy dojdzie trzeci stan?

PRZYKŁAD 2

Kilka klas naraz — obiekt w [class]

Wolne miejsca kolorujemy na trzy sposoby: zielono (dużo), żółto (mało), czerwono (brak). Obiekt w [class] zawiera trzy klasy i trzy warunki.

src/app/nowa-15-2/demo.ts — pełny plik
import { Component, signal } from '@angular/core';

@Component({
    selector: 'app-demo',
    imports: [],
    templateUrl: './demo.html'
})
export class Demo {
    readonly wolne = signal(6);

    zapisz(): void {
        this.wolne.update(n => (n > 0 ? n - 1 : 0));
    }
}
src/app/nowa-15-2/demo.html — pełny plik
<div class="p-3">
  <p class="fs-5">
    Wolne miejsca:
    <span class="badge"
          [class]="{
            'text-bg-success': wolne() > 3,
            'text-bg-warning': wolne() > 0 && wolne() <= 3,
            'text-bg-danger': wolne() === 0
          }">
      {{ wolne() }}
    </span>
  </p>
  <button class="btn btn-primary" (click)="zapisz()">Zapisz osobę</button>
</div>
wynik kolejnych kliknięć
6, 5, 4  → zielona etykieta
3, 2, 1  → żółta
0        → czerwona

Klasa badge zapisana zwykłym atrybutem zostaje na stałe. Obiekt możesz rozpisać na kilka wierszy — czyta się go wtedy jak tabelkę „klasa: kiedy”.

Zatrzymaj się

Usuń apostrofy wokół text-bg-success i przeczytaj błąd. Co Angular próbował policzyć?

PRZYKŁAD 3

Styl z liczby: [style.nazwa]

Podgląd plakatu: przyciskami zmieniasz wielkość tytułu w pikselach i obrót w stopniach, a trzeci przycisk zmienia kolor. Wartości są liczbami, więc tu lepiej sprawdza się styl niż klasa.

src/app/nowa-15-3/demo.ts — pełny plik
import { Component, signal } from '@angular/core';

@Component({
    selector: 'app-demo',
    imports: [],
    templateUrl: './demo.html'
})
export class Demo {
    readonly rozmiar = signal(24);
    readonly obrot = signal(0);
    readonly kolor = signal('#0d6efd');

    wiekszy(): void {
        this.rozmiar.update(r => Math.min(r + 4, 64));
    }

    mniejszy(): void {
        this.rozmiar.update(r => Math.max(r - 4, 12));
    }

    obroc(): void {
        this.obrot.update(o => (o + 15) % 360);
    }
}
src/app/nowa-15-3/demo.html — pełny plik
<div class="p-3">
  <div class="mb-3">
    <button class="btn btn-outline-secondary me-1" (click)="mniejszy()">A−</button>
    <button class="btn btn-outline-secondary me-1" (click)="wiekszy()">A+</button>
    <button class="btn btn-outline-secondary me-1" (click)="obroc()">⟳ 15°</button>
    <button class="btn btn-outline-danger" (click)="kolor.set('#dc3545')">Czerwony</button>
  </div>

  <div class="border p-4 text-center" style="width: 26rem">
    <p class="fw-bold mb-0"
       [style.font-size.px]="rozmiar()"
       [style.color]="kolor()"
       [style.transform]="'rotate(' + obrot() + 'deg)'">
      NOC ROBOTÓW
    </p>
  </div>
  <p class="mt-2 text-secondary">rozmiar: {{ rozmiar() }} px, obrót: {{ obrot() }}°</p>
</div>
wynik po A+ i dwóch obrotach
NOC ROBOTÓW   (28 px, obrócony o 30°)
rozmiar: 28 px, obrót: 30°

Tytuł rośnie i maleje co 4 px (od 12 do 64). [style.font-size.px] sam dopisuje px. Przy transform takiej końcówki nie ma, więc cały napis rotate(30deg) sklejamy ręcznie.

Zatrzymaj się

(o + 15) % 360 — co robi tu operator % i co by się działo bez niego po 30 kliknięciach?

PRZYKŁAD 4

NgClass i NgStyle — ten sam efekt, starszy zapis

Tabela wyników sprawdzianu. Wiersz ucznia, który zdał, jest zielony, a który nie zdał — czerwony; wynik od 90% jest pogrubiony. Tym razem zapis przez NgClass i NgStyle, więc dyrektywy trzeba zaimportować.

src/app/nowa-15-4/demo.ts — pełny plik
import { Component, signal } from '@angular/core';
import { NgClass, NgStyle } from '@angular/common';

@Component({
    selector: 'app-demo',
    imports: [NgClass, NgStyle],
    templateUrl: './demo.html'
})
export class Demo {
    readonly progZdania = 50;
    readonly wyniki = signal([
        { uczen: 'Ala', procent: 92 },
        { uczen: 'Bartek', procent: 47 },
        { uczen: 'Celina', procent: 68 },
        { uczen: 'Darek', procent: 31 },
    ]);
}
src/app/nowa-15-4/demo.html — pełny plik
<div class="p-3">
  <table class="table" style="width: 26rem">
    <thead><tr><th>Uczeń</th><th>Wynik</th></tr></thead>
    <tbody>
    @for (w of wyniki(); track w.uczen) {
      <tr [ngClass]="{
            'table-success': w.procent >= progZdania,
            'table-danger': w.procent < progZdania
          }">
        <td>{{ w.uczen }}</td>
        <td [ngStyle]="{ 'font-weight': w.procent >= 90 ? 'bold' : 'normal' }">{{ w.procent }}%</td>
      </tr>
    }
    </tbody>
  </table>
</div>
wynik w przeglądarce
Ala      92%   (zielony wiersz, pogrubione)
Bartek   47%   (czerwony)
Celina   68%   (zielony)
Darek    31%   (czerwony)

Ten sam efekt bez importów: [class]="{ 'table-success': … }" i [style.font-weight]="w.procent >= 90 ? 'bold' : 'normal'". Oba zapisy są poprawne — wybierz jeden i trzymaj się go w całym projekcie.

Zatrzymaj się

Usuń NgClass z tablicy imports i przeczytaj błąd w terminalu. Potem przywróć i zmień progZdania na 70. Które wiersze zmienią kolor? Czy trzeba było cokolwiek zmieniać w szablonie?

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
[class.nazwa]="warunek"jedna klasaPozostałe zostają.
[class]="{ 'a': x, 'b': y }"kilka klas z warunkamiNazwy w apostrofach.
[style.wlasciwosc.px], .%, .remstyl z jednostkąWartość — liczba.
[style.color]="kolor()"styl bez jednostkiWartość — tekst CSS.
NgClass, [ngClass]starszy zapis klasImport z @angular/common.
NgStyle, [ngStyle]starszy zapis stylówImport z @angular/common.
table-success, table-dangerkolor wiersza tabeliBootstrap.
%reszta z dzieleniaLicznik „w kółko”.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
[class.btn-warning]="ulubione"Ostrzeżenie NG8109, a klasa jest włączona zawsze — sam sygnał jest „prawdziwy”. Dopisz ().
[class]="{ text-bg-success: … }"NG5002 i TS2339 (Property 'text’ does not exist). Nazwy z myślnikiem muszą być w apostrofach.
[style.width]="procent()"Pasek nie zmienia szerokości — brak jednostki. Napisz [style.width.%].
[ngClass] bez importu NgClassNG8002: Can’t bind to 'ngClass’ since it isn’t a known property. Dodaj NgClass do imports.
Ta sama klasa w class="…" i w wiązaniu z warunkiemWiązanie wygrywa: przy fałszywym warunku zdejmie klasę także tę wpisaną na stałe. Każdą klasę trzymaj w jednym miejscu — stałe w class, zmienne w wiązaniu.
[style.transform]="rotate(obrot()deg)"Błąd składni — to nie jest tekst. Sklej go: 'rotate(' + obrot() + 'deg)'.
ZADANIA

Zadania

ZAD 1Włącz / wyłącz★☆☆

Zrób przycisk-przełącznik, który jest zielony z napisem „Włączone” albo szary z napisem „Wyłączone”. W szablonie ma być jeden element <button>.

Kryterium sukcesu: jeden przycisk w szablonie, kolor i napis zmieniają się po kliknięciu.

Podpowiedź

Dwie klasy: btn-success i btn-secondary, z przeciwnymi warunkami.

ZAD 2Lista obecności★☆☆

Tablica uczniów { id, imie, obecny } w tabeli. Kliknięcie w wiersz przełącza obecność. Obecni mają zielony wiersz, nieobecni — czerwony i przekreślone imię. Pod tabelą liczba obecnych.

Kryterium sukcesu: kliknięcie zmienia kolor jednego wiersza, a liczba obecnych zgadza się z kolorami.

Podpowiedź

(click) na <tr>, zmiana przez map z kopią obiektu. Przekreślenie: klasa text-decoration-line-through. Liczba obecnych — computed z filter(...).length.

ZAD 3Siła hasła★★☆

Pole hasła (type="password") i pasek postępu pod nim. Szerokość paska to długość hasła × 10% (maksymalnie 100%). Kolor: czerwony poniżej 6 znaków, żółty od 6 do 9, zielony od 10. Pod paskiem napis „słabe / średnie / silne”.

Kryterium sukcesu: pasek rośnie przy każdym wpisanym znaku i zmienia kolor oraz napis na progach 6 i 10.

Podpowiedź

Długość hasła zapisuj w sygnale w zdarzeniu (input) z lekcji 08. Szerokość i poziom to dwa computed; kolory paska: bg-danger, bg-warning, bg-success w obiekcie [class].

ZAD 4Termometr★★☆

Sygnał z temperaturą od −20 do 40 °C, zmienianą przyciskami ±5. Pokaż pionowy „termometr”: div o stałej wysokości 200 px z wypełnieniem od dołu, którego wysokość w procentach zależy od temperatury. Kolor wypełnienia: niebieski poniżej 0, zielony 0–25, czerwony powyżej 25.

Kryterium sukcesu: wypełnienie rośnie i maleje z temperaturą, nie wychodzi poza ramkę i zmienia kolor na progach.

Podpowiedź

Procent: (temp + 20) / 60 * 100 w computed. Wypełnienie od dołu: rodzic z d-flex flex-column justify-content-end, dziecko z [style.height.%].

ZAD 5Kolory z NgClass i bez★★★

Zrób tabelę ocen klasy (5 uczniów, oceny 1–6) z kolumną „Ocena”. Kolor komórki: 1 — czerwony, 2 — żółty, 3–4 — bez koloru, 5–6 — zielony. Zrób to dwa razy w jednym szablonie: raz z [ngClass], raz z obiektem w [class]. Na koniec usuń z imports to, co już niepotrzebne, gdy zostawisz tylko drugą wersję.

Kryterium sukcesu: obie tabele wyglądają identycznie, a po usunięciu pierwszej terminal nie pokazuje ostrzeżenia NG8113.

Podpowiedź

Klasy komórek: table-danger, table-warning, table-success.

ZAD 6Plansza do kółka i krzyżyka★★★

Siatka 3×3 zbudowana pętlą @for z tablicy dziewięciu pól. Kliknięcie w puste pole stawia na zmianę X albo O. Pola z X mają niebieskie tło, z O — czerwone, puste — jasnoszare. Nad planszą napis, czyj jest ruch.

Kryterium sukcesu: gracze stawiają znaki na zmianę, zajętego pola nie da się nadpisać, a kolory zgadzają się ze znakami.

Podpowiedź

Tablica w sygnale: signal<string[]>(Array(9).fill('')). Zmiana jednego pola: update(p => p.map((v, i) => i === nr ? znak : v)). Siatka: row i col-4 z ratio ratio-1x1 albo stała wysokość.

PODSUMOWANIE

Co trzeba zapamiętać

  • Zamiast dwóch elementów w @if często wystarczy jeden element z przełączaną klasą.
  • [class.nazwa]="warunek" włącza jedną klasę; pozostałe klasy elementu zostają.
  • [class]="{ 'klasa': warunek, … }" steruje kilkoma klasami naraz; nazwy w apostrofach.
  • [style.wlasciwosc.jednostka] ustawia styl z liczby; bez jednostki — tekst CSS.
  • NgClass i NgStyle robią to samo starszym zapisem i wymagają importu z @angular/common.

Sprawdź się: masz etykietę, która ma być zielona, żółta albo czerwona. Kiedy wybierzesz trzy [class.nazwa], a kiedy jeden [class] z obiektem?

Dokumentacja: angular.dev — wiązanie klas i stylów.

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