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

[(ngModel)] — jak działa wiązanie dwukierunkowe

Od tej lekcji każde pole formularza połączysz z danymi zapisem [(ngModel)]. Zanim zaczniesz używać go z różnymi kontrolkami, rozbierzesz go na części: co robią nawiasy kwadratowe, co okrągłe, skąd bierze się dyrektywa ngModel i dlaczego tu — wyjątkowo — sygnał piszesz bez nawiasów.

Angular 22 [(ngModel)] FormsModule [ngModel] (ngModelChange) name label for 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wyjaśnisz, czym jest wiązanie dwukierunkowe i dlaczego #pole oraz (input) z lekcji 08 dają tylko jeden kierunek.
  • Zaimportujesz FormsModule i powiesz, skąd bierze się dyrektywa ngModel.
  • Połączysz pole tekstowe z sygnałem zapisem [(ngModel)]="sygnal" — bez nawiasów przy sygnale.
  • Rozpiszesz skrót na [ngModel] i (ngModelChange) i powiesz, za który kierunek odpowiada każda część.
  • Zmienisz zawartość pola z kodu, ustawiając sygnał.
  • Połączysz dwa pola z jednym sygnałem i zbudujesz z niego computed.
  • Dodasz polu atrybuty name i id oraz etykietę label for.
  • Rozpoznasz błędy NG8002, NG5002 i ostrzeżenie NG8101.

Przygotowanie: lekcje 06, 08 i 09–11 — znasz [ ], ( ), #pole, sygnały i computed. Przewidywany czas: 2 × 45 min z zadaniami.

TEORIA

Dwa kierunki naraz

Pomyśl o formularzu zapisu. Użytkownik wpisuje imię — klasa musi je poznać. Użytkownik klika „Wyczyść” — pole musi zrobić się puste. Ładujesz zapisane dane — pola muszą się wypełnić. Dane płyną więc w dwie strony: z pola do klasy i z klasy do pola.

SposóbPole → klasaKlasa → poleUwagi
#pole i pole.value (lekcja 08)tylko w chwili kliknięciaręcznie: pole.value = ''klasa nie wie, co jest w polu
(input)="…($event)" (lekcja 08)przy każdej literzenietrzeba wyciągać tekst z $event.target
[value]="tekst()" (lekcja 06)nietakpole tylko pokazuje wartość
[(ngModel)]="tekst"taktakpole i sygnał mają zawsze tę samą wartość

Wiązanie dwukierunkowe to po prostu oba kierunki naraz: wiązanie właściwości [ ] (klasa → pole) i zdarzenie ( ) (pole → klasa) zapisane jednym znakiem.

TEORIA

Co jest pod spodem: dyrektywa ngModel

1. ngModel to dyrektywa z FormsModule

Zwykły <input> nie ma właściwości ngModel. Dodaje mu ją dyrektywa — kawałek kodu Angulara, który „przykleja się” do elementu i daje mu nowe możliwości. Dyrektywa NgModel mieszka w pakiecie @angular/forms, w zestawie FormsModule. Dlatego każdy komponent z ngModel ma w imports wpis FormsModule — bez niego Angular nie rozpozna zapisu i zgłosi błąd NG8002.

2. Dyrektywa ma wejście i wyjście

NgModel ma wejście ngModel (wartość, którą ma pokazać pole) i wyjście ngModelChange (zdarzenie z nową wartością po każdej zmianie w polu). Z tymi dwiema częściami pracujesz tak samo jak z [title] i (click):

dwa równoważne zapisy
<input [(ngModel)]="imie">

<input [ngModel]="imie()" (ngModelChange)="imie.set($event)">
CzęśćKierunekCo robi
[ngModel]="imie()"klasa → polepole pokazuje wartość sygnału; przy każdej zmianie sygnału pole się aktualizuje
(ngModelChange)="imie.set($event)"pole → klasapo każdej zmianie w polu do sygnału trafia nowa wartość; $event to gotowy tekst, a nie obiekt zdarzenia
[(ngModel)]="imie"obaskrót, który Angular sam rozpisuje na dwie linijki powyżej

3. „Banan w pudełku” i sygnał bez nawiasów

Zapis [( )] łatwo pomylić z ([ ]). Zapamiętaj obrazek: banan () leży w pudełku []. W skrócie piszesz samą nazwę sygnału, bez (), bo Angular potrzebuje całego sygnału — będzie go i czytał (imie()), i zmieniał (imie.set(…)). Gdybyś napisał imie(), dostałby tylko tekst, do którego nie da się nic zapisać. Wszędzie indziej odczytujesz sygnał jak zwykle: {{ imie() }}.

