Pola tekstowe: text, password, email, textarea
Hasło nie może być widoczne przez ramię. Adres e-mail na telefonie potrzebuje klawiatury z małpą. Dłuższy opis nie zmieści się w jednej linijce. Wszystko to są pola tekstowe — różnią się typem, a z [(ngModel)] pracuje się z nimi dokładnie tak samo.
Czego się dziś nauczysz
- Dobierzesz typ pola (
text,password,email) do rodzaju danych. - Połączysz z sygnałem wielowierszowe pole
<textarea>. - Zrobisz przycisk „pokaż hasło”, zmieniając typ pola wiązaniem
[type]. - Użyjesz atrybutów
placeholder,maxlengthiautocomplete. - Pokażesz licznik pozostałych znaków jako
computed. - Sprawdzisz, czy pole nie jest puste, używając
trim(). - Zachowasz podział na wiersze w podglądzie tekstu stylem
white-space: pre-line.
Przygotowanie: lekcja 17 — umiesz połączyć pole z sygnałem przez [(ngModel)] i wiesz, co dzieje się pod spodem. Przewidywany czas: 45 min z zadaniami.
Tekst niejedno ma imię
Dla Angulara każde z tych pól zwraca po prostu tekst — typ string. Różnica jest po stronie przeglądarki i użytkownika. Dobrze dobrany typ pola to mniej pracy dla użytkownika i mniej błędów w danych, a kod w klasie zostaje ten sam: sygnał z tekstem.
| Znacznik | Do czego | Co robi przeglądarka |
|---|---|---|
<input type="text"> | imię, nick, miasto | zwykła linijka; typ domyślny |
<input type="password"> | hasło, PIN | zamienia znaki na kropki |
<input type="email"> | adres e-mail | na telefonie — klawiatura z @; poprawność sprawdzimy w lekcji 26 |
<textarea></textarea> | opis, uwagi, wiadomość | wiele wierszy; wysokość ustawia atrybut rows |
| Atrybut | Działanie |
|---|---|
placeholder="…" | szary tekst podpowiedzi; znika przy pisaniu |
maxlength="200" | przeglądarka nie pozwoli wpisać więcej znaków |
autocomplete="off" | wyłącza podpowiedzi wcześniej wpisanych wartości |
rows="4" | wysokość textarea w wierszach |
placeholder nie zastępuje etykiety
Podpowiedź znika, gdy tylko użytkownik zacznie pisać — a wtedy nie wiadomo już, czego dotyczy pole. Etykieta <label> zostaje zawsze.
textarea nie jest pustym znacznikiem
<input> nie ma zamknięcia, a <textarea> — musi mieć </textarea>. Między znacznikami nic nie wpisujesz: zawartość ustawia ngModel.
Hasło z przyciskiem „pokaż”
Typ pola hasła zmienia wiązanie [type]: raz password, raz text. Grupa input-group Bootstrapa skleja pole z przyciskiem.
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-demo',
imports: [FormsModule],
templateUrl: './demo.html'
})
export class Demo {
readonly login = signal('');
readonly haslo = signal('');
readonly widoczne = signal(false);
}
<div class="p-3" style="width: 24rem">
<label class="form-label" for="login">Login</label>
<input id="login" name="login" type="text" class="form-control mb-3"
autocomplete="off" [(ngModel)]="login">
<label class="form-label" for="haslo">Hasło</label>
<div class="input-group">
<input id="haslo" name="haslo" class="form-control"
[type]="widoczne() ? 'text' : 'password'" [(ngModel)]="haslo">
<button class="btn btn-outline-secondary" type="button"
(click)="widoczne.set(!widoczne())">
{{ widoczne() ? 'Ukryj' : 'Pokaż' }}
</button>
</div>
<p class="mt-3 text-secondary">Hasło ma {{ haslo().length }} znaków.</p>
</div>
Hasło [••••••••] [Pokaż]
Hasło ma 8 znaków.
po kliknięciu Pokaż: [tajne123] [Ukryj]
Zmienia się tylko typ pola; wartość w sygnale zostaje ta sama. type="button" przy przycisku przyda się w lekcji 24, gdy pola trafią do znacznika <form>.
Zatrzymaj się
Dlaczego licznik znaków pokazuje długość hasła także wtedy, gdy na ekranie są kropki?
E-mail i prosta kontrola
Pole email połączone z sygnałem i bardzo prosta kontrola w computed: czy jest dokładnie jedna małpa i czy po niej jest kropka. Prawdziwą walidację zrobimy w lekcji 26. form-text to drobny, szary opis pod polem.
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 email = signal('');
readonly wygladaDobrze = computed(() => {
const tekst = this.email().trim();
const malpa = tekst.indexOf('@');
return malpa > 0
&& tekst.lastIndexOf('@') === malpa
&& tekst.indexOf('.', malpa) > malpa + 1;
});
}
<div class="p-3" style="width: 26rem">
<label class="form-label" for="email">Adres e-mail rodzica</label>
<input id="email" name="email" type="email" class="form-control"
placeholder="np. jan.kowalski@poczta.pl" [(ngModel)]="email">
@if (email() === '') {
<p class="form-text">Wyślemy tu potwierdzenie zapisu.</p>
} @else if (wygladaDobrze()) {
<p class="text-success mt-2">Wygląda poprawnie.</p>
} @else {
<p class="text-danger mt-2">To nie wygląda na adres e-mail.</p>
}
</div>
jan → To nie wygląda na adres e-mail.
jan@poczta → To nie wygląda na adres e-mail.
jan@poczta.pl → Wygląda poprawnie.
Otwórz stronę w trybie urządzenia mobilnego (F12 → ikona telefonu) — klawiatura ekranowa ma osobny klawisz @.
Zatrzymaj się
indexOf zwraca numer pozycji albo -1, gdy nie znalazł. Dlaczego warunek to malpa > 0, a nie malpa >= 0?
textarea z licznikiem znaków
Wiele wierszy tekstu i ograniczenie do 200 znaków. maxlength pilnuje limitu w przeglądarce, a licznik pod polem robi się czerwony, gdy zostaje mało miejsca.
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 limit = 200;
readonly opis = signal('');
readonly zostalo = computed(() => this.limit - this.opis().length);
}
<div class="p-3" style="width: 30rem">
<label class="form-label" for="opis">Opisz swoją drużynę</label>
<textarea id="opis" name="opis" class="form-control" rows="4"
[maxlength]="limit" [(ngModel)]="opis"></textarea>
<p class="form-text text-end"
[class.text-danger]="zostalo() < 20">
Zostało znaków: {{ zostalo() }} / {{ limit }}
</p>
</div>
Zostało znaków: 15 / 200 (czerwony)
Po dojściu do zera przeglądarka nie pozwala dopisać nic więcej. Enter przenosi do nowego wiersza, a znak nowej linii też liczy się jako znak. text-end wyrównuje licznik do prawej.
Zatrzymaj się
[maxlength]="limit" ma nawiasy kwadratowe. Co by się zmieniło przy zapisie maxlength="200"? Kiedy ta różnica zaczęłaby mieć znaczenie?
Wiadomość z podglądem i czyszczeniem
Mały formularz kontaktowy: temat i treść, a obok podgląd wiadomości. Przycisk „Wyślij” działa tylko wtedy, gdy oba pola zawierają coś więcej niż spacje.
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 temat = signal('');
readonly tresc = signal('');
readonly wyslane = signal<string[]>([]);
readonly gotowe = computed(() =>
this.temat().trim() !== '' && this.tresc().trim() !== ''
);
wyslij(): void {
this.wyslane.update(lista => [...lista, this.temat().trim()]);
this.temat.set('');
this.tresc.set('');
}
}
<div class="p-3">
<div class="row g-4" style="max-width: 56rem">
<div class="col-md-6">
<label class="form-label" for="temat">Temat</label>
<input id="temat" name="temat" class="form-control mb-2" [(ngModel)]="temat">
<label class="form-label" for="tresc">Treść</label>
<textarea id="tresc" name="tresc" class="form-control mb-2" rows="5" [(ngModel)]="tresc"></textarea>
<button class="btn btn-primary" [disabled]="!gotowe()" (click)="wyslij()">Wyślij</button>
<p class="mt-3 small">Wysłane tematy: {{ wyslane().join(', ') || 'brak' }}</p>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">{{ temat() || '(bez tematu)' }}</div>
<div class="card-body" style="white-space: pre-line">{{ tresc() || '(pusta wiadomość)' }}</div>
</div>
</div>
</div>
</div>
Wysłane tematy: Sala 12
(pola wyczyszczone, podgląd: „(bez tematu)”, „(pusta wiadomość)”)
Podgląd zachowuje podział na wiersze dzięki stylowi white-space: pre-line. Wartość zastępczą — gdy pole jest puste — daje operator ||.
Zatrzymaj się
Bez white-space: pre-line cała treść w podglądzie byłaby w jednej linijce. Dlaczego przeglądarka domyślnie ignoruje znaki nowej linii w HTML?
Zestawienie elementów
| Element | Znaczenie | Uwagi |
|---|---|---|
type="text|password|email" | rodzaj pola | Wartość zawsze tekstem. |
[type]="warunek ? 'text' : 'password'" | typ zmieniany w locie | „Pokaż hasło”. |
<textarea rows="4"></textarea> | pole wielowierszowe | Wymaga zamknięcia. |
placeholder, maxlength, autocomplete | podpowiedź, limit, podpowiedzi przeglądarki | [maxlength] — z pola klasy. |
tekst.trim() | bez spacji na brzegach | Przed sprawdzaniem, czy puste. |
tekst.indexOf('@'), lastIndexOf | pozycja znaku albo -1 | Z TypeScriptu. |
a || 'zastępczy' | wartość, gdy a jest puste | Pusty tekst = fałsz. |
white-space: pre-line | zachowanie nowych wierszy | Styl CSS. |
input-group, form-text, text-end | pole z przyciskiem, opis pod polem, do prawej | Bootstrap. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
<textarea [(ngModel)]="opis" /> | NG5002: Only void, custom and foreign elements can be self closed. textarea wymaga </textarea>. |
type="pasword" (literówka) | Przeglądarka nie zna typu i pokazuje zwykłe pole — hasło jest widoczne. Błędu nie ma nigdzie. |
[type]="widoczne ? 'text' : 'password'" | Ostrzeżenie NG8109, a hasło jest zawsze widoczne — sygnał bez nawiasów jest „prawdziwy”. Dopisz (). |
opis() === '' zamiast opis().trim() === '' | Wiadomość z samych spacji przechodzi kontrolę. |
Tekst wpisany między <textarea> a </textarea> | ngModel i tak go nadpisze wartością sygnału. Wartość początkową ustaw w sygnale. |
Sam placeholder bez etykiety | Po wpisaniu pierwszej litery nie wiadomo, czego dotyczy pole. |
Zadania
Pole password na 4-cyfrowy PIN z maxlength 4. Pod polem kropki pokazujące, ile cyfr już wpisano, np. ●●○○.
Kryterium sukcesu: nie da się wpisać więcej niż 4 znaki, a liczba pełnych kropek zgadza się z długością PIN-u.
Podpowiedź
'●'.repeat(n) powtarza znak n razy; resztę uzupełnij '○'.repeat(4 - n) w computed.
Pole „Imię” i pole wielowierszowe „O mnie” (5 wierszy, limit 150 znaków). Pod nimi karta profilu: imię w nagłówku i opis z zachowanym podziałem na wiersze. Gdy opis jest pusty, karta pokazuje szary napis „Brak opisu”.
Kryterium sukcesu: karta zmienia się podczas pisania, podział na wiersze jest zachowany, a pusty opis daje szary napis.
Podpowiedź
@if (opis().trim()) { … } @else { … } i styl white-space: pre-line.
Formularz zgłoszenia usterki w pracowni: numer stanowiska (text), e-mail zgłaszającego (email) i opis (textarea, maksymalnie 300 znaków, z licznikiem). Obok podgląd zgłoszenia jako karta. Przycisk „Zgłoś” aktywny tylko, gdy wszystkie trzy pola są wypełnione (po trim).
Kryterium sukcesu: wszystkie trzy pola zasilają podgląd na żywo, licznik pokazuje pozostałe znaki, a przycisk blokuje się przy pustym polu.
Podpowiedź
Przykłady 2, 3 i 4 razem dają prawie gotowe rozwiązanie.
Dwa pola password: „Hasło” i „Powtórz hasło”, oba z przyciskiem „Pokaż”. Pod nimi komunikat: szary, gdy drugie pole jest puste; czerwony „Hasła się różnią”; zielony „Hasła zgodne”.
Kryterium sukcesu: komunikat zmienia się przy każdej literze w obu polach; przyciski „Pokaż” działają niezależnie.
Podpowiedź
Dwa sygnały z hasłami i dwa sygnały logiczne do widoczności. Porównanie: haslo() === powtorz().
Pole hasła z przyciskiem „Pokaż”. Pod nim lista czterech warunków z ikonką ✔ albo ✘: co najmniej 8 znaków, wielka litera, cyfra, znak specjalny. Hasło jest „silne”, gdy spełnione są wszystkie — wtedy pojawia się zielona ramka alert.
Kryterium sukcesu: każdy warunek zmienia ikonkę w trakcie pisania; ramka pojawia się tylko przy komplecie.
Podpowiedź
Każdy warunek to osobny computed. Wielką literę wykryjesz porównaniem haslo() !== haslo().toLowerCase(), cyfrę — /[0-9]/.test(haslo()), znak specjalny — /[^a-zA-Z0-9]/.test(haslo()).
Pole textarea na treść SMS-a. Pod nim: liczba znaków, liczba SMS-ów (po 160 znaków każdy, pusty tekst = 0) i liczba słów. Dodaj przyciski „Wielkie litery” i „Usuń podwójne spacje”, które zmieniają treść w polu.
Kryterium sukcesu: dla 161 znaków widać 2 SMS-y; przyciski zmieniają tekst w polu, a liczniki od razu się przeliczają.
Podpowiedź
SMS-y: Math.ceil(dlugosc / 160). Słowa: tekst.trim().split(/\s+/), ale pusty tekst daje 0. Przyciski: tresc.set(tresc().toUpperCase()) i replace(/ {2,}/g, ' ').
Co trzeba zapamiętać
text,password,emailitextareałączysz z sygnałem tak samo:[(ngModel)]; wartość zawsze jest tekstem.[type]pozwala zmieniać typ pola w trakcie działania — tak działa „pokaż hasło”.maxlengthogranicza długość, a licznik pozostałych znaków to prostycomputed.- Przed sprawdzaniem, czy pole jest puste, wywołuj
trim(). textareawymaga znacznika zamykającego; nowe wiersze w podglądzie zachowawhite-space: pre-line.placeholderto podpowiedź, nie etykieta.
Sprawdź się: dlaczego placeholder nie może zastąpić etykiety pola? Opisz moment, w którym użytkownik się pogubi.
Dokumentacja: Bootstrap — pola formularza.