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 venonNullable- Doğrulama: yerleşik, özel ve alanlar arası; hata gösteriminin
toucheddisiplini valueChanges— formun RxJS köprüsü: bağımlı alanlar, taslak kaydetmeFormArrayile 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ük | Sınıfta, açık — tek doğruluk kaynağı |
| Tip güvenliği | Zayıf — şablonda dize dünyası | Tipli formlar: değer ve hatalar derleyicice bilinir |
| Doğrulama | Şablon öznitelikleriyle (required…) | Saf fonksiyonlarla — birim testlenebilir |
| Dinamiklik | Zor — şablonu eğip bükersin | Doğal — kontrol ekle/çıkar, FormArray |
| RxJS erişimi | Dolaylı | valueChanges/statusChanges doğrudan akış |
| Doğru işi | Yerel, 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:
nonNullableneden var: düzFormControl("")'ün tipiFormControl<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 tiptennull'u düşürür. Bunu bilmeyen ekip, form değerini kullanan her satırda?? ""yazarak yaşar.valueilegetRawValue()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 gerekiyorsaform.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ümdekitoucheddisiplini 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
gonderiliyorile kilitlemek ilk savunmadır (5. derstekiexhaustMapaynı 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.dirtyise çı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
- Formu kur: ad (zorunlu, en az 2), e-posta (zorunlu +
Validators.email), şifre + şifre tekrar.nonNullable.groupkullan;form.value'nun tipini editörde imleçle incele. - Hata gösterim kalıbını uygula: hatalar yalnız
touchedya 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. - Grup doğrulayıcısı:
sifrelerEsit'i yaz ve grup düzeyine bağla. Önce bilerek yanlış yap — yalnızsifreTekraralanına koy — ve ilk şifreyi değiştirince hatanın güncellenmediğini gör; sonra grup düzeyine taşı. - Reset deneyi: formu doldur,
form.reset()çağır. SonranonNullable'ı geçici olarak kaldırıp aynı deneyi yap — değerlerinnull'a düştüğünü tipin de değiştiğini gör; geri koy. FormArrayile "referanslar" bölümü ekle: satır ekle/çıkar,track ref, iç düğmeleretype="button". Üç satır ekle, ortadakini sil — değerlerin kaymadığını doğrula (sonratrack'i$indexyapıp kaymayı bir kez gözle — ve geri al).- Gönderim kalıbını tamamla:
markAllAsTouched, düğme kilidi ve sahte serviste %50 ihtimalle dönen "e-posta kayıtlı" hatasınısetErrorsile 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."
