Angular formları standart HTML ile harika bir şekilde çalışır. input, textarea ve select bileşenlerinin tamamı, ekstra bir çaba gerektirmeden Reactive Forms yapısına dahil olur. Framework; bunların olaylarını (events), değerlerini ve durumlarını (states) anlar.

Ancak modern uygulamalar nadiren sadece standart elementlerle idare edebilir. Bir yıldız puanlama widget'ına, bileşik bir tarih seçiciye veya özel bir renk seçiciye (color picker) ihtiyaç duyabilirsiniz. Bunlardan birini bir form grubuna yerleştirdiğinizde, Angular onu işlevsiz bir HTML olarak görür. patchValue hiçbir şey yapmaz. Validator'lar onu görmezden gelir. Form, kullanıcının kontrolle ne zaman etkileşime girdiğinden haberdar olmaz ve form.disable() komutu özel widget'ı tamamen etkileşimli bırakır.

İşte ControlValueAccessor tam olarak bu sorunu çözmek için vardır.

ControlValueAccessor Aslında Ne Yapar?

ControlValueAccessor, özel bir bileşeni (component) birinci sınıf bir form öğesine dönüştüren sözleşmedir (contract). Angular Forms API ile kendi kullanıcı arayüzünüz (UI) arasında bir çevirmen görevi görür. Onu doğru şekilde uyguladığınızda, bileşeniniz formun bakış açısına göre yerel (native) bir input'tan ayırt edilemez hale gelir. Yerleşik bir element gibi değerleri alabilir, değişiklikleri yayabilir (emit), dokunma (touch) durumlarını bildirebilir ve devre dışı (disabled) durumlarına uyum sağlayabilir.

Arayüz dört belirli metod gerektirir. Her biri iletişimin farklı bir yönünü yönetir.

writeValue: Form'dan Bileşene

writeValue(obj) gelen veri yoludur. Form modeli her güncellendiğinde ve UI'ınıza yeni bir değer göndermesi gerektiğinde, Angular bu metodu çağırır. Bir form grubu üzerinde patchValue({ rating: 4 }) komutunu çalıştırırsanız, 4 değeri writeValue aracılığıyla bileşeninizin içine ulaşır. Formu sıfırlarsanız, writeValue yeni başlangıç değerini veya null değerini alır. Bu metod içindeki göreviniz, gelen bu veriyi alıp bileşeninizin dahili durumuna (internal state) eşlemektir. Eğer bir renk seçici yapıyorsanız, writeValue #ff4400 gibi bir hex dizisi alır ve siz de seçili rengi göstermek için görünümünüzü (view) güncellemelisiniz.

Burada pratik bir zorluk bulunmaktadır. Angular, özellikle dinamik olarak oluşturulan bileşenlerde, diyaloglarda veya sekmeli arayüzlerde, görünümünüz (view) tam olarak başlatılmadan önce writeValue metodunu çağırabilir. Eğer bileşeniniz DOM'a veya alt bileşenlere çok erken erişmeye çalışırsa, çalışma zamanı (runtime) hatalarıyla karşılaşabilirsiniz. Sağlam bir yöntem, değeri yerel bir özellik (property) içinde saklamak ve görünüm başlatıldıktan sonra uygulamak veya tanımsız (undefined) alt referanslara karşı önlem almaktır. writeValue metodunun yalnızca şablonunuz (template) kararlı hale geldiğinde çalışacağını asla varsaymayın.

registerOnChange: Bileşenden Form'a

registerOnChange(fn) giden veri yolunu kurar. Angular size bir geri çağırma (callback) fonksiyonu verir ve sizin bu fonksiyona bir referans tutmanız gerekir. Kullanıcı bileşeninizdeki değeri her değiştirdiğinde, o fonksiyonu yeni değerle birlikte çağırırsınız. Bir yıldız puanlama bileşeninde, kullanıcı üçüncü yıldıza tıkladığında, saklanan callback fonksiyonunu 3 değeriyle çağırırsınız. Bu çağrı FormControl'e geri döner, modeli günceller, tüm valueChanges aboneliklerini tetikler ve validator'ları yeniden çalıştırır.