Zwykłe pole też zadziała

[(ngModel)]="imie" działa także ze zwykłym polem klasy (imie = '') — Angular wtedy po prostu przypisuje do niego nowy tekst. W tym kursie używamy sygnałów: wtedy z wartości pola korzystają computed i wszystko, czego nauczyłeś się w lekcjach 09–11, a ekran odświeża się także po zmianach spoza szablonu.

4. name, id i etykieta

Każdemu polu z ngModel nadawaj atrybut name. Poza znacznikiem <form> nie jest wymagany, ale wewnątrz formularza (lekcja 24) bez niego Angular zgłosi błąd. Etykieta <label for="imie"> i pole id="imie" z tą samą wartością sprawiają, że kliknięcie w napis ustawia kursor w polu, a czytnik ekranu wie, czego pole dotyczy.

PRZYKŁAD 1

Pole → sygnał: podgląd na żywo

Najprostsze połączenie: jedno pole, jeden sygnał, jeden napis pod spodem.

src/app/nowa-17-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 imie = signal('');
}
src/app/nowa-17-1/demo.html — pełny plik
<div class="p-3" style="width: 24rem">
  <label class="form-label" for="imie">Twoje imię</label>
  <input id="imie" name="imie" class="form-control" [(ngModel)]="imie">
  <p class="mt-3 fs-5">Cześć, {{ imie() }}!</p>
</div>
wynik po wpisaniu „Ola”
Twoje imię
[Ola                ]
Cześć, Ola!

Napis nadąża za każdą literą. Klasa nie ma ani jednej metody: wartość z pola trafia do sygnału sama, przez ngModelChange ukryte w skrócie.

Zatrzymaj się

Porównaj z lekcją 08, przykład 3. Ile linijek w klasie było tam potrzebnych, żeby wyciągnąć tekst z pola, a ile tutaj?

PRZYKŁAD 2

Sygnał → pole: kod zmienia zawartość pola

Teraz kierunek, którego #pole i (input) nie dają za darmo: przyciski ustawiają sygnał, a pole samo pokazuje nową wartość.

src/app/nowa-17-2/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 nick = signal('');

    przyklad(): void {
        this.nick.set('Pixel2026');
    }

    wyczysc(): void {
        this.nick.set('');
    }
}
src/app/nowa-17-2/demo.html — pełny plik
<div class="p-3" style="width: 24rem">
  <label class="form-label" for="nick">Nick w turnieju</label>
  <input id="nick" name="nick" class="form-control mb-2" [(ngModel)]="nick">
  <button class="btn btn-outline-primary me-2" (click)="przyklad()">Wpisz przykładowy</button>
  <button class="btn btn-outline-danger" (click)="wyczysc()">Wyczyść</button>
  <p class="mt-3">W sygnale jest: „{{ nick() }}”</p>
</div>
wynik po kliknięciu „Wpisz przykładowy”
[Pixel2026          ]
W sygnale jest: „Pixel2026”

„Wyczyść” opróżnia pole, choć metoda zmienia tylko sygnał. Dopisz coś ręcznie po przykładowym nicku — sygnał od razu ma nową wartość. W klasie nie ma ani słowa o polu tekstowym.

Zatrzymaj się

Metoda wyczysc zmienia tylko sygnał. Która część rozpisanego skrótu sprawia, że pole się opróżnia?

PRZYKŁAD 3

Skrót i wersja rozpisana obok siebie

Dwa pola połączone z dwoma sygnałami: pierwsze skrótem, drugie wersją rozpisaną z teorii. Sprawdź, że zachowują się identycznie — także po kliknięciu „Wyczyść oba”.

src/app/nowa-17-3/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 a = signal('');
    readonly b = signal('');

    wyczysc(): void {
        this.a.set('');
        this.b.set('');
    }
}
src/app/nowa-17-3/demo.html — pełny plik
<div class="p-3" style="width: 28rem">
  <label class="form-label" for="a">Skrót: [(ngModel)]</label>
  <input id="a" name="a" class="form-control mb-1" [(ngModel)]="a">
  <p class="small">a() = „{{ a() }}”</p>

  <label class="form-label" for="b">Rozpisane: [ngModel] + (ngModelChange)</label>
  <input id="b" name="b" class="form-control mb-1" [ngModel]="b()" (ngModelChange)="b.set($event)">
  <p class="small">b() = „{{ b() }}”</p>

  <button class="btn btn-outline-secondary" (click)="wyczysc()">Wyczyść oba</button>
