Mert Özen Her Satırda Daha İleri
Frontend Angular — #15

Angular Template-driven Forms: ngModel, ngForm ve Form Doğrulama

Mert Özen 31 Eki 2026 7 dk 1 görüntülenme
Angular Template-driven Forms: ngModel, ngForm ve Form Doğrulama

Angular 12'de template-driven form kuruyoruz: FormsModule, ngModel ve name, ngForm ile ngSubmit, required/email/pattern validator'ları, touched/dirty durumları, CSS class'ları ve ngModelGroup.

Sürüm notu: Bu yazıdaki kod ve komutlar Angular 12 içindir.

Formun Görünmeyen Yükü

Bir form ilk bakışta birkaç input ve bir butondan ibaret görünür. İşin içine girince liste uzar: değerleri toplamak, zorunlu alanları kontrol etmek, e-posta biçimine bakmak, hata mesajını doğru anda göstermek, form geçersizken butonu kapatmak. Bunları elle, her input'a event bağlayarak yapmaya kalkarsan component kısa sürede bir if-else yığınına döner.

Bankada form doldurduğunu düşün. Formu gişeye uzatırsın, memur bir bakışta eksik alanları, yanlış yazılmış numarayı işaretler ve "şurayı düzeltin" der. Angular'da bu memurun iki farklı çalışma şekli var: template-driven form'lar ve reactive form'lar. Bu yazıda kuralların doğrudan template'e yazıldığı birincisini, yani template-driven form'ları konuşacağız. Örnek olarak küçük bir etkinlik kayıt formu kuracağız.

FormsModule'ü Eklemek

Daha önce gördüğümüz [(ngModel)] için FormsModule'ü import etmiştik. Template-driven form'ların bütün araçları da aynı module'den gelir:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { RegisterComponent } from './register/register.component';

@NgModule({
  declarations: [AppComponent, RegisterComponent],
  imports: [BrowserModule, FormsModule],
  bootstrap: [AppComponent]
})
export class AppModule { }

FormsModule import edildiği anda arka planda iki şey olur. Template'teki her <form> etiketine otomatik olarak NgForm adlı bir directive bağlanır ve formdaki alanları takip etmeye başlar. Ayrıca her forma novalidate attribute'u eklenir. Böylece tarayıcının kendi doğrulama balonları devreden çıkar ve kontrol tamamen Angular'a geçer.

İlk Form: ngModel ve name

<form>
  <label>Ad Soyad</label>
  <input name="fullName" ngModel>

  <label>E-posta</label>
  <input name="email" ngModel>

  <button type="submit">Kayıt Ol</button>
</form>

Burada ngModel köşeli ya da normal parantez olmadan, çıplak bir attribute olarak duruyor. Bu hâliyle "bu input'u formun bir alanı olarak kaydet" anlamına gelir. Class'ta herhangi bir property'ye bağlı değil; değeri form kendisi tutar.

name attribute'u ise zorunlu. Form, alanları bu isimle tanır ve değeri bu anahtarla saklar. name olmadan ngModel yazarsan Angular, form içindeki ngModel için ya name verilmesi ya da alanın standalone işaretlenmesi gerektiğini söyleyen bir hata fırlatır. Değeri aynı zamanda class'taki bir property'de de görmek istersen [(ngModel)]="fullName" yazabilirsin; name yine de yerinde durmalı.

#f="ngForm" ve ngSubmit

Form gönderildiğinde değerlere ulaşmak için forma bir isim veriyoruz:

<form #f="ngForm" (ngSubmit)="onSubmit(f)">
  <input name="fullName" ngModel>
  <input name="email" ngModel>
  <button type="submit">Kayıt Ol</button>
</form>

<pre>{{ f.value | json }}</pre>
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html'
})
export class RegisterComponent {
  onSubmit(form: NgForm): void {
    console.log(form.value);
    form.resetForm();
  }
}

#f daha önce gördüğümüz template reference variable. Normalde DOM elemanının kendisini tutar, ama ="ngForm" kısmı ona "bu etiketin DOM elemanını değil, üzerindeki NgForm directive'ini ver" der. Artık f üzerinden formun değerine, geçerliliğine, gönderilip gönderilmediğine erişebiliriz.

(ngSubmit) kullanıcı submit butonuna bastığında ya da bir input'tayken Enter'a bastığında tetiklenir ve sayfa yenilenmez. form.value bütün alanları name değerlerini anahtar yaparak tek bir nesnede toplar: { fullName: '...', email: '...' }. resetForm ise hem değerleri hem de birazdan göreceğimiz durum bilgilerini sıfırlar. Alttaki json pipe'lı satır, sen yazdıkça form değerinin nasıl değiştiğini canlı gösterir; geliştirme sırasında çok işe yarar.

Built-in Validator Attribute'ları

Kuralları da tanıdık HTML attribute'larıyla yazıyoruz:

<input name="fullName" ngModel required minlength="3">
<input name="email" ngModel required email>
<input name="phone" ngModel pattern="5[0-9]{9}">
<input name="guests" type="number" ngModel min="1" max="5">

<button type="submit" [disabled]="f.invalid">Kayıt Ol</button>

Bunlar standart attribute'lar gibi görünüyor ama FormsModule her birine bir validator directive'i bağlar. required alanı zorunlu yapar, minlength ve maxlength karakter sınırı koyar. email, Angular'ın kendi e-posta kontrolüdür; standart HTML'de böyle bir attribute yoktur. pattern ise bir regular expression alır. Angular ifadenin başına ve sonuna ^ ve $ işaretlerini kendisi ekler, yani yukarıdaki telefon alanı tam olarak 5 ile başlayan 10 haneli bir sayı ister. type="number" alanlarda min ve max da validator olarak çalışır.

Formun geçerliliği bütün alanların toplamıdır. Tek bir alan bile geçersizse f.invalid true olur ve butonu [disabled] ile kapatırız.

Control State'leri ve CSS Class'ları

Formun yanı sıra her alanın da kendi durumu vardır. Bir alana erişmek için ona da bir referans veriyoruz, bu sefer ngModel ile:

<input name="fullName" ngModel required minlength="3" #fullName="ngModel">
<p>valid: {{ fullName.valid }} | touched: {{ fullName.touched }} | dirty: {{ fullName.dirty }}</p>

Üç çift durum var. valid ve invalid kurallara uyulup uyulmadığını söyler. touched ve untouched kullanıcının alana girip çıkıp çıkmadığını, yani odak kaybını takip eder. dirty ve pristine ise değerin kullanıcı tarafından değiştirilip değiştirilmediğini gösterir. Gişedeki memurun aklından geçen üç soru gibi: bu alana dokunulmuş mu, bir şey değiştirilmiş mi, yazılan doğru mu?

Angular bu durumları aynı zamanda CSS class'ı olarak da elemanlara ekler: ng-valid ya da ng-invalid, ng-touched ya da ng-untouched, ng-dirty ya da ng-pristine. Geliştirici araçlarında input'a bakarsan, sen yazdıkça bu class'ların değiştiğini görürsün. Bu sayede tek satır TypeScript yazmadan görsel geri bildirim verebilirsin:

input.ng-invalid.ng-touched {
  border: 1px solid crimson;
}

input.ng-valid.ng-dirty {
  border: 1px solid seagreen;
}

Neden iki class birden? Sadece ng-invalid kullansaydın, sayfa açılır açılmaz bütün zorunlu alanlar kırmızı görünürdü. Kullanıcı daha hiçbir şey yapmadan ona hata göstermek kaba kaçar. ng-touched şartı, kırmızıyı kullanıcı alandan çıktıktan sonraya erteler.

Hata Mesajı Göstermek

Kırmızı çerçeve bir şeylerin yanlış olduğunu söyler ama neyin yanlış olduğunu söylemez. Bunun için alan referansını *ngIf ile birlikte kullanırız:

<input name="email" ngModel required email #email="ngModel">
<div *ngIf="email.invalid && (email.dirty || email.touched || f.submitted)" class="error">
  <p *ngIf="email.hasError('required')">E-posta zorunlu.</p>
  <p *ngIf="email.hasError('email')">Geçerli bir e-posta adresi gir.</p>
</div>

Dıştaki koşul mesajın ne zaman görüneceğini belirler: alan geçersizse ve kullanıcı ona dokunduysa ya da formu göndermeyi denediyse. f.submitted, kullanıcı hiçbir alana dokunmadan butona bastığında da hataların görünmesini sağlar. Bunun için butonun kapalı olmaması gerekir; butonu [disabled] ile kapattıysan bu koşula hiç sıra gelmez.

İçteki koşullar hangi kuralın bozulduğuna bakar. Her alanın bir errors nesnesi vardır. Alan geçerliyse bu nesne null olur, değilse bozulan her kural için bir anahtar taşır: { required: true } gibi. hasError bu nesneye güvenli bir şekilde bakmanın en okunaklı yolu. Bazı hatalar ek bilgi de taşır:

<p *ngIf="fullName.hasError('minlength')">
  En az {{ fullName.getError('minlength').requiredLength }} karakter yazmalısın.
</p>

minlength hatası { requiredLength: 3, actualLength: 1 } şeklinde bir nesne taşır. getError bu nesneyi verir, biz de sınırı mesajın içine yazıyoruz. Böylece kuralı değiştirdiğinde mesajı ayrıca güncellemen gerekmez.

ngModelGroup ile Alanları Gruplamak

Adres gibi birbirine ait alanları bir grup altında toplamak hem değeri düzenler hem de grubu tek parça olarak kontrol etmeni sağlar:

<div ngModelGroup="address" #address="ngModelGroup">
  <input name="city" ngModel required placeholder="Şehir">
  <input name="zip" ngModel pattern="[0-9]{5}" placeholder="Posta kodu">
</div>
<p *ngIf="address.invalid && address.touched">Adres bilgilerini kontrol et.</p>

Form gönderildiğinde değer artık iç içe bir nesnedir:

{
  "fullName": "Elif Kaya",
  "email": "elif@example.com",
  "phone": "5321234567",
  "address": {
    "city": "İzmir",
    "zip": "35000"
  }
}

ngModelGroup kendi içindeki alanların durumunu birleştirir. Gruptaki tek bir alan geçersizse grubun tamamı geçersiz sayılır. API'n veriyi zaten bu şekilde bekliyorsa form değerini olduğu gibi gönderebilirsin, ayrıca dönüştürmen gerekmez.

Küçük Bir Deneme

Şimdi sıra sende. Bir etkinlik kayıt formu kur: ad soyad (zorunlu, en az 3 karakter), e-posta (zorunlu, geçerli biçim), telefon (pattern ile 10 hane) ve katılımcı sayısı (type="number", min="1", max="5"). Şehir ve posta kodunu bir ngModelGroup içine al.

Her alan için hata mesajlarını yalnızca kullanıcı alana dokunduktan sonra göster, ng-invalid ve ng-touched class'larıyla çerçeve rengini değiştir. Formun altına {{ f.value | json }} koy ve değerin nasıl oluştuğunu izle. Gönderimden sonra resetForm ile formu temizle. En sonda bir input'un name attribute'unu sil ve konsoldaki hatayı oku.

Fark ettiysen bütün kurallar template'te yaşıyor. Form büyüdükçe, alanlar dinamik olarak eklenip çıkarıldıkça ya da iki alanı birbirine göre kontrol etmek gerektiğinde bu yaklaşım zorlanmaya başlar. Bir sonraki yazıda formun kontrolünü class'a taşıyan reactive form'ları konuşacağız.