Bu adımı atlamak, bir formu sessizce bozmanın en yaygın yoludur. Widget canlı görünebilir. Kullanıcı yıldızların parladığını, renklerin değiştiğini veya tarihlerin dolduğunu görür. Ancak form modeli asla güncellenmez. Validator'lar güncel olmayan verileri değerlendirmeye devam eder. Gönderim (submit) işleyicileri eski değerleri gönderir. Bileşen çalışıyor gibi görünür ancak form aslında kördür. Eğer özel kontrolünüz kullanıcı girişini kabul ediyor ama çevresindeki form bunu hiç fark etmiyorsa, suçlu neredeyse her zaman budur.

registerOnTouched: Etkileşimi Bildirme

Formlar sadece değerleri takip etmez. Bir kullanıcının bir alanla etkileşime girip girmediğini de takip ederler. Angular, doğrulama hatalarını ne zaman göstermenin uygun olduğuna karar vermek için "touched" durumunu kullanır. Zorunlu (required) bir metin girişi, sayfa yüklenir yüklenmez kırmızı yanıp sönmemelidir. Kullanıcı başka bir yere geçene (tab) veya başka bir yere tıklayana kadar beklemelidir.

Yerel (native) inputlar bunu blur olayları aracılığıyla otomatik olarak halleder. Özel bileşenler halletmez. Bu etkileşimleri kendiniz bildirmek için registerOnTouched(fn) metodunu kullanmalısınız. Angular size başka bir callback verir; kullanıcının kontrolle anlamlı bir şekilde etkileşime girdiğine karar verdiğinizde bunu çağırırsınız.

Tam zamanlama bileşeninize bağlıdır. Metin benzeri özel bir input için bunu blur anında çağırabilirsiniz. Yıldız puanlama için muhtemelen doğru an ilk tıklamadır. Bir popover açan renk seçici için palet kapanana kadar bekleyebilirsiniz. Anahtar nokta tutarlılıktır. Eğer "touched" callback'ini asla çağırmazsanız, Angular kontrolü pristine olarak işaretlemeye devam eder. Kullanıcı düzenlemeyi açıkça bitirmiş olsa bile doğrulama hataları gizli kalır. Bu da kafa karışıklığına ve kötü bir kullanıcı deneyimine yol açar.

setDisabledState: Form Komutlarına Uymak

Dinamik formlar, iş mantığına bağlı olarak alanları sürekli olarak etkinleştirir veya devre dışı bırakır. Bir FormControl üzerinde .disable() metodunu çağırdığınızda, Angular özel bileşeninizin buna yanıt vermesine ihtiyaç duyar. setDisabledState(isDisabled) bir boolean değer alır. Bu değer true olduğunda, kullanıcı arayüzünüzü (UI) kilitlemelisiniz.

Bu, sadece tıklamaları görmezden gelmekten daha fazlasını ifade eder. Dahili butonları devre dışı bırakmalı, odaklanılabilir (focusable) durumları kaldırmalı ve düşük opaklık veya pointer-events: none gibi görsel düzenlemeler uygulamalısınız. Eğer bu metodu görmezden gelirseniz, form modeli bileşenin devre dışı olduğunu iddia ederken bileşeniniz tamamen etkileşimli kalmaya devam eder. Bu durum, takibi zor hatalara (bug) yol açar. Kullanıcılar, formun reddetmesi gereken değerleri değiştirebilir. Kaydet butonları geçersiz durumlara dayanarak etkinleşebilir. Form grubu ile kullanıcı arayüzü birbirinden kopar.

İyi yapılandırılmış bir özel kontrol (custom control), setDisabledState metodunu sonradan akla gelen bir şey olarak değil, birinci sınıf bir gereksinim olarak ele alır.

Size Hata Ayıklama Süresi Kaybettirecek Hatalar

Bu arayüze yeni başlayan geliştiricilerin düştüğü birkaç tekrarlayan hata vardır.

Değişiklik geri bildirimini (change callback) çağırmayı unutmak. Bileşeniniz dahili durumunu günceller ancak form bundan asla haberdar olmaz. Doğrulayıcılar (validators) duraksar ve üst formlar güncel olmayan (stale) verileri gönderir. Kullanıcı yeni bir değer girdiğinde, saklanan onChange fonksiyonunu her zaman çalıştırın.

Dokunuldu geri bildirimini (touched callback) atlamak. Bu olmadan Angular, kontrolü asla "touched" olarak işaretlemez. touched veya dirty durumlarına bağlı hata mesajları görünmez. Kullanıcılar, doğru görünen ancak gönderilemeyen ve neyin yanlış olduğuna dair görünür bir belirti olmayan bir forma bakakalır.

Devre dışı durumunu ihmal etmek. Formun devre dışı olduğunu düşündüğü ancak görsel olarak etkin görünen bir kontrol, bozulmuş bir güven sınırı oluşturur. Kullanıcı yazmaya veya tıklamaya devam edebilir ancak model onları görmezden gelir. Ya da daha kötüsü, model senkronizasyon döngüleri sırasında kullanıcının girişini rastgele geçersiz kılar (overwrites).

NG_VALUE_ACCESSOR sağlayıcısını (provider) atlamak. Bu, sessiz katildir. Dört metodu da uygular ancak NG_VALUE_ACCESSOR'ı bileşeninizin providers dizisine eklemeyi unutursanız, Angular bileşeninizi asla bir değer erişimcisi (value accessor) olarak kaydetmez. Kod derlenir. Görünüm (view) işlenir. Ancak hiçbir şey bağlanmaz (bind). Bir hata mesajı gelmez; sadece formun tamamen dışında yüzen bir bileşeniniz olur. Onu her zaman dekoratör meta verisine (decorator metadata) dahil edin.

Signals, Validators ve Modern Angular

ControlValueAccessor eski bir API yüzeyi değildir. Modern Angular geliştirmesine tam uyum sağlar. Dahili durumu Signals, düz özellikler (plain properties) veya RxJS subject'leri ile yönetiyor olun, bu dört metot form modülü ile olan genel sözleşmeniz (public contract) olmaya devam eder. Değerleri writeValue içinde tüketir, Signals veya durumunuzu değiştirir ve Angular'ın sağladığı geri bildirimler (callbacks) aracılığıyla yayınlarsınız.

Standart doğrulayıcılar (validators) herhangi bir değişiklik gerektirmeden çalışır. Validators.required, Validators.min, Validators.pattern ve özel alanlar arası (cross-field) doğrulayıcıların tümü, CVA tabanlı bileşeninizi tıpkı yerel bir input gibi değerlendirir. Form kontrolü bir değer ve bir durum görür. Bu değerin bir metin kutusundan mı yoksa özel yapım bir ay seçiciden (month-picker) mi geldiğiyle ilgilenmez.

Bu taşınabilirlik, CVA'nın tasarım sistemleri ve paylaşılan UI kütüphaneleri için neden önemli olduğunun cevabıdır. Bir ekip sağlam bir telefon numarası girişi veya bir dosya yükleme widget'ı oluşturur. Arayüzü bir kez uygularlar. Organizasyondaki diğer tüm ekipler, hiçbir ek bağlantı gerektirmeden bunu kendi Reactive Forms yapılarına dahil eder. Bileşen her özellik modülünde öngörülebilir şekilde davranır, tek tip doğrular ve tutarlı bir şekilde devre dışı kalır.

Asıl Çıkarılması Gereken Ders

ControlValueAccessor sadece mülakat soruları için ezberlenmesi gereken başka bir arayüz değildir. Özel bileşenlerinizin, Angular'ın form ekosistemine yerel HTML öğeleriyle eşit düzeyde katılmasını sağlayan köprüdür. Bunda ustalaşmak; widget'ınız ile form arasındaki tüm iletişimi anlamak demektir: değerleri almak, değişiklikleri bildirmek, dokunulmaları duyurmak ve devre dışı durumlarına uymak. Bu dört parçayı doğru yaparsanız, onları kullanan geliştiriciler için "görünmez" hissettiren karmaşık ve yeniden kullanılabilir form kontrolleri oluşturabilirsiniz. Bu, profesyonel bir Angular bileşeninin işaretidir.