</div>
wynik po wpisaniu „abc” w oba pola
a() = „abc”
b() = „abc”

Nie ma żadnej różnicy w działaniu. W wersji rozpisanej przy [ngModel] są nawiasy (odczyt sygnału), a w (ngModelChange) jest set z $event — czyli dokładnie to, co Angular dopisuje za Ciebie w skrócie. W lekcji 25 wykorzystasz wersję rozpisaną, żeby między odczytem a zapisem wstawić własny kod.

Zatrzymaj się

W [ngModel]="b()" są nawiasy, a w [(ngModel)]="a" nie ma. Wyjaśnij to jednym zdaniem.

PRZYKŁAD 4

Jeden sygnał, dwa pola i computed

Dwa pola połączone z tym samym sygnałem. Pisanie w jednym zmienia sygnał, a sygnał — drugie pole. Z sygnału liczą się dwa computed.

src/app/nowa-17-4/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 nick = signal('');
    readonly dlugosc = computed(() => this.nick().trim().length);
    readonly email = computed(() => this.nick().trim().toLowerCase() + '@klub.zsmi.pl');
}
src/app/nowa-17-4/demo.html — pełny plik
<div class="p-3" style="width: 28rem">
  <label class="form-label" for="nick1">Nick — pole 1</label>
  <input id="nick1" name="nick1" class="form-control mb-2" [(ngModel)]="nick">

  <label class="form-label" for="nick2">Nick — pole 2 (ten sam sygnał)</label>
  <input id="nick2" name="nick2" class="form-control" [(ngModel)]="nick">

  <p class="mt-3 mb-1">Znaków: {{ dlugosc() }}</p>
  <p>Adres w klubie: {{ email() }}</p>
</div>
wynik po wpisaniu „Pixel” w pole 1
Nick — pole 1   [Pixel   ]
Nick — pole 2   [Pixel   ]
Znaków: 5
Adres w klubie: pixel@klub.zsmi.pl

To najlepszy dowód, że dane naprawdę płyną w obie strony: pole 1 → sygnał → pole 2. Pola mają różne name i id, bo to dwa różne elementy strony — wspólny jest tylko sygnał.

Zatrzymaj się

W computed nie ma ani słowa o polach, a mimo to przelicza się przy każdym naciśnięciu klawisza. Co je łączy?

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
import { FormsModule } from '@angular/forms'zestaw dyrektyw formularzyDo imports komponentu.
[(ngModel)]="sygnal"wiązanie dwukierunkoweSygnał bez ().
[ngModel]="sygnal()"klasa → poleOdczyt z ().
(ngModelChange)="sygnal.set($event)"pole → klasa$event = nowa wartość.
name="…"nazwa polaObowiązkowa w <form>.
<label for="x"> + id="x"etykieta połączona z polemKlik w napis = kursor w polu.
form-label, form-controletykieta i pole BootstrapaZ lekcji 08 i 12.
NG8002nieznana właściwość ngModelBrak FormsModule.
NG5002 Unsupported expression in a two-way bindingnawiasy przy sygnale w skrócieUsuń ().
NG8101odwrócone nawiasy ([ ])Banan w pudełku: [( )].
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
Brak FormsModule w importsNG8002: Can’t bind to 'ngModel’ since it isn’t a known property of 'input’. Ten komunikat zawsze oznacza brakujący import.
[(ngModel)]="imie()"NG5002: Unsupported expression in a two-way binding. Do wyniku odczytu nie da się nic zapisać — podaj sam sygnał.
([ngModel])="imie"Ostrzeżenie NG8101, a pole nie działa. Nawiasy w złej kolejności — banan ma być w pudełku: [( )].
[ngModel]="imie()" bez (ngModelChange)Pole pokazuje wartość sygnału, ale pisanie go nie zmienia — to tylko jeden kierunek.
(ngModelChange)="imie.set($event.target.value)"Błąd — w ngModelChange $event to już gotowy tekst, nie obiekt zdarzenia.
Pole z ngModel w <form> bez nameBłąd w konsoli przeglądarki: If ngModel is used within a form tag, either the name attribute must be set… Dopisz name.
for="imie" i id="Imie"Działa, ale kliknięcie w etykietę nie ustawia kursora. Wielkość liter w id ma znaczenie.
ZADANIA

Zadania

ZAD 1Powitanie z miasta★☆☆

Pole „Miasto” i napis pod nim: „Pozdrowienia dla mieszkańców miasta …!”. Dodaj przycisk „Lębork”, który wpisuje tę nazwę do pola, i przycisk „Wyczyść”.

Kryterium sukcesu: napis zmienia się podczas pisania, a przyciski zmieniają jednocześnie pole i napis.

Podpowiedź

Jeden sygnał, [(ngModel)] i dwie metody z set.

ZAD 2Skrót na piechotę★☆☆

Weź przykład 1 i zamień skrót [(ngModel)] na wersję rozpisaną. Sprawdź, że wszystko działa tak samo. Potem usuń (ngModelChange) i zapisz w komentarzu HTML, co przestało działać i dlaczego. Na koniec przywróć skrót.

Kryterium sukcesu: wersja rozpisana działa identycznie, a komentarz poprawnie opisuje utracony kierunek.

Podpowiedź

[ngModel]="imie()" (ngModelChange)="imie.set($event)".

ZAD 3Generator loginu★★☆

Dwa pola: imię i nazwisko. Pod nimi login: pierwsze trzy litery imienia + pierwsze trzy litery nazwiska, małymi literami, np. jankow. Gdy któreś pole ma mniej niż 3 znaki, zamiast loginu pokaż szary napis „za krótko”.

Kryterium sukcesu: login aktualizuje się w trakcie pisania i zawsze składa się z małych liter.

Podpowiedź

computed ze slice(0, 3) i toLowerCase(), warunek — @if.

ZAD 4Identyfikator uczestnika★★☆

Trzy pola (imię, nazwisko, klasa) po lewej i karta po prawej (siatka col-md-6). Karta pokazuje duże inicjały, imię i nazwisko oraz klasę. Gdy pola są puste, zamiast inicjałów widać „?”, a zamiast klasy — kreskę.

Kryterium sukcesu: karta wypełnia się w miarę pisania; puste pola dają „?” i „—”.

Podpowiedź

Inicjały: computed(() => (this.imie().charAt(0) + this.nazwisko().charAt(0)).toUpperCase()). Wartość zastępczą da operator ||: {{ klasa() || '—' }}. Duży tekst: klasa display-6.

ZAD 5Wypełnij z szablonu★★★

Formularz ogłoszenia z trzema polami: tytuł, sala, godzina. Nad nim trzy przyciski z gotowymi szablonami („Turniej retro / 12 / 16:00”, „Noc robotów / 7 / 18:00”, „Druk 3D / 14 / 14:30”), które wypełniają wszystkie trzy pola naraz. Użytkownik może potem poprawić dowolne pole. Pod formularzem podgląd ogłoszenia jako karta, a przycisk „Wyczyść” opróżnia wszystko.

Kryterium sukcesu: przyciski wypełniają pola, ręczne poprawki od razu trafiają do podglądu, a „Wyczyść” czyści pola i kartę.

Podpowiedź

Jedna metoda wypelnij(tytul, sala, godzina) z trzema set. Kierunek „klasa → pole” zrobi za Ciebie [(ngModel)].

ZAD 6Kopia lustrzana★★★

Zrób dwa pola połączone z dwoma różnymi sygnałami: „Tekst” i „Tekst od tyłu”. Pisanie w pierwszym wpisuje do drugiego odwrócony tekst, a pisanie w drugim — odwrócony do pierwszego. Użyj wersji rozpisanej w obu polach.

Kryterium sukcesu: niezależnie od tego, w którym polu piszesz, drugie zawsze zawiera ten sam tekst czytany od końca.

Podpowiedź

W (ngModelChange) wywołaj metodę, która ustawia oba sygnały. Odwrócenie tekstu: tekst.split('').reverse().join('').

PODSUMOWANIE

Co trzeba zapamiętać

  • Wiązanie dwukierunkowe to wiązanie właściwości [ ] i zdarzenie ( ) naraz: pole i dane mają zawsze tę samą wartość.
  • ngModel to dyrektywa z FormsModule — bez importu dostaniesz NG8002.
  • [(ngModel)]="s" to skrót od [ngModel]="s()" i (ngModelChange)="s.set($event)".
  • W skrócie podajesz sam sygnał, bez nawiasów; odczyt w innych miejscach — jak zwykle, z nawiasami.
  • W (ngModelChange) $event to gotowa nowa wartość, nie obiekt zdarzenia.
  • Zmiana sygnału w kodzie od razu zmienia zawartość pola; jeden sygnał może obsługiwać kilka pól.
  • Każde pole dostaje name, a etykieta for wskazuje id pola.

Sprawdź się: czym różni się odczyt pola przez #pole od połączenia przez [(ngModel)]? Podaj sytuację, w której zmienna szablonowa nie wystarczy.

Dokumentacja: angular.dev — wiązanie dwukierunkowe.

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