Arsitektur form Vue 3 untuk aplikasi bisnis: pola yang dapat digunakan ulang
Arsitektur form Vue 3 untuk aplikasi bisnis: komponen field dapat digunakan ulang, aturan validasi yang jelas, dan cara praktis menampilkan error server di setiap input.

Mengapa kode form rusak di aplikasi bisnis nyata
Form di aplikasi bisnis jarang tetap kecil. Awalnya hanya “beberapa input”, lalu tumbuh jadi puluhan field, bagian kondisional, izin, dan aturan yang harus selaras dengan logika backend. Setelah beberapa perubahan produk, form masih bekerja, tapi kodenya mulai terasa rapuh.
Arsitektur form Vue 3 penting karena form adalah tempat tumpukan "perbaikan cepat": satu watcher lagi, satu kasus khusus lagi, satu komponen yang disalin lagi. Hari ini masih berjalan, tapi semakin sulit dipercaya dan diubah.
Tanda-tandanya sudah familier: perilaku input terulang di banyak halaman (label, format, penanda wajib, hint), penempatan error yang tidak konsisten, aturan validasi tersebar di banyak komponen, dan error backend disederhanakan jadi toast generik yang tidak memberi tahu pengguna apa yang harus diperbaiki.
Ketidakkonsistenan itu bukan sekadar gaya kode. Mereka berubah menjadi masalah UX: orang mengirim ulang form, tiket support meningkat, dan tim menghindari menyentuh form karena khawatir sesuatu rusak di edge case tersembunyi.
Setup yang baik membuat form menjadi membosankan dengan cara terbaik. Dengan struktur yang dapat diprediksi, Anda bisa menambah field, mengubah aturan, dan menangani respons server tanpa merombak semuanya.
Anda butuh sistem form yang memberi reuse (satu field berperilaku sama di mana pun), kejelasan (aturan dan penanganan error mudah ditinjau), perilaku yang dapat diprediksi (touched, dirty, reset, submit), dan umpan balik yang lebih baik (error server muncul di input yang tepat). Pola di bawah fokus pada komponen field yang dapat digunakan ulang, validasi yang terbaca, dan pemetaan error server ke input spesifik.
Model mental sederhana untuk struktur form
Form yang tahan lama adalah sistem kecil dengan bagian yang jelas, bukan tumpukan input.
Pikirkan dalam empat lapisan yang saling berkomunikasi searah: UI mengumpulkan input, state form menyimpannya, validasi menjelaskan yang salah, dan lapisan API memuat dan menyimpan.
Empat lapisan (dan apa yang masing-masing tangani)
- Komponen Field UI: merender input, label, hint, dan teks error. Mengemisi perubahan nilai.
- State Form: menyimpan values dan errors (plus flag touched dan dirty).
- Aturan Validasi: fungsi murni yang membaca values dan mengembalikan pesan error.
- Panggilan API: memuat data awal, mengirim perubahan, dan menerjemahkan respons server menjadi error field.
Pemecahan tanggung jawab ini menjaga perubahan tetap terlokalisir. Ketika kebutuhan baru datang, Anda memperbarui satu lapisan tanpa merusak yang lain.
Apa yang harus ada di field vs form induk
Komponen field yang dapat digunakan ulang harus bersifat ‘membosankan’. Ia tidak perlu tahu tentang API Anda, model data, atau aturan validasi. Ia hanya menampilkan nilai dan menunjukkan error.
Form induk mengoordinasikan sisanya: field apa yang ada, di mana values disimpan, kapan memvalidasi, dan bagaimana submit dilakukan.
Aturan sederhana membantu: jika logika bergantung pada field lain (misalnya, "State" wajib hanya ketika "Country" adalah US), letakkan itu di form induk atau lapisan validasi, bukan di dalam komponen field.
Jika menambah field benar-benar mudah, biasanya Anda hanya menyentuh default atau skema, markup tempat field diletakkan, dan aturan validasi field itu. Jika menambah satu input memaksa perubahan di komponen yang tidak berkaitan, batasan Anda kabur.
Komponen field dapat digunakan ulang: apa yang distandarisasi
Saat form tumbuh, kemenangan tercepat adalah berhenti membangun setiap input seolah satu-satunya. Komponen field harus terasa dapat diprediksi. Itu yang membuatnya cepat digunakan dan mudah ditinjau.
Sekumpulan blok bangunan praktis:
- BaseField: pembungkus untuk label, hint, teks error, spasi, dan atribut aksesibilitas.
- Komponen Input: TextInput, SelectInput, DateInput, Checkbox, dan seterusnya. Masing-masing fokus pada kontrol.
- FormSection: mengelompokkan field terkait dengan judul, teks bantuan singkat, dan spasi konsisten.
Untuk props, jaga supaya set-nya kecil dan diterapkan di mana-mana. Mengganti nama prop di 40 form itu menyakitkan.
Yang biasanya langsung terasa manfaat:
modelValuedanupdate:modelValueuntukv-modellabelrequireddisablederror(pesan tunggal, atau array jika Anda suka)hint
Slots adalah tempat Anda memberi fleksibilitas tanpa merusak konsistensi. Pertahankan tata letak BaseField stabil, tapi izinkan variasi kecil seperti aksi di sisi kanan ("Kirim kode") atau ikon di depan. Jika variasi muncul lebih dari sekali, buat itu menjadi slot daripada mem-fork komponen.
Standarisasi urutan render (label, kontrol, hint, error). Pengguna memindai lebih cepat, tes lebih sederhana, dan pemetaan error server menjadi mudah karena setiap field punya satu tempat jelas untuk menampilkan pesan.
State form: values, touched, dirty, dan reset
Sebagian besar bug form di aplikasi bisnis bukan soal input. Mereka muncul dari state yang tersebar: values di satu tempat, errors di tempat lain, dan tombol reset yang hanya bekerja setengahnya. Arsitektur form Vue 3 yang bersih dimulai dengan bentuk state yang konsisten.
Pertama, pilih skema penamaan untuk key field dan patuhi itu. Aturan paling sederhana: key field sama dengan key payload API. Jika server mengharapkan first_name, key form Anda juga first_name. Pilihan kecil ini membuat validasi, penyimpanan, dan pemetaan error server jauh lebih mudah.
Simpan state form Anda di satu tempat (sebuah composable, store Pinia, atau komponen induk), dan biarkan setiap field membaca serta menulis melalui state itu. Struktur flat bekerja untuk sebagian besar layar. Gunakan nested hanya ketika API Anda memang nested.
const state = reactive({
values: { first_name: '', last_name: '', email: '' },
touched: { first_name: false, last_name: false, email: false },
dirty: { first_name: false, last_name: false, email: false },
errors: { first_name: '', last_name: '', email: '' },
defaults: { first_name: '', last_name: '', email: '' }
})
Cara praktis memikirkan flag:
touched: apakah pengguna sudah berinteraksi dengan field ini?dirty: apakah nilainya berbeda dari default (atau nilai terakhir yang disimpan)?errors: pesan apa yang harus dilihat pengguna sekarang?defaults: ke mana kita mereset?
Perilaku reset harus bisa diprediksi. Ketika memuat record yang ada, setel kedua values dan defaults dari sumber yang sama. Lalu reset() bisa menyalin defaults kembali ke values, mengosongkan touched, dirty, dan errors.
Contoh: form profil pelanggan memuat email dari server. Jika pengguna mengubahnya, dirty.email menjadi true. Jika mereka klik Reset, email kembali ke nilai yang dimuat (bukan string kosong), dan layar kembali bersih.
Aturan validasi yang tetap terbaca
Validasi yang terbaca lebih soal cara Anda mengekspresikan aturan daripada library yang digunakan. Jika Anda bisa melirik sebuah field dan memahami aturannya dalam beberapa detik, kode form Anda tetap mudah dirawat.
Pilih gaya aturan yang bisa Anda pertahankan
Kebanyakan tim memilih salah satu pendekatan ini:
- Aturan per-field: aturan hidup dekat penggunaan field. Mudah dipindai, cocok untuk form kecil sampai menengah.
- Aturan berbasis skema: aturan berada dalam satu objek atau file. Bagus saat banyak layar memakai model yang sama.
- Hibrida: aturan sederhana dekat field, aturan bersama atau kompleks di skema pusat.
Apapun yang dipilih, jaga nama aturan dan pesan agar bisa diprediksi. Beberapa aturan umum (required, length, format, range) lebih berguna daripada daftar panjang helper satu-kali.
Tulis aturan seperti bahasa sehari-hari
Aturan yang baik terbaca seperti kalimat: "Email wajib dan harus terlihat seperti email." Hindari satu-liner cerdas yang menyembunyikan maksud.
Untuk kebanyakan form bisnis, mengembalikan satu pesan per field sekaligus (kegagalan pertama) menjaga UI tenang dan membantu pengguna memperbaiki lebih cepat.
Aturan umum yang ramah pengguna:
- Required hanya ketika pengguna memang harus mengisi field.
- Length dengan angka nyata (misalnya 2 sampai 50 karakter).
- Format untuk email, telepon, ZIP, tanpa regex berlebihan yang menolak input valid.
- Range seperti "tanggal tidak di masa depan" atau "kuantitas antara 1 dan 999."
Buat pengecekan async terlihat jelas
Validasi async (mis. "username sudah dipakai") membingungkan jika dipicu diam-diam.
Trigger cek pada blur atau setelah jeda singkat, tampilkan status "Checking..." yang jelas, dan batalkan atau abaikan permintaan usang saat pengguna terus mengetik.
Tentukan kapan validasi dijalankan
Waktu sama pentingnya dengan aturan. Setup yang ramah pengguna biasanya:
- On change untuk field yang mendapat manfaat dari umpan balik langsung (mis. kekuatan password), tapi gunakan secukupnya.
- On blur untuk kebanyakan field, sehingga pengguna bisa mengetik tanpa diserang error terus-menerus.
- On submit untuk keseluruhan form sebagai safety net terakhir.
Memetakan error server ke input yang tepat
Pengecekan sisi klien hanya sebagian cerita. Di aplikasi bisnis, server menolak penyimpanan karena aturan yang browser tidak tahu: duplikat, pemeriksaan izin, data usang, perubahan status, dan lain-lain. UX yang baik tergantung pada mengubah respons itu menjadi pesan yang jelas di sebelah input yang tepat.
Normalisasikan error ke satu bentuk internal
Backend jarang sepakat soal format error. Ada yang mengembalikan objek tunggal, ada yang daftar, ada yang map nested keyed by field name. Ubah apa pun yang Anda dapat menjadi satu bentuk internal yang bisa dirender form Anda.
// what your form code consumes
{
fieldErrors: { "email": ["Already taken"], "address.street": ["Required"] },
formErrors: ["You do not have permission to edit this customer"]
}
Tetap pegang beberapa aturan:
- Simpan field errors sebagai array (bahkan jika hanya satu pesan).
- Konversikan gaya path yang berbeda menjadi satu gaya (notasi titik bekerja baik:
address.street). - Simpan non-field errors terpisah sebagai
formErrors. - Simpan payload server mentah untuk logging, tapi jangan render langsung.
Pemetaan path server ke key field Anda
Bagian sulit adalah menyelaraskan "path" versi server dengan key field di form Anda. Tentukan key untuk setiap komponen field (misalnya, email, profile.phone, contacts.0.type) dan patuhi itu.
Lalu buat mapper kecil yang menangani kasus umum:
address.street(notasi titik)address[0].street(bracket untuk array)/address/street(gaya JSON Pointer)
Setelah normalisasi, <Field name="address.street" /> seharusnya bisa membaca fieldErrors["address.street"] tanpa kasus khusus.
Dukung alias jika perlu. Jika backend mengembalikan customer_email tapi UI Anda memakai email, simpan peta seperti { customer_email: "email" } saat normalisasi.
Error field, error tingkat form, dan pemfokusan
Tidak semua error cocok untuk satu input. Jika server bilang "Batas paket tercapai" atau "Pembayaran diperlukan", tampilkan itu di atas form sebagai pesan tingkat form.
Untuk error spesifik field, tampilkan pesan di samping input dan arahkan pengguna ke masalah pertama:
- Setelah menyetel error server, temukan key pertama di
fieldErrorsyang ada di form yang sedang dirender. - Scroll ke situ dan fokuskan (menggunakan ref per field dan
nextTick). - Hapus error server untuk field itu ketika pengguna mengedit field tersebut lagi.
Langkah demi langkah: menyatukan arsitektur
Form tetap tenang ketika Anda memutuskan sejak awal apa yang menjadi tanggung jawab state form, UI, validasi, dan API, lalu menghubungkannya dengan beberapa fungsi kecil.
Urutan yang bekerja untuk sebagian besar aplikasi bisnis:
- Mulai dengan satu model form dan key field yang stabil. Key-key itu menjadi kontrak antar komponen, validator, dan error server.
- Buat satu pembungkus BaseField untuk label, teks bantuan, tanda wajib, dan tampilan error. Jaga komponen input kecil dan konsisten.
- Tambahkan lapisan validasi yang bisa menjalankan per-field dan bisa memvalidasi semuanya pada submit.
- Kirim ke API. Jika gagal, terjemahkan error server ke
{ [fieldKey]: message }sehingga input yang tepat menampilkan pesan yang sesuai. - Jaga penanganan sukses terpisah (reset, toast, navigasi) agar tidak bocor ke komponen dan validator.
Titik awal state yang sederhana:
const values = reactive({ email: '', name: '', phone: '' })
const touched = reactive({ email: false, name: false, phone: false })
const errors = reactive({}) // { email: '...', name: '...' }
BaseField Anda menerima label, error, dan mungkin touched, lalu merender pesan di satu tempat. Setiap komponen input hanya khawatir tentang binding dan mengemisi pembaruan.
Untuk validasi, letakkan aturan dekat model menggunakan key yang sama:
const rules = {
email: v => (!v ? 'Email is required' : /@/.test(v) ? '' : 'Enter a valid email'),
name: v => (v.length < 2 ? 'Name is too short' : ''),
}
function validateAll() {
Object.keys(rules).forEach(k => {
const msg = rules[k](values[k])
if (msg) errors[k] = msg
else delete errors[k]
touched[k] = true
})
return Object.keys(errors).length === 0
}
Saat server merespons dengan error, petakan menggunakan key yang sama. Jika API mengembalikan { "field": "email", "message": "Already taken" }, set errors.email = 'Already taken' dan tandai sebagai touched. Jika error bersifat global (mis. "permission denied"), tampilkan di atas form.
Skenario contoh: mengedit profil pelanggan
Bayangkan layar admin internal di mana agen support mengedit profil pelanggan. Form punya empat field: name, email, phone, dan role (Customer, Manager, Admin). Ukurannya kecil, tapi menunjukkan isu umum.
Aturan sisi-klien harus jelas:
- Name: wajib, panjang minimal.
- Email: wajib, format email valid.
- Phone: opsional, tapi jika diisi harus sesuai format yang diterima.
- Role: wajib, dan kadang kondisional (hanya pengguna dengan izin tertentu bisa menetapkan Admin).
Kontrak komponen yang konsisten membantu: setiap field menerima nilai saat ini, teks error saat ini (jika ada), dan beberapa boolean seperti touched dan disabled. Label, penanda wajib, spasi, dan gaya error tidak boleh ditemukan ulang di setiap layar.
Sekarang alur UX. Agen mengubah email, menekan tab, dan melihat pesan inline di bawah Email jika formatnya salah. Mereka memperbaiki, klik Save, dan server merespons:
- email already exists: tampilkan di bawah Email dan fokus pada field itu.
- phone invalid: tampilkan di bawah Phone.
- permission denied: tampilkan satu pesan tingkat form di bagian atas.
Jika Anda menjaga error dikunci pada nama field (email, phone, role), pemetaan menjadi sederhana. Error field mendarat di samping input, error tingkat form mendarat di area pesan khusus.
Kesalahan umum dan cara menghindarinya
Jaga logika di satu tempat
Menyalin aturan validasi ke setiap layar terasa cepat sampai kebijakan berubah (aturan password, ID pajak wajib, domain yang diperbolehkan). Simpan aturan terpusat (skema, file aturan, fungsi bersama), dan biarkan form menggunakan set aturan yang sama.
Juga hindari membiarkan input level-rendah melakukan terlalu banyak hal. Jika <TextField> Anda tahu cara memanggil API, retry saat gagal, dan mem-parse payload error server, ia berhenti bisa digunakan ulang. Komponen field hanya harus merender, mengemisi perubahan nilai, dan menampilkan error. Taruh pemanggilan API dan logika pemetaan di container form atau composable.
Gejala Anda mencampur concern:
- Pesan validasi yang sama ditulis di banyak tempat.
- Komponen field mengimpor klien API.
- Mengganti satu endpoint merusak beberapa form yang tidak terkait.
- Tes harus mount setengah aplikasi hanya untuk memeriksa satu input.
Perangkap UX dan aksesibilitas
Banner error tunggal seperti "Something went wrong" tidak cukup. Orang perlu tahu field mana yang salah dan apa yang harus dilakukan selanjutnya. Gunakan banner untuk kegagalan global (jaringan turun, izin ditolak), dan petakan error server ke input spesifik agar pengguna bisa memperbaikinya dengan cepat.
Masalah loading dan double-submit menciptakan keadaan membingungkan. Saat submit, nonaktifkan tombol submit, nonaktifkan field yang tidak boleh berubah saat penyimpanan, dan tampilkan state sibuk yang jelas. Pastikan reset dan cancel mengembalikan form dengan bersih.
Dasar aksesibilitas mudah dilewatkan dengan komponen kustom. Beberapa pilihan mencegah masalah nyata:
- Setiap input memiliki label terlihat (bukan hanya placeholder).
- Error terhubung ke field dengan atribut aria yang tepat.
- Fokus berpindah ke field pertama yang tidak valid setelah submit.
- Field yang disable benar-benar non-interaktif dan diumumkan dengan benar.
- Navigasi keyboard bekerja end-to-end.
Daftar periksa cepat dan langkah selanjutnya
Sebelum merilis form baru, jalankan daftar periksa cepat. Ini menangkap celah kecil yang berubah jadi tiket support nantinya.
- Apakah setiap field memiliki key stabil yang cocok dengan payload dan respons server (termasuk path nested seperti
billing.address.zip)? - Dapatkah Anda merender setiap field menggunakan API komponen field yang konsisten (nilai masuk, event keluar, error dan hint masuk)?
- Saat submit, apakah Anda memvalidasi sekali, memblokir double submit, dan memfokuskan field pertama yang tidak valid agar pengguna tahu harus mulai dari mana?
- Dapatkah Anda menampilkan error di tempat yang tepat: per field (di samping input) dan di tingkat form (pesan umum bila diperlukan)?
- Setelah sukses, apakah Anda mereset state dengan benar (values, touched, dirty) sehingga edit berikutnya dimulai bersih?
Jika satu jawaban "tidak", perbaiki itu dulu. Rasa sakit form yang paling umum adalah ketidakcocokan: nama field melenceng dari API, atau error server kembali dalam bentuk yang UI Anda tidak bisa tempatkan.
Jika Anda membangun tools internal dan ingin bergerak lebih cepat, AppMaster (appmaster.io) mengikuti fundamental yang sama: jaga UI field konsisten, pusatkan aturan dan alur kerja, dan buat respons server muncul di tempat pengguna bisa bertindak.
FAQ
Standarkan ketika Anda mulai melihat label, hint, tanda wajib, spasi, dan gaya error yang sama muncul berulang di banyak halaman. Jika satu perubahan “kecil” memaksa Anda mengedit banyak file, pembungkus BaseField bersama dan beberapa komponen input konsisten akan menghemat waktu dengan cepat.
Buat komponen field tetap sederhana: ia merender label, kontrol, hint, dan error, lalu mengemisi pembaruan nilai. Letakkan logika lintas-field, aturan kondisional, dan apa pun yang bergantung pada nilai lain di form induk atau lapisan validasi agar komponen field tetap dapat digunakan ulang.
Gunakan key yang stabil yang secara default cocok dengan payload API Anda, misalnya first_name atau billing.address.zip. Ini membuat validasi dan pemetaan error server lebih mudah karena Anda tidak perlu sering menerjemahkan nama antar lapisan.
Format sederhana adalah satu objek state yang memegang values, errors, touched, dirty, dan defaults. Ketika semuanya membaca dan menulis melalui bentuk yang sama, perilaku reset dan submit menjadi dapat diprediksi dan Anda menghindari bug “setengah-reset”.
Tetapkan values dan defaults dari data yang sama yang dimuat dari server. Lalu reset() harus menyalin defaults kembali ke values dan mengosongkan touched, dirty, dan errors sehingga UI terlihat bersih dan sesuai dengan apa yang terakhir dikembalikan server.
Mulailah dengan fungsi sederhana yang dipetakan berdasarkan nama field yang sama seperti state form Anda. Kembalikan satu pesan jelas per field (gagal pertama) sehingga UI tetap tenang dan pengguna tahu apa yang perlu diperbaiki.
Validasi sebagian besar field pada blur, lalu validasi semuanya pada submit sebagai pemeriksaan akhir. Gunakan validasi on-change hanya jika benar-benar membantu (misalnya kekuatan password) sehingga pengguna tidak terus-menerus mendapat error saat mengetik.
Jalankan cek async pada blur atau setelah debounce singkat, dan tampilkan status eksplisit “memeriksa”. Batalkan atau abaikan permintaan usang sehingga respons lambat tidak menimpa input terbaru dan membuat error membingungkan.
Normalisasikan semua format backend menjadi satu bentuk internal seperti { fieldErrors: { key: [messages] }, formErrors: [messages] }. Gunakan satu gaya path (notasi titik bekerja baik) sehingga field bernama address.street selalu bisa membaca fieldErrors['address.street'] tanpa penanganan khusus.
Tampilkan error level form di atas form, tetapi letakkan error field tepat di samping input terkait. Setelah submit gagal, fokuskan field pertama yang punya error dan hapus error server untuk field tersebut segera setelah pengguna mengeditnya lagi.


