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.
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
NgClassiNgStylei 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.
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.
| Zapis | Kiedy |
|---|---|
[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.
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.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-demo',
imports: [],
templateUrl: './demo.html'
})
export class Demo {
readonly ulubione = signal(false);
}
<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>
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?
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.
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));
}
}
<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>
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ć?
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.
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);
}
}
<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>
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?
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ć.
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 },
]);
}
<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>
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?
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
[class.nazwa]="warunek" | jedna klasa | Pozostałe zostają. |
[class]="{ 'a': x, 'b': y }" | kilka klas z warunkami | Nazwy w apostrofach. |
[style.wlasciwosc.px], .%, .rem | styl z jednostką | Wartość — liczba. |
[style.color]="kolor()" | styl bez jednostki | Wartość — tekst CSS. |
NgClass, [ngClass] | starszy zapis klas | Import z @angular/common. |
NgStyle, [ngStyle] | starszy zapis stylów | Import z @angular/common. |
table-success, table-danger | kolor wiersza tabeli | Bootstrap. |
% | reszta z dzielenia | Licznik „w kółko”. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
[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 NgClass | NG8002: 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 warunkiem | Wią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
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.
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.
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].
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.%].
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.
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ść.
Co trzeba zapamiętać
- Zamiast dwóch elementów w
@ifczę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.NgClassiNgStylerobią 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.