Kurs Angular · Moduł 2: Komponenty
Data binding - jak klasa rozmawia z szablonem
W tej lekcji5
Gdy samuraj oberwie i jego zdrowie spadnie ze 100 do 70, ekran musi pokazać 70, a nie starą wartość. Gdy uczeń kliknie przycisk ataku, klasa musi się o tym dowiedzieć, żeby odjąć przeciwnikowi punkty życia.
Bez pomocy frameworka musiałbyś robić to ręcznie: szukać elementów strony, przepisywać ich tekst po każdej zmianie danych i podpinać nasłuchiwanie kliknięć. Każdy zapomniany krok oznacza ekran, który kłamie. Angular robi to za Ciebie dzięki mechanizmowi data binding, czyli wiązaniu danych. W szablonie raz opisujesz, co z czym jest połączone, a Angular pilnuje, żeby klasa i widok zawsze mówiły to samo.
Na dawnych polach bitew dowódca porozumiewał się z oddziałami chorągwiami i bębnami. Chorągiew niosła rozkaz od dowódcy do żołnierzy, a bęben przekazywał meldunek w drugą stronę. Wiązanie danych działa podobnie i ma cztery odmiany:
| Zapis | Kierunek | Przykład |
|---|---|---|
{{ wyrażenie }} | klasa → szablon (tekst) | {{ name }} |
[właściwość]="wyrażenie" | klasa → szablon (właściwość elementu) | [disabled]="isDefeated" |
(zdarzenie)="instrukcja" | szablon → klasa | (click)="attack()" |
[(ngModel)]="pole" | w obie strony | [(ngModel)]="samuraiName" |
Omówimy je po kolei, zaczynając od najprostszej.
Interpolacja - napis na chorągwi
Interpolacja wstawia wartość z klasy do tekstu strony. Wyrażenie zapisujesz w podwójnych klamrach, a Angular oblicza je i wyświetla wynik w tym miejscu:
<h2>{{ name }}</h2>
<p>Poziom: {{ level }}</p>
<p>Siła ciosu: {{ level * 10 }}</p>
<p>{{ name.toUpperCase() }}</p>W klamrach może stać nazwa właściwości, działanie matematyczne albo wywołanie metody. Musi to być jednak wyrażenie, czyli coś, co zwraca wartość, a nie instrukcja. Zapis {{ level = 5 }} się nie skompiluje, bo Angular zabrania przypisań w interpolacji. Klamry służą do pokazywania danych, a nie do ich zmieniania.
Wynik interpolacji zawsze trafia na stronę jako tekst. Jeśli w polu name znajdzie się znacznik <b>, uczeń zobaczy go dosłownie zamiast pogrubienia. To celowe zabezpieczenie: dane wpisane przez użytkownika nie mogą w ten sposób wstrzyknąć do strony własnego HTML-a. Gdy wartość w klasie się zmieni, Angular sam podmieni tekst na ekranie.
Property binding - ustawianie właściwości
Interpolacja wstawia tekst, ale czasem trzeba ustawić właściwość elementu: adres obrazka, stan przycisku albo klasę CSS. Do tego służy property binding, czyli nazwa właściwości w nawiasach kwadratowych i wyrażenie po znaku równości:
<img [src]="bannerUrl" [alt]="name">
<button [disabled]="isDefeated">Atakuj!</button>
<li [class.active]="isActive">Musashi</li>Nawiasy kwadratowe mówią Angularowi: to, co stoi w cudzysłowie, oblicz jako wyrażenie. Zapis [src]="bannerUrl" ustawia właściwość src obrazka na wartość pola bannerUrl z klasy. Zapis [disabled]="isDefeated" wyłącza przycisk, dopóki isDefeated ma wartość true. Specjalna forma [class.active] dodaje klasę CSS active, gdy wyrażenie jest prawdziwe, i usuwa ją, gdy jest fałszywe.
Bez nawiasów prawa strona jest zwykłym napisem. Właśnie tu czeka pułapka, w którą wpada wielu początkujących, więc porównaj te dwa przyciski:
<!-- przycisk NADAL wyłączony: atrybut disabled istnieje -->
<button disabled="false">Atakuj!</button>
<!-- przycisk włączony: właściwość dostaje wartość false -->
<button [disabled]="false">Atakuj!</button>W pierwszej linii false to tylko tekst atrybutu, a dla przeglądarki liczy się sama obecność atrybutu disabled, więc przycisk zostaje wyłączony. W drugiej linii Angular oblicza wyrażenie false i ustawia właściwość elementu na prawdziwą wartość logiczną, więc przycisk działa. Dlatego wszystko, co ma się zmieniać razem z danymi, wiąż w nawiasach kwadratowych.
Przy tekście nawiasy nie są potrzebne. Zapisy [title]="'Musashi'" i title="Musashi" dają identyczny efekt, bo w pierwszym wyrażeniem jest po prostu napis w apostrofach. Nawiasy są niezbędne wtedy, gdy wartość pochodzi z klasy albo nie jest tekstem, na przykład przy liczbie [level]="10", którą w ostatniej lekcji modułu przekażesz komponentowi dziecka.
Event binding - meldunek z pola bitwy
Property binding przekazuje dane z klasy do szablonu. Event binding działa w drugą stronę: gdy na stronie coś się wydarzy, na przykład kliknięcie, Angular wywoła wskazaną metodę klasy. Nazwę zdarzenia zapisujesz w nawiasach okrągłych:
<button (click)="attack()">Atakuj!</button>
<input (input)="onNameInput($event)">Pierwsza linia wywołuje metodę attack() przy każdym kliknięciu. Druga reaguje na każdą zmianę tekstu w polu i przekazuje do metody specjalną zmienną $event. Przy zdarzeniach przeglądarki $event to obiekt zdarzenia, z którego odczytasz na przykład element, w którym zdarzenie wystąpiło:
export class SamuraiComponent {
name = 'Musashi';
onNameInput(event: Event): void {
const field = event.target as HTMLInputElement;
this.name = field.value;
}
}Właściwość event.target wskazuje pole tekstowe, a zapis as HTMLInputElement mówi TypeScriptowi, że to właśnie element input, więc można odczytać jego value. Po przypisaniu nowego imienia Angular odświeży wszystkie miejsca w szablonie, które pokazują name.
Uważaj na trzy zapisy, które wyglądają podobnie, ale nie są event bindingiem Angulara. [click]="attack()" to property binding: Angular potraktuje click jak właściwość elementu, a nie jak zdarzenie. onclick="attack()" to zwykły atrybut HTML, który nie widzi metod Twojej klasy. @click="attack()" to składnia z Vue, której Angular nie rozumie. W Angularze zdarzenie zawsze zapisujesz w nawiasach okrągłych.
Two-way binding - rozmowa w obie strony
Pole formularza potrzebuje obu kierunków naraz. Klasa ma pokazać w polu aktualne imię samuraja, a gdy uczeń wpisze nowe, klasa ma je od razu poznać. Łączy to two-way binding, zapisywany jako nawiasy okrągłe wewnątrz kwadratowych: [(ngModel)]. Dyrektywa ngModel pochodzi z FormsModule, więc komponent musi go mieć na liście imports:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-samurai-form',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="samuraiName">
<p>Imię samuraja: {{ samuraiName }}</p>
`
})
export class SamuraiFormComponent {
samuraiName = '';
}Każda litera wpisana w pole trafia od razu do właściwości samuraiName, a interpolacja w akapicie pokazuje nową wartość. Działa to też w drugą stronę: jeśli metoda klasy przypisze do samuraiName nowe imię, pole tekstowe samo je wyświetli. Bez FormsModule na liście imports kompilator zgłosi, że nie może powiązać ngModel, bo element input nie ma takiej właściwości.
Programiści nazywają ten zapis bananem w pudełku: banan, czyli nawiasy okrągłe, leży w pudełku, czyli w nawiasach kwadratowych. To skrót, który Angular rozwija do dwóch wiązań, które już znasz:
<input [ngModel]="samuraiName" (ngModelChange)="samuraiName = $event">Property binding [ngModel] wpisuje wartość z klasy do pola, a event binding (ngModelChange) przy każdej zmianie przypisuje nową wartość z $event z powrotem do klasy. Oba zapisy działają identycznie, ale skrót czyta się szybciej i trudniej w nim o pomyłkę. Kolejność nawiasów ma znaczenie: odwrócony zapis ([ngModel]) nie jest two-way bindingiem, a kompilator Angulara ostrzeże, że nawiasy okrągłe mają stać w środku.
Pełny przykład - panel treningowy
Zbierzmy wszystkie cztery wiązania w jednym komponencie. Panel treningowy pozwala wpisać liczbę powtórzeń, wykonać trening i śledzić sumę wszystkich powtórzeń:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-training',
standalone: true,
imports: [FormsModule],
template: `
<div class="training-panel">
<h2>{{ trainingName }}</h2>
<input [(ngModel)]="reps" type="number">
<button
[disabled]="reps <= 0"
(click)="doTraining()">
Trenuj!
</button>
<p>Wykonane powtórzenia: {{ totalReps }}</p>
</div>
`
})
export class TrainingComponent {
trainingName = 'Trening miecza';
reps = 10;
totalReps = 0;
doTraining(): void {
this.totalReps += this.reps;
}
}Prześledź ten kod krok po kroku. Interpolacja {{ trainingName }} pokazuje nazwę treningu w nagłówku. Two-way binding [(ngModel)]="reps" łączy pole liczbowe z właściwością reps, więc każda wpisana liczba od razu trafia do klasy. Property binding [disabled]="reps <= 0" wyłącza przycisk, gdy liczba powtórzeń jest zerem albo liczbą ujemną. Event binding (click)="doTraining()" wywołuje metodę, która dodaje reps do totalReps, a interpolacja w ostatnim akapicie pokazuje nową sumę.
Zwróć uwagę, czego ta klasa nie robi: nie szuka elementów na stronie i nie przepisuje tekstu akapitu. Zmienia wyłącznie swoje zwykłe właściwości, a resztę załatwiają wiązania. Moja rada: trzymaj logikę w metodach klasy, a w szablonie tylko ją wywołuj, bo długie obliczenia wpisane w cudzysłów szablonu szybko stają się nieczytelne.
W ostatniej lekcji tego modułu przekażesz przez property binding dane do komponentu dziecka, a w szóstym module zbudujesz ten sam panel na sygnałach.
Sala treningowa poniżej to ten sam komponent co w pierwszej lekcji. Znajdź w nim sekcję Data Binding Demo: interpolację imienia i klanu samuraja, property binding [value] paska zdrowia, event binding przycisków Atakuj! i Lecz oraz two-way binding pola z imieniem.
Kod do tej lekcji: src/app/app.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
interface Samurai {
id: number;
name: string;
clan: string;
level: number;
health: number;
}
@Component({
selector: 'app-root',
standalone: true,
imports: [FormsModule],
template: `
<div class="dojo-training">
<header>
<h1>Sala treningowa Dojo</h1>
<p>Moduł 2: Komponenty Angular</p>
</header>
<section class="training-section">
<h2>Data Binding Demo</h2>
<!-- Interpolacja -->
<div class="demo-box">
<h3 ngNonBindable>Interpolacja {{ }}</h3>
<p>Imię: {{ samurai.name }}</p>
<p>Klan: {{ samurai.clan }}</p>
</div>
<!-- Property Binding -->
<div class="demo-box">
<h3>Property Binding [ ]</h3>
<progress [value]="samurai.health" max="100"></progress>
<p>Zdrowie: {{ samurai.health }}%</p>
</div>
<!-- Event Binding -->
<div class="demo-box">
<h3>Event Binding ( )</h3>
<button (click)="attack()">Atakuj!</button>
<button (click)="heal()">Lecz</button>
</div>
<!-- Two-way Binding -->
<div class="demo-box">
<h3>Two-way Binding [( )]</h3>
<input [(ngModel)]="samurai.name" placeholder="Imię samuraja">
<p>Aktualne imię: {{ samurai.name }}</p>
</div>
</section>
<section class="control-flow-section">
<h2>Control Flow Demo</h2>
<!-- @if -->
<div class="demo-box">
<h3>@if / @else</h3>
@if (samurai.health > 50) {
<p class="status good">Samuraj w dobrej formie!</p>
} @else if (samurai.health > 20) {
<p class="status warning">Samuraj osłabiony</p>
} @else {
<p class="status danger">Samuraj w krytycznym stanie!</p>
}
</div>
<!-- @for -->
<div class="demo-box">
<h3>@for</h3>
<ul>
@for (s of samuraiList; track s.id) {
<li [class.selected]="s.id === selectedId" (click)="selectSamurai(s.id)">
{{ s.name }} (Lvl {{ s.level }})
</li>
} @empty {
<li>Brak samurajów</li>
}
</ul>
</div>
<!-- @switch -->
<div class="demo-box">
<h3>@switch</h3>
@switch (getRank(samurai.level)) {
@case ('master') {
<span class="badge master">Mistrz</span>
}
@case ('advanced') {
<span class="badge advanced">Zaawansowany</span>
}
@default {
<span class="badge beginner">Początkujący</span>
}
}
</div>
</section>
</div>
`,
styles: [`
.dojo-training {
font-family: 'Segoe UI', sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 20px;
background: #1a1a2e;
color: #eee;
}
header {
text-align: center;
border-bottom: 2px solid #c9a227;
padding-bottom: 20px;
margin-bottom: 20px;
}
h1 { color: #c9a227; }
h2 { color: #c9a227; margin-top: 30px; }
h3 { color: #888; font-size: 0.9rem; margin-bottom: 10px; }
.demo-box {
background: rgba(255,255,255,0.05);
border: 1px solid #333;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
button {
background: #c9a227;
color: #1a1a2e;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 4px;
cursor: pointer;
}
button:hover { opacity: 0.9; }
input {
padding: 10px;
border: 1px solid #c9a227;
border-radius: 4px;
background: #16213e;
color: #eee;
width: 200px;
}
progress {
width: 100%;
height: 20px;
}
.status { padding: 10px; border-radius: 4px; }
.status.good { background: #2d6a4f; }
.status.warning { background: #b45309; }
.status.danger { background: #991b1b; }
ul { list-style: none; padding: 0; }
li {
padding: 10px;
margin: 5px 0;
background: #16213e;
border-radius: 4px;
cursor: pointer;
}
li:hover { background: #1e3a5f; }
li.selected { border: 2px solid #c9a227; }
.badge {
padding: 5px 15px;
border-radius: 20px;
font-weight: bold;
}
.badge.master { background: #c9a227; color: #1a1a2e; }
.badge.advanced { background: #7c3aed; }
.badge.beginner { background: #4a5568; }
`]
})
export class AppComponent {
samurai: Samurai = {
id: 1,
name: 'Musashi',
clan: 'Niten Ichi-ryū',
level: 8,
health: 100
};
samuraiList: Samurai[] = [
{ id: 1, name: 'Musashi', clan: 'Niten', level: 10, health: 100 },
{ id: 2, name: 'Kojiro', clan: 'Ganryū', level: 9, health: 85 },
{ id: 3, name: 'Jubei', clan: 'Yagyu', level: 7, health: 90 }
];
selectedId = 1;
attack(): void {
this.samurai.health = Math.max(0, this.samurai.health - 15);
}
heal(): void {
this.samurai.health = Math.min(100, this.samurai.health + 20);
}
selectSamurai(id: number): void {
this.selectedId = id;
}
getRank(level: number): string {
if (level >= 9) return 'master';
if (level >= 5) return 'advanced';
return 'beginner';
}
}Pamiętaj: klamry pokazują dane, nawiasy kwadratowe ustawiają właściwości, nawiasy okrągłe przynoszą meldunki o zdarzeniach, a banan w pudełku prowadzi rozmowę w obie strony.
Hasła z dokumentacji w tej lekcji
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaka składnia służy do interpolacji w szablonie Angulara?
2. Jaka składnia służy do nasłuchiwania zdarzeń w Angularze?
3. Jaka składnia służy do property binding (wiązania właściwości) w Angularze?
Zadania praktyczne w grze
- Układanie w poziomie
Ułóż tokeny jednej linii, aby utworzyć two-way binding wartości pola input do właściwości samuraiName:
- Edytor kodu
Uzupełnij kod: dodaj property binding dla [disabled] i event binding dla (click) na przycisku ataku.
- Klikanie w kolejności
Ułóż znaki, aby zbudować property binding atrybutu src: [src]
- Układanie w poziomie
Ułóż tokeny jednej linii, aby podpiąć metodę onAttack() do kliknięcia przycisku: