Skip to content

Bölüm 8 / 13

Reactive Forms

Form grubu, doğrulama, dinamik form.

11 dakikalık okuma

Formu 8. hatta HTML düzeyinde kurdun: öğeler, yerleşik doğrulama, gönderim. Angular'da form, bileşen sınıfında yaşayan bir modele dönüşür: her alan bir FormControl, form bir FormGroup, doğrulama saf fonksiyon, değer değişimi bir Observable. Bu ders Reactive Forms'u uçtan uca işler — tipli formlar, doğrulama ve hata gösterimi, valueChanges köprüsü, dinamik satırlar ve sunucu hatasını forma geri yazma. Template-driven yaklaşımı da tanıyacaksın; kararı tabloyla vereceğiz.

01Ne öğreneceksin

  • İki yaklaşım: template-driven vs reactive — karar tablosu; ngModel'in gerçek yeri
  • FormControl / FormGroup / FormBuilder; tipli formlar ve nonNullable
  • Doğrulama: yerleşik, özel ve alanlar arası; hata gösteriminin touched disiplini
  • valueChanges — formun RxJS köprüsü: bağımlı alanlar, taslak kaydetme
  • FormArray ile dinamik satırlar
  • Gönderim kalıbı: markAllAsTouched, çift gönderim koruması, sunucu hatasını forma yazma

02İki yaklaşım — ve kararın tablosu

Angular'da form iki türlü kurulur. Template-driven: model şablonda doğar — [(ngModel)]="ad" yazarsın, Angular arka planda görünmez kontroller kurar. Reactive: model sınıfta doğar — kontrolleri sen kurarsın, şablon onlara bağlanır. İkisi de çalışır; fark, formun kimin elinde olduğudur:

Template-driven (ngModel)Reactive (bu dersin konusu)
Modelin yeriŞablonda, örtükSınıfta, açık — tek doğruluk kaynağı
Tip güvenliğiZayıf — şablonda dize dünyasıTipli formlar: değer ve hatalar derleyicice bilinir
DoğrulamaŞablon öznitelikleriyle (required…)Saf fonksiyonlarla — birim testlenebilir
DinamiklikZor — şablonu eğip bükersinDoğal — kontrol ekle/çıkar, FormArray
RxJS erişimiDolaylıvalueChanges/statusChanges doğrudan akış
Doğru işiYerel, tek alanlık durumlar: arama kutusu, tema anahtarıGerçek formlar: kayıt, başvuru, çok adımlı sihirbaz

Karar cümlesi net: ngModel yerel arayüz durumu içindir; doğrulama, gönderim ve iş kuralı taşıyan her form Reactive kurulur. ("Şablon aptal kalır" — 3. dersin kuralı burada da hükmeder: kural sınıfta, çizim şablonda.) Sahada template-driven formlar okuyacaksın; kavramlar bire bir örtüştüğü için bu dersten sonra ikisini de okuyabilir olacaksın.

03Modeli kur: FormBuilder ve tipli formlar

import { FormBuilder, Validators } from "@angular/forms";

export class BasvuruFormu {
  private fb = inject(FormBuilder);

  form = this.fb.nonNullable.group({
    ad:      ["", [Validators.required, Validators.minLength(2)]],
    eposta:  ["", [Validators.required, Validators.email]],
    unvan:   [""],
    maasBeklentisi: [0, Validators.min(0)],
  });
}
<form [formGroup]="form" (ngSubmit)="gonder()">
  <label>Ad <input formControlName="ad"></label>
  <label>E-posta <input formControlName="eposta" type="email"></label>
  <button [disabled]="gonderiliyor">Başvur</button>
</form>

Model artık sınıfta tek doğruluk kaynağıdır: form.value o anki değer, form.valid o anki geçerlilik. Modern Angular'da formlar tiplidir — form.value.ad'ın string olduğunu derleyici bilir, olmayan alan adı derleme hatasıdır. İki incelik kıdem ayırt eder:

  • nonNullable neden var: düz FormControl("")'ün tipi FormControl<string | null>'dur — çünkü reset() varsayılanda kontrolü null'a döndürür. nonNullable, reset'i başlangıç değerine döndürür ve tipten null'u düşürür. Bunu bilmeyen ekip, form değerini kullanan her satırda ?? "" yazarak yaşar.
  • value ile getRawValue() farklı: form.value, devre dışı (disabled) kontrolleri içermez — tipi de bu yüzden tüm alanları isteğe bağlı gösterir. "Devre dışı bıraktığım alan sunucuya gitmiyor" şaşkınlığının cevabı budur; tüm alanlar gerekiyorsa form.getRawValue() kullanılır.

Toptan değer yazmanın iki yolu da bilinsin: setValue tam nesne ister (eksik alan hatadır — güvenli), patchValue kısmî günceller (fazlası sessizce yok sayılır — esnek ama yazım hatasını yutar). Düzenleme ekranı, sunucudan gelen kaydı patchValue ile forma döker.

04Doğrulama ve hata gösterimi

Doğrulayıcı, kontrolü alıp hata nesnesi ya da null döndüren saf fonksiyondur — Validators.required dahil hepsi bu sözleşmededir. Bu yüzden özel doğrulayıcı yazmak sıradan bir iştir:

import { AbstractControl, ValidationErrors } from "@angular/forms";

// Alan düzeyi: kurum e-postası şartı
export function kurumEpostasi(c: AbstractControl): ValidationErrors | null {
  return c.value?.endsWith("@kurum.gov.tr") ? null : { kurumEpostasi: true };
}

// Form (grup) düzeyi: alanlar ARASI kural — şifre tekrarı
export function sifrelerEsit(grup: AbstractControl): ValidationErrors | null {
  return grup.value.sifre === grup.value.sifreTekrar ? null : { sifrelerEsit: true };
}
// kayıt: this.fb.nonNullable.group({ ... }, { validators: [sifrelerEsit] })

Alanlar arası kural grup düzeyine konur — tek alana koyarsan yalnız o alan değişince çalışır, diğer alanın değişimi kuralı tetiklemez; klasik "şifreyi düzelttim ama hata gitmedi" hatasının sebebi budur. Asenkron doğrulayıcılar da vardır (üçüncü parametre): "bu e-postayla başvuru var mı?" sorusunu servise soran, dönene dek kontrolü pending yapan mekanizma — varlığını ve pending durumunu bil.

Hata gösterimi bir UX kuralıdır (8. hattın form derslerinden beri): kullanıcı daha alana dokunmadan kıpkırmızı bir form, azarlanmış hissettirir. Kontrol, bunun için durum bayrakları taşır: touched (alandan çıkıldı mı), dirty (değer değişti mi). Yerleşik kalıp:

<input formControlName="eposta" [attr.aria-invalid]="epostaHatali">
@if (epostaHatali) {
  <p class="alan-hata" role="alert">Geçerli bir e-posta girin.</p>
}

// sınıfta:
get epostaHatali() {
  const c = this.form.controls.eposta;
  return c.invalid && (c.touched || this.gonderimDenendi);
}

Kural: hata, alan terk edildikten ya da gönderim denendikten sonra gösterilir. Hata metni alanın yanında, role="alert" ile ekran okuyucuya da duyurulur — 8. hattın erişilebilirlik disiplini formda da geçerli.

05valueChanges: form bir akıştır

Reactive Forms'un RxJS köprüsü burasıdır: her kontrol ve grup, valueChanges Observable'ı yayınlar. 5. derste kurduğun bütün dağarcık forma uygulanır:

export class BasvuruFormu {
  constructor() {
    // Bağımlı alan: şehir değişince ilçe listesini çek (5-6. derslerin kalıbı)
    this.form.controls.sehir.valueChanges.pipe(
      distinctUntilChanged(),
      switchMap((sehir) => this.adresServisi.ilceler(sehir)),
      takeUntilDestroyed(),                    // sınıf içinde abonelik → disiplin
    ).subscribe((ilceler) => this.ilceler = ilceler);

    // Taslak kaydetme: kullanıcı durunca, değişmişse
    this.form.valueChanges.pipe(
      debounceTime(1000),
      takeUntilDestroyed(),
    ).subscribe((deger) => this.taslakKaydet(deger));
  }
}

Formun valueChanges'i sürekli yaşaması gereken bir akıştır — 5. dersin kuralı gereği, içine HTTP girecekse catchError iç akışa konur; yoksa ilk ağ hatasında ilçe listesi bir daha dolmaz. İkinci incelik: koddan setValue/patchValue çağrıları da valueChanges'i tetikler — taslak yükleyip forma dökerken taslak kaydetmeyi yeniden tetiklememek için patchValue(deger, { emitEvent: false }) seçeneği vardır; döngüye girmiş form akışlarının teşhisinde ilk bakılacak yer budur.

06FormArray: dinamik satırlar

"Referans ekle" düğmesiyle çoğalan satırlar — sabit alanlı grup burada yetmez; kontrol listesi çalışma anında büyüyüp küçülür:

form = this.fb.nonNullable.group({
  ad: ["", Validators.required],
  referanslar: this.fb.array([this.yeniReferans()]),
});

get referanslar() { return this.form.controls.referanslar; }

yeniReferans() {
  return this.fb.nonNullable.group({
    adSoyad: ["", Validators.required],
    telefon: [""],
  });
}
ekle()            { this.referanslar.push(this.yeniReferans()); }
cikar(i: number)  { this.referanslar.removeAt(i); }
<div formArrayName="referanslar">
  @for (ref of referanslar.controls; track ref; let i = $index) {
    <fieldset [formGroupName]="i">
      <input formControlName="adSoyad">
      <input formControlName="telefon">
      <button type="button" (click)="cikar(i)">Çıkar</button>
    </fieldset>
  }
</div>

İki ayrıntı: track ref — kontrol nesnesinin kendisi kimliktir (3. dersin track zorunluluğu; $index ile izlersen satır silmede odak ve değerler kayar, tam da orada gösterilen zarar). Ve içerideki silme/ekleme düğmeleri type="button" taşır — 8. hattan beri bildiğin kural: formdaki düğmenin varsayılan tipi submit'tir, unutursan her "Çıkar" tıklaması formu göndermeye kalkar.

07Gönderim kalıbı: uçtan uca

gonderiliyor = false;

gonder() {
  if (this.form.invalid) {
    this.form.markAllAsTouched();   // gizlenen hataları görünür yap
    return;
  }
  this.gonderiliyor = true;          // buton disabled → çift gönderim kapısı
  const govde = formdanIstege(this.form.getRawValue()); // tek dönüştürücü — 6. ders
  this.servis.basvur(govde).pipe(finalize(() => this.gonderiliyor = false))
    .subscribe({
      next:  ()    => this.router.navigate(["/basvurular"]),
      error: (err) => this.sunucuHatasiniYaz(err),
    });
}

Kalıbın her satırı bir ders taşır:

  • markAllAsTouched(): geçersiz formda gönderime basan kullanıcı, 4. bölümdeki touched disiplini yüzünden hiç hata göremezdi — bu çağrı tüm alanları "dokunulmuş" sayar, saklı hatalar bir anda görünür olur.
  • Çift gönderim: düğmeyi gonderiliyor ile kilitlemek ilk savunmadır (5. dersteki exhaustMap aynı işin akış çözümüydü); asıl güvence yine sunucudadır — istemci kilidi ağ gecikmesinde yeterli olmayabilir.
  • Sunucu hatası forma döner: 6. dersin 400/422 satırı burada kapanır — hata sözleşmesindeki alan hataları kontrol üstüne yazılır: this.form.controls.eposta.setErrors({ sunucu: "Bu e-postayla başvuru var" }); alan kalıbı bu hatayı da diğerleri gibi gösterir. Genel hatalar form üstü tek mesaja düşer.
  • Form değeri istek gövdesi değildir: araya tek dönüştürücü girer (formdanIstege) — 9. hattın sınır ilkesinin çıkış yönü: tarih biçimlenir, alan adları API sözleşmesine eşlenir, arayüze özgü alanlar elenir.
  • Kaydedilmemiş değişiklik: 7. dersin CanDeactivateFn'i formla burada buluşur: form.dirty ise çıkışta kullanıcıya sorulur — yanlış tıklamayla kaybolan yarım başvuru, tasarlanmamış formların klasiğidir.

08Mini alıştırma — Başvuru formu uçtan uca

Yaklaşık 20 dakika · kurulum yok — StackBlitz'te Angular şablonu; gönderimi konsola/sahte servise yap

  1. Formu kur: ad (zorunlu, en az 2), e-posta (zorunlu + Validators.email), şifre + şifre tekrar. nonNullable.group kullan; form.value'nun tipini editörde imleçle incele.
  2. Hata gösterim kalıbını uygula: hatalar yalnız touched ya da gönderim denendiyse görünsün; role="alert" eklemeyi unutma. Alana dokunmadan hata görünmediğini, çıkınca göründüğünü doğrula.
  3. Grup doğrulayıcısı: sifrelerEsit'i yaz ve grup düzeyine bağla. Önce bilerek yanlış yap — yalnız sifreTekrar alanına koy — ve ilk şifreyi değiştirince hatanın güncellenmediğini gör; sonra grup düzeyine taşı.
  4. Reset deneyi: formu doldur, form.reset() çağır. Sonra nonNullable'ı geçici olarak kaldırıp aynı deneyi yap — değerlerin null'a düştüğünü tipin de değiştiğini gör; geri koy.
  5. FormArray ile "referanslar" bölümü ekle: satır ekle/çıkar, track ref, iç düğmelere type="button". Üç satır ekle, ortadakini sil — değerlerin kaymadığını doğrula (sonra track'i $index yapıp kaymayı bir kez gözle — ve geri al).
  6. Gönderim kalıbını tamamla: markAllAsTouched, düğme kilidi ve sahte serviste %50 ihtimalle dönen "e-posta kayıtlı" hatasını setErrors ile e-posta alanına yaz. valueChanges + debounceTime(1000) ile taslağı konsola logla.

09Kendini sına

Template-driven ile Reactive form farkı nedir? Hangisini ne zaman seçersin?

Fark, form modelinin nerede doğduğudur. Template-driven'da model şablonda örtük kurulur: [(ngModel)] yazarsın, Angular arka planda kontrolleri kendisi üretir — hızlı başlar ama model dolaylıdır, tip güvenliği zayıftır, doğrulama şablon özniteliklerine dağılır ve dinamik yapılar (koşullu alanlar, çoğalan satırlar) şablonu eğip bükmeye döner. Reactive'de model sınıfta açıkça kurulur: FormGroup/FormControl tek doğruluk kaynağıdır, şablon ona yalnız bağlanır; tipli formlar sayesinde değer ve alan adları derleyicice denetlenir, doğrulayıcılar saf fonksiyon olduğundan birim testlenir, valueChanges ile RxJS dünyasına doğrudan köprü vardır ve FormArray dinamikliği doğal kılar. Karar kalıbım: ngModel yalnız yerel arayüz durumu için — arama kutusu, tema anahtarı gibi tek alanlık, kuralsız bağlamalar; doğrulama, gönderim ve iş kuralı taşıyan her gerçek form Reactive. Bu, "şablon aptal kalır" ilkesinin form hâlidir: kural sınıfta yaşar, şablon çizer. Sahada template-driven form da okunur — kavramlar örtüştüğü için geçiş zihinsel olarak ucuzdur.

Tipli formlarda nonNullable neden önemli? value ile getRawValue() farkı nedir?

İkisi de tipli formların "kâğıt üstünde küçük, sahada büyük" ayrıntılarıdır. Düz FormControl("")'ün tipi FormControl<string | null>'dur, çünkü reset() varsayılan davranışta kontrolü başlangıç değerine değil null'a döndürür; bu da form değerini tüketen her satıra null kontrolü bulaştırır. fb.nonNullable.group(...) iki şeyi birden düzeltir: reset artık başlangıç değerine döner ve tipten null düşer — form değeri temiz string/number olarak akar. value / getRawValue() ayrımı ise disabled kontrollerle ilgilidir: form.value devre dışı kontrolleri içermez — tipinin tüm alanları isteğe bağlı (Partial) göstermesinin sebebi de budur; "alanı disable ettim, sunucuya gitmez oldu" şaşkınlığı bu davranıştan çıkar. getRawValue() devre dışılar dahil tam değeri, tam tiple verir. Kalıbım: gönderimde getRawValue() + tek dönüştürücü; kısmî doldurmada patchValue (fazla alanı sessizce yutar — esnek), tam doldurmada setValue (eksik alanı hata yapar — güvenli).

Doğrulama katmanlarını anlat: yerleşik, özel, alanlar arası, asenkron. Hata gösteriminin UX kuralı nedir?

Doğrulayıcı sözleşmesi tektir: kontrolü (ya da grubu) alır, geçersizse hata nesnesi, geçerliyse null döndürür — saf fonksiyon olduğu için DOM'suz test edilir. Yerleşikler (required, email, min/maxLength…) bu sözleşmenin hazır örnekleridir; özel doğrulayıcı aynı imzayla yazılır. Alanlar arası kural (şifre tekrarı, tarih aralığı) grup düzeyine konur — tek alana konursa yalnız o alan değişince çalışır ve diğer alan düzeltildiğinde hata güncellenmez; bu, en sık yapılan form hatalarındandır. Asenkron doğrulayıcılar üçüncü katmandır: servise sorar (bu e-posta kayıtlı mı), dönene dek kontrol pending olur — geçerli/geçersiz ikilisinin arasında üçüncü durum bulunduğunu bilmek gerekir. Gösterim kuralı UX'tir: hata, kullanıcı alanı terk ettikten (touched) ya da gönderimi denedikten sonra gösterilir — daha yazmaya başlamadan kızaran form kötü tasarımdır. Gönderime basıldığında markAllAsTouched() saklı hataları görünür yapar; hata metni alanın yanında ve role="alert" ile verilir ki ekran okuyucu da duysun.

valueChanges ne sağlar? Gönderim kalıbını uçtan uca anlat — çift gönderim ve sunucu hatası dahil.

valueChanges, formu RxJS dünyasına bağlayan Observable'dır: her kontrol ve grup, değer değişimlerini akış olarak yayınlar. Bu, 5. dersin bütün dağarcığını forma açar — bağımlı alanlar (sehir.valueChanges → switchMap ile ilçe listesi), taslak kaydetme (debounceTime), canlı özet. İki disiplin: sınıf içi abonelik olduğundan takeUntilDestroyed şarttır; ve akış sürekli yaşayacağı için içindeki HTTP hatası iç akışta yakalanır — yoksa ilk hata bağımlı alanı ölü bırakır. Koddan yapılan setValue/patchValue'nun da akışı tetiklediği bilinmeli; döngü riskinde emitEvent: false vardır. Gönderim kalıbı: form geçersizse markAllAsTouched() + dur; geçerliyse düğme kilitlenir (çift gönderimin istemci savunması — akış tarafındaki karşılığı exhaustMap; asıl güvence sunucudadır), getRawValue() tek dönüştürücüden geçirilip gövdeye çevrilir, istek atılır, finalize kilidi her durumda açar. 400/422 dönerse hata sözleşmesindeki alan hataları setErrors ile ilgili kontrollere yazılır — böylece sunucu hatası da aynı gösterim kalıbından akar; genel hata form üstü tek mesaja düşer. Form dirty iken rota değişimi CanDeactivateFn ile sorgulanır.

10Sınav köşesi

Yazılıda nasıl sorulur

(1) Template-driven ile reactive form arasındaki farklar nelerdir? (2) FormGroup, FormControl, FormBuilder nedir? (3) Tipli formlar ne sağlar; nonNullable neyi değiştirir? (4) setValue ile patchValue farkı nedir? (5) Özel doğrulayıcı nasıl yazılır; alanlar arası doğrulama nereye konur? (6) touched / dirty nedir; hata gösteriminde nasıl kullanılır? (7) valueChanges hangi senaryolarda kullanılır? (8) FormArray ne işe yarar? (9) Çift form gönderimi nasıl engellenir? (10) Sunucu doğrulama hataları forma nasıl yansıtılır?

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Karar — "ngModel yalnız yerel durum; gerçek form Reactive: model sınıfta tek doğruluk kaynağı, şablon bağlanır — kural sınıfta, çizim şablonda." (2) Tip — "nonNullable ile kurarım: reset başlangıca döner, tipten null düşer; gönderimde getRawValue — value disabled alanları içermez." (3) Doğrulama — "Doğrulayıcı saf fonksiyondur, test edilir; alanlar arası kural grup düzeyine; hata touched'dan sonra, role=alert ile." (4) Akış — "valueChanges RxJS köprüsüdür: bağımlı alanda switchMap, taslakta debounceTime; takeUntilDestroyed ve iç akışta catchError disiplinleriyle." (5) Kıdem vuruşu — "Gönderim kalıbım: markAllAsTouched → kilit → tek dönüştürücü → finalize; 422'yi setErrors ile forma geri yazarım; dirty formda CanDeactivate sorarım; FormArray'de track kontrol nesnesidir, $index değil."