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

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.

Angular 22 type password email textarea placeholder maxlength trim 45 min
CEL LEKCJI

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, maxlength i autocomplete.
  • 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.

TEORIA

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.

ZnacznikDo czegoCo robi przeglądarka
<input type="text">imię, nick, miastozwykła linijka; typ domyślny
<input type="password">hasło, PINzamienia znaki na kropki
<input type="email">adres e-mailna telefonie — klawiatura z @; poprawność sprawdzimy w lekcji 26
<textarea></textarea>opis, uwagi, wiadomośćwiele wierszy; wysokość ustawia atrybut rows
AtrybutDział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.

PRZYKŁAD 1

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.

src/app/nowa-18-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 login = signal('');
    readonly haslo = signal('');
    readonly widoczne = signal(false);
}
src/app/nowa-18-1/demo.html — pełny plik
<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>
wynik po wpisaniu hasła „tajne123”
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?

PRZYKŁAD 2

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.

src/app/nowa-18-2/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 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;
    });
}
src/app/nowa-18-2/demo.html — pełny plik
<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>
wynik dla kolejnych wpisów
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?

PRZYKŁAD 3

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.

src/app/nowa-18-3/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 limit = 200;
    readonly opis = signal('');
    readonly zostalo = computed(() => this.limit - this.opis().length);
}
src/app/nowa-18-3/demo.html — pełny plik
<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>
wynik po wpisaniu 185 znaków
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?

PRZYKŁAD 4

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.

src/app/nowa-18-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 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('');
    }
}
src/app/nowa-18-4/demo.html — pełny plik
<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>
wynik po wysłaniu wiadomości o temacie „Sala 12”
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?

ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZnaczenieUwagi
type="text|password|email"rodzaj polaWartość zawsze tekstem.
[type]="warunek ? 'text' : 'password'"typ zmieniany w locie„Pokaż hasło”.
<textarea rows="4"></textarea>pole wielowierszoweWymaga zamknięcia.
placeholder, maxlength, autocompletepodpowiedź, limit, podpowiedzi przeglądarki[maxlength] — z pola klasy.
tekst.trim()bez spacji na brzegachPrzed sprawdzaniem, czy puste.
tekst.indexOf('@'), lastIndexOfpozycja znaku albo -1Z TypeScriptu.
a || 'zastępczy'wartość, gdy a jest pustePusty tekst = fałsz.
white-space: pre-linezachowanie nowych wierszyStyl CSS.
input-group, form-text, text-endpole z przyciskiem, opis pod polem, do prawejBootstrap.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
<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 etykietyPo wpisaniu pierwszej litery nie wiadomo, czego dotyczy pole.
ZADANIA

Zadania

ZAD 1PIN★☆☆

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.

ZAD 2Bio w profilu★☆☆

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.

ZAD 3Zgłoszenie usterki★★☆

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.

ZAD 4Powtórz hasło★★☆

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().

ZAD 5Siła hasła po polsku★★★

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()).

ZAD 6Generator SMS-a★★★

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, ' ').

PODSUMOWANIE

Co trzeba zapamiętać

  • text, password, email i textarea łą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”.
  • maxlength ogranicza długość, a licznik pozostałych znaków to prosty computed.
  • Przed sprawdzaniem, czy pole jest puste, wywołuj trim().
  • textarea wymaga znacznika zamykającego; nowe wiersze w podglądzie zachowa white-space: pre-line.
  • placeholder to 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.

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