Angular forms bekerja dengan sangat baik dengan HTML standar. input, textarea, dan select semuanya dapat masuk ke dalam Reactive Forms tanpa upaya tambahan. Framework ini memahami event, nilai, dan status mereka.
Namun, aplikasi modern jarang bisa mengandalkan elemen standar saja. Anda mungkin membutuhkan widget rating bintang, pemilih tanggal komposit, atau pemilih warna kustom. Masukkan salah satu dari ini ke dalam form group, dan Angular akan menganggapnya sebagai HTML mati. patchValue tidak melakukan apa-apa. Validator mengabaikannya. Form tidak tahu kapan pengguna berinteraksi dengan kontrol tersebut, dan form.disable() membiarkan widget kustom tetap sepenuhnya interaktif.
Inilah masalah yang ingin diselesaikan oleh ControlValueAccessor.
Apa yang Sebenarnya Dilakukan ControlValueAccessor
ControlValueAccessor adalah kontrak yang mengubah komponen kustom menjadi bagian utama dari form. Ia bertindak sebagai penerjemah antara Angular Forms API dan UI Anda sendiri. Setelah Anda mengimplementasikannya dengan benar, komponen Anda tidak dapat dibedakan dari input asli dari sudut pandang form. Komponen tersebut dapat menerima nilai, memancarkan perubahan, melaporkan sentuhan (touches), dan menghormati status nonaktif (disabled) persis seperti elemen bawaan.
Interface ini memerlukan empat metode khusus. Masing-masing menangani arah komunikasi yang berbeda.
writeValue: Form ke Komponen
writeValue(obj) adalah jalur masuk (inbound). Setiap kali model form diperbarui dan perlu mendorong nilai baru ke UI Anda, Angular memanggil metode ini. Jika Anda memanggil patchValue({ rating: 4 }) pada sebuah form group, nilai 4 tersebut sampai ke dalam komponen Anda melalui writeValue. Jika Anda mereset form, writeValue akan menerima nilai awal baru atau null. Tugas Anda di dalam metode ini adalah mengambil data yang masuk tersebut dan memetakannya ke status internal komponen Anda. Jika Anda sedang membangun pemilih warna, writeValue menerima string hex seperti #ff4400, dan Anda harus memperbarui tampilan Anda untuk menunjukkan warna tersebut sebagai yang terpilih.
Ada sedikit kendala praktis di sini. Angular dapat memanggil writeValue sebelum tampilan (view) Anda diinisialisasi sepenuhnya, terutama di dalam komponen yang dirender secara dinamis, dialog, atau antarmuka tab. Jika komponen Anda mencoba menyentuh DOM atau komponen anak terlalu dini, Anda dapat mengalami error saat runtime. Pola yang solid adalah menyimpan nilai tersebut dalam properti lokal dan menerapkannya setelah view diinisialisasi, atau melakukan proteksi terhadap referensi anak yang tidak terdefinisi (undefined). Jangan pernah berasumsi bahwa writeValue hanya dipicu saat template Anda sudah stabil.
registerOnChange: Komponen ke Form
registerOnChange(fn) menyiapkan jalur keluar (outbound). Angular memberikan Anda sebuah fungsi callback, dan Anda harus menyimpan referensi ke fungsi tersebut. Setiap kali pengguna mengubah nilai di dalam komponen Anda, Anda memanggil fungsi tersebut dengan nilai yang baru. Dalam komponen rating bintang, saat pengguna mengklik bintang ketiga, Anda memanggil callback yang tersimpan dengan nilai 3. Panggilan tersebut mengalir kembali ke FormControl, memperbarui model, memicu langganan (subscription) valueChanges apa pun, dan menjalankan ulang validator.
Melewatkan langkah ini adalah cara paling umum untuk merusak form secara diam-diam. Widget mungkin terlihat berfungsi. Pengguna melihat bintang menyala, warna berubah, atau tanggal terisi. Namun, model form tidak pernah diperbarui. Validator terus mengevaluasi data lama (stale data). Handler submit mengirimkan nilai lama. Komponen tampak bekerja, namun form tersebut secara efektif "buta". Jika kontrol kustom Anda menerima input pengguna tetapi form di sekitarnya tidak pernah menyadarinya, inilah penyebabnya.
registerOnTouched: Melaporkan Interaksi
Form tidak hanya melacak nilai. Form juga melacak apakah pengguna telah berinteraksi dengan sebuah field. Angular menggunakan status touched untuk memutuskan kapan waktu yang tepat untuk menampilkan error validasi. Input teks yang wajib diisi (required) tidak seharusnya langsung berkedip merah saat halaman dimuat. Ia harus menunggu sampai pengguna berpindah tab atau mengklik tempat lain.
Input asli menangani hal ini secara otomatis melalui event blur. Komponen kustom tidak. Anda harus menggunakan registerOnTouched(fn) untuk melaporkan interaksi ini sendiri. Angular memberi Anda callback lain; Anda memanggilnya saat Anda memutuskan bahwa pengguna telah berinteraksi secara bermakna dengan kontrol tersebut.
Waktu yang tepat bergantung pada komponen Anda. Untuk input kustom seperti teks, Anda mungkin memanggilnya saat blur. Untuk rating bintang, klik pertama mungkin merupakan saat yang tepat. Untuk pemilih warna yang membuka popover, Anda mungkin menunggu hingga palet ditutup. Kuncinya adalah konsistensi. Jika Anda tidak pernah memanggil callback touched, Angular akan terus menandai kontrol tersebut sebagai pristine. Error validasi tetap tersembunyi bahkan setelah pengguna selesai mengedit. Hal ini menyebabkan kebingungan dan pengalaman pengguna yang buruk.
setDisabledState: Menghormati Perintah Form
Form dinamis terus-menerus mengaktifkan dan menonaktifkan field berdasarkan logika bisnis. Saat Anda memanggil .disable() pada sebuah FormControl, Angular membutuhkan komponen kustom Anda untuk merespons. setDisabledState(isDisabled) menerima nilai boolean. Ketika bernilai true, Anda harus mengunci UI Anda.
Ini berarti lebih dari sekadar mengabaikan klik. Anda harus menonaktifkan tombol internal, menghapus status yang dapat difokuskan (focusable), dan menerapkan perlakuan visual seperti pengurangan opasitas atau pointer-events: none. Jika Anda mengabaikan metode ini, komponen Anda akan tetap sepenuhnya interaktif sementara model form bersikeras bahwa komponen tersebut dinonaktifkan. Hal ini menciptakan bug yang sulit dilacak. Pengguna dapat mengubah nilai yang seharusnya ditolak oleh form. Tombol simpan mungkin aktif berdasarkan status yang tidak valid. Form group dan UI menjadi tidak sinkron.
Kontrol kustom yang dibangun dengan baik memperlakukan setDisabledState sebagai persyaratan utama, bukan sekadar tambahan belakangan.
Kesalahan yang Akan Membuang Waktu Debugging Anda
Beberapa kesalahan berulang sering menjebak pengembang yang baru mengenal interface ini.
Lupa memanggil callback perubahan. Komponen Anda memperbarui status internalnya, tetapi form tidak pernah mengetahuinya. Validator terhenti, dan form induk mengirimkan data usang. Selalu jalankan fungsi onChange yang tersimpan segera setelah pengguna menetapkan nilai baru.
Melewatkan callback touched. Tanpa itu, Angular tidak pernah menandai kontrol sebagai touched. Pesan kesalahan yang terikat pada status touched atau dirty tidak akan muncul. Pengguna akan menatap form yang terlihat benar tetapi tidak bisa dikirim, tanpa indikasi visual tentang apa yang salah.
Mengabaikan status disabled. Kontrol yang secara visual aktif tetapi dianggap dinonaktifkan oleh form menciptakan batas kepercayaan yang rusak. Pengguna dapat terus mengetik atau mengklik, tetapi model mengabaikan mereka. Atau lebih buruk lagi, model secara sporadis menimpa input mereka selama siklus sinkronisasi.
Melewatkan provider NG_VALUE_ACCESSOR. Ini adalah pembunuh senyap. Jika Anda mengimplementasikan keempat metode tersebut tetapi lupa menambahkan NG_VALUE_ACCESSOR ke array providers komponen Anda, Angular tidak akan pernah mendaftarkan komponen Anda sebagai value accessor. Kode berhasil dikompilasi. View berhasil dirender. Namun, tidak ada yang terikat (bind). Tidak ada pesan kesalahan, hanya komponen yang melayang sepenuhnya di luar form. Selalu sertakan dalam metadata decorator.
Signals, Validators, dan Angular Modern
ControlValueAccessor bukanlah permukaan API lama (legacy). Ia menyatu dengan baik ke dalam pengembangan Angular modern. Baik Anda mengelola status internal dengan Signals, properti biasa, atau RxJS subjects, keempat metode tersebut tetap menjadi kontrak publik Anda dengan modul forms. Anda mengonsumsi nilai di writeValue, mengubah Signals atau status Anda, dan memancarkannya melalui callback yang disediakan Angular.
Validator standar bekerja tanpa modifikasi. Validators.required, Validators.min, Validators.pattern, dan validator lintas-field kustom semuanya mengevaluasi komponen berbasis CVA Anda persis seperti mereka mengevaluasi input asli. Form control melihat nilai dan status. Ia tidak peduli apakah nilai tersebut berasal dari kotak teks atau pemilih bulan (month-picker) buatan sendiri.
Portabilitas itulah sebabnya CVA sangat penting bagi design system dan library UI bersama. Satu tim membangun input nomor telepon yang tangguh atau widget unggah file. Mereka mengimplementasikan interface tersebut sekali. Setiap tim lain di organisasi dapat memasukkannya ke dalam Reactive Forms mereka tanpa perlu konfigurasi tambahan. Komponen berperilaku secara terprediksi, melakukan validasi secara seragam, dan menonaktifkan secara konsisten di setiap modul fitur.
Kesimpulan Utama
ControlValueAccessor bukan sekadar interface lain untuk dihafal demi pertanyaan wawancara. Ia adalah jembatan yang memungkinkan komponen kustom Anda berpartisipasi dalam ekosistem form Angular setara dengan elemen HTML asli. Menguasainya berarti memahami seluruh percakapan antara widget Anda dan form: menerima nilai, melaporkan perubahan, mengumumkan status touched, dan menghormati status disabled. Jika Anda melakukan keempat hal ini dengan benar, Anda dapat membangun kontrol form yang kompleks dan dapat digunakan kembali yang terasa transparan bagi pengembang yang menggunakannya. Itulah ciri dari komponen Angular yang profesional.
