สถาปัตยกรรมฟอร์ม Vue 3 สำหรับแอปธุรกิจ: รูปแบบที่นำกลับมาใช้ใหม่ได้
สถาปัตยกรรมฟอร์ม Vue 3 สำหรับแอปธุรกิจ: คอมโพเนนต์ฟิลด์นำกลับมาใช้ใหม่ได้ กฎการตรวจสอบที่อ่านง่าย และวิธีปฏิบัติจริงเพื่อแสดงข้อผิดพลาดจากเซิร์ฟเวอร์บนอินพุตแต่ละตัว

ทำไมโค้ดฟอร์มจึงพังได้ในแอปธุรกิจจริง\n\nฟอร์มในแอปธุรกิจไม่ค่อยอยู่กับที่แบบเรียบง่าย มันเริ่มจาก “แค่ไม่กี่ช่อง” แล้วเติบโตเป็นหลายสิบฟิลด์ ส่วนที่แสดงตามเงื่อนไข สิทธิ์การเข้าถึง และกฎที่ต้องสอดคล้องกับตรรกะแบบแบ็กเอนด์ หลังการเปลี่ยนผลิตภัณฑ์ไม่กี่ครั้ง ฟอร์มยังใช้งานได้ แต่โค้ดจะเริ่มรู้สึกเปราะบาง\n\nสถาปัตยกรรมฟอร์มใน Vue 3 สำคัญเพราะฟอร์มคือที่ที่ "การแก้ไขด่วน" ถมทับ: watcher อีกตัว เงื่อนไขพิเศษอีกหนึ่งกรณี คัดลอกคอมโพเนนต์อีกอัน มันอาจใช้งานได้วันนี้ แต่ยากที่จะไว้วางใจและยากที่จะเปลี่ยนแปลง\n\nสัญญาณเตือนคุ้นเคย: พฤติกรรมอินพุตซ้ำกันข้ามหน้า (ป้ายชื่อ การจัดรูปแบบ เครื่องหมาย required คำแนะนำ), ตำแหน่งข้อผิดพลาดไม่สอดคล้องกัน, กฎการตรวจสอบกระจายอยู่ในคอมโพเนนต์ต่าง ๆ และข้อผิดพลาดจากแบ็กเอนด์ถูกย่อลงเป็น toast ทั่วไปที่ไม่บอกผู้ใช้ว่าจะต้องแก้ไขตรงไหน\n\nความไม่สอดคล้องเหล่านั้นไม่ใช่แค่ปัญหาสไตล์โค้ด แต่กลายเป็นปัญหา UX: ผู้คนส่งฟอร์มซ้ำ เพิ่มตั๋วซัพพอร์ต และทีมหลีกเลี่ยงการแก้ไขฟอร์มเพราะกลัวว่าจะมีบางมุมที่พังโดยไม่รู้ตัว\n\nการตั้งค่าที่ดีทำให้ฟอร์มเป็นเรื่องน่าเบื่อในทางที่ดีที่สุด ด้วยโครงสร้างที่คาดเดาได้ คุณสามารถเพิ่มฟิลด์ เปลี่ยนกฎ และจัดการการตอบกลับจากเซิร์ฟเวอร์โดยไม่ต้องเดินสายไฟใหม่ทั้งหมด\n\nคุณต้องการระบบฟอร์มที่ให้การนำกลับมาใช้ซ้ำได้ (ฟิลด์เดียวทำงานเหมือนกันทุกที่), ความชัดเจน (กฎและการจัดการข้อผิดพลาดตรวจสอบได้ง่าย), พฤติกรรมที่คาดเดาได้ (touched, dirty, reset, submit) และฟีดแบ็กที่ดีขึ้น (ข้อผิดพลาดฝั่งเซิร์ฟเวอร์ปรากฏบนอินพุตที่ต้องการการแก้ไข) รูปแบบด้านล่างเน้นคอมโพเนนต์ฟิลด์ที่นำกลับมาใช้ใหม่ได้ การตรวจสอบที่อ่านง่าย และการแมปข้อผิดพลาดจากเซิร์ฟเวอร์กลับไปยังอินพุตเฉพาะ\n\n## แบบจำลองคิดง่าย ๆ สำหรับโครงสร้างฟอร์ม\n\nฟอร์มที่ทนทานเมื่อเวลาผ่านไปคือระบบขนาดเล็กที่มีส่วนชัดเจน ไม่ใช่กองของอินพุต\n\nคิดเป็นสี่ชั้นที่สื่อสารกันในทิศทางเดียว: UI รวบรวมอินพุต, สถานะฟอร์มเก็บค่า, การตรวจสอบอธิบายว่าสิ่งใดผิด, และเลเยอร์ API โหลดและบันทึกข้อมูล\n\n### สี่ชั้น (และสิ่งที่แต่ละชั้นเป็นเจ้าของ)\n\n- คอมโพเนนต์ UI ของฟิลด์: เรนเดอร์อินพุต ป้ายชื่อ คำแนะนำ และข้อความข้อผิดพลาด ส่งเหตุการณ์การเปลี่ยนค่า\n- สถานะฟอร์ม: เก็บค่าและข้อผิดพลาด (พร้อมธง touched และ dirty)\n- กฎการตรวจสอบ: ฟังก์ชันบริสุทธิ์ที่อ่านค่าและคืนข้อความข้อผิดพลาด\n- การเรียก API: โหลดข้อมูลเริ่มต้น ส่งการเปลี่ยนแปลง และแปลการตอบกลับจากเซิร์ฟเวอร์เป็นข้อผิดพลาดของฟิลด์\n\nการแยกส่วนนี้ทำให้การเปลี่ยนแปลงถูกจำกัด เมื่อมีข้อกำหนดใหม่มา คุณอัปเดตชั้นเดียวโดยไม่ทำให้ชั้นอื่นพัง\n\n### อะไรควรอยู่ในฟิลด์ vs ฟอร์มพาเรนต์\n\nคอมโพเนนต์ฟิลด์ที่นำกลับมาใช้ได้ควรน่าเบื่อ ไม่ควรรู้จัก API, โมเดลข้อมูล, หรือกฎการตรวจสอบ ควรแสดงค่าและแสดงข้อผิดพลาดเท่านั้น\n\nฟอร์มพาเรนต์ประสานงานทุกอย่างที่เหลือ: ฟิลด์ใดมีอยู่ ค่าอยู่ที่ไหน เมื่อไรจะตรวจสอบ และส่งอย่างไร\n\nกฎง่าย ๆ ช่วยได้: ถ้าตรรกะขึ้นกับฟิลด์อื่น (เช่น "State" จำเป็นเฉพาะเมื่อ "Country" เป็น US) ให้เก็บไว้ที่ฟอร์มพาเรนต์หรือเลเยอร์การตรวจสอบ ไม่ใช่ในคอมโพเนนต์ฟิลด์\n\nเมื่อการเพิ่มฟิลด์ใหม่เป็นเรื่องง่ายจริง ๆ คุณมักจะแค่แตะค่าดีฟอลต์หรือสคีมา มาร์กอัปที่วางฟิลด์ และกฎการตรวจสอบของฟิลด์ หากการเพิ่มอินพุตหนึ่งตัวบังคับให้เปลี่ยนแปลงหลายคอมโพเนนต์ที่ไม่เกี่ยวข้อง ขอบเขตของคุณเบลอแล้ว\n\n## คอมโพเนนต์ฟิลด์ที่นำกลับมาใช้ได้: ควรมาตรฐานอะไรบ้าง\n\nเมื่อฟอร์มโตขึ้น สิ่งที่ได้ผลเร็วที่สุดคือหยุดสร้างอินพุตทีละอัน คอมโพเนนต์ฟิลด์ควรมีความคาดเดาได้ นั่นทำให้ใช้งานเร็วและตรวจสอบง่าย\n\nชุดบล็อกที่เป็นประโยชน์:\n\n- BaseField: ห่อป้ายชื่อ คำแนะนำ ข้อความข้อผิดพลาด ระยะ และแอตทริบิวต์การเข้าถึง\n- คอมโพเนนต์อินพุต: TextInput, SelectInput, DateInput, Checkbox ฯลฯ แต่ละตัวมุ่งที่ตัวควบคุม\n- FormSection: จัดกลุ่มฟิลด์ที่เกี่ยวข้องพร้อมหัวเรื่อง คำช่วยสั้น ๆ และระยะที่สอดคล้องกัน\n\nสำหรับ props ให้เก็บผลให้เล็กและบังคับใช้ทุกที่ การเปลี่ยนชื่อ prop ข้าม 40 ฟอร์มเจ็บปวด\n\nรายการที่มักคุ้มค่าในทันที:\n\n- modelValue และ update:modelValue สำหรับ v-model\n- label\n- required\n- disabled\n- error (ข้อความเดียว หรือเป็นอาร์เรย์ถ้าต้องการ)\n- hint\n\nSlots คือที่ที่คุณให้ความยืดหยุ่นโดยไม่ทำลายความสอดคล้อง เก็บเลย์เอาต์ของ BaseField ให้คงที่ แต่อนุญาตความแปรปรวนเล็ก ๆ เช่น action ทางขวา ("ส่งรหัส") หรือไอคอนด้านหน้า หากความแปรปรวนเกิดขึ้นซ้ำสองครั้ง ให้ทำเป็น slot แทนแยกคอมโพเนนต์\n\nมาตรฐานลำดับการเรนเดอร์ (ป้ายชื่อ, คอนโทรล, คำแนะนำ, ข้อผิดพลาด) ผู้ใช้จะสแกนเร็วขึ้น การทดสอบง่ายขึ้น และการแมปข้อผิดพลาดจากเซิร์ฟเวอร์ตรงไปตรงมามากขึ้นเพราะแต่ละฟิลด์มีที่ชัดเจนเพื่อแสดงข้อความ\n\n## สถานะฟอร์ม: values, touched, dirty และ reset\n\nบั๊กส่วนใหญ่ในฟอร์มแอปธุรกิจไม่เกี่ยวกับอินพุต โดยมาจากสถานะกระจัดกระจาย: ค่าในที่หนึ่ง ข้อผิดพลาดในอีกที่หนึ่ง และปุ่มรีเซ็ตที่ทำงานไม่ครบ แบบสถาปัตยกรรมฟอร์ม Vue 3 ที่สะอาดเริ่มจากรูปร่างสถานะที่สอดคล้องเดียว\n\nก่อนอื่น เลือกวิธีตั้งชื่อคีย์ของฟิลด์แล้วใช้ให้ต่อเนื่อง กฎง่ายที่สุดคือ: คีย์ฟิลด์เท่ากับคีย์ payload ของ API ถา้เซิร์ฟเวอร์ต้องการ first_name ฟิลด์ของฟอร์มควรเป็น first_name เช่นกัน การเลือกเล็ก ๆ นี้ทำให้การตรวจสอบ การบันทึก และการแมปข้อผิดพลาดจากเซิร์ฟเวอร์ง่ายขึ้นมาก\n\nเก็บสถานะฟอร์มในที่เดียว (composable, Pinia store, หรือคอมโพเนนต์พาเรนต์) และให้แต่ละฟิลด์อ่าน/เขียนผ่านสถานะนั้น โครงสร้างแบบแบนเหมาะกับหน้าส่วนใหญ่ ไปทำเป็น nested ก็ต่อเมื่อ API ของคุณเป็น nested จริง ๆ\n\n```js
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: '' }
})
\n\nวิธีปฏิบัติที่เป็นประโยชน์ในการคิดเกี่ยวกับแฟลก:\n\n- `touched`: ผู้ใช้มีปฏิสัมพันธ์กับฟิลด์นี้หรือไม่?\n- `dirty`: ค่าต่างจากค่าเริ่มต้น (หรือค่าที่บันทึกครั้งล่าสุด) หรือไม่?\n- `errors`: ข้อความใดที่ผู้ใช้ควรเห็นตอนนี้?\n- `defaults`: เราจะรีเซ็ตกลับไปหาอะไร?\n\nพฤติกรรมรีเซ็ตควรคาดเดาได้ เมื่อคุณโหลดระเบียนที่มีอยู่ ให้ตั้งทั้ง `values` และ `defaults` จากแหล่งเดียวกัน จากนั้น `reset()` สามารถคัดลอก `defaults` กลับไปยัง `values`, ล้าง `touched`, ล้าง `dirty`, และล้าง `errors`\n\nตัวอย่าง: ฟอร์มโปรไฟล์ลูกค้าโหลด `email` จากเซิร์ฟเวอร์ ถ้าผู้ใช้แก้ไขมัน `dirty.email` จะเป็นจริง ถ้าคลิก Reset อีเมลจะกลับเป็นค่าที่โหลดมา (ไม่ใช่สตริงว่าง) และหน้าจอก็ดูสะอาดอีกครั้ง\n\n## กฎการตรวจสอบที่อ่านได้\n\nการทำให้การตรวจสอบอ่านได้ไม่ใช่เรื่องไลบรารีมากเท่ากับการแสดงกฎ ถ้าคุณมองฟิลด์แล้วเข้าใจกฎได้ในไม่กี่วินาที โค้ดฟอร์มจะคงรักษาง่าย\n\n### เลือกรูปแบบกฎที่ยึดตามได้\n\nทีมส่วนใหญ่จะยึดวิธีหนึ่งในสามนี้:\n\n- **กฎต่อฟิลด์**: กฎอยู่ใกล้กับการใช้งานฟิลด์ สแกนง่าย เหมาะกับฟอร์มขนาดเล็กถึงกลาง\n- **กฎแบบสคีมา**: กฎอยู่ในออบเจ็กต์หรือไฟล์เดียว เหมาะเมื่อหลายหน้าซ้ำโมเดลเดียวกัน\n- **ไฮบริด**: กฎง่ายอยู่ใกล้ฟิลด์ กฎที่ใช้ซ้ำหรือซับซ้อนเก็บกลาง\n\nไม่ว่าจะเลือกแบบไหน ให้ชื่อนโยบายและข้อความคาดเดาได้ กฎทั่วไปไม่กี่ข้อ (required, length, format, range) ดีกว่ามี helper เฉพาะกิจจำนวนมาก\n\n### เขียนกฎเหมือนประโยคภาษาอังกฤษธรรมดา\n\nกฎที่ดีอ่านเหมือนประโยค: "อีเมลจำเป็นและต้องมีรูปแบบอีเมล" หลีกเลี่ยง one-liners ที่ซ่อนเจตนา\n\nสำหรับฟอร์มธุรกิจส่วนใหญ่ การคืนข้อความเดียวต่อฟิลด์ในแต่ละครั้ง (ความล้มเหลวข้อแรก) ทำให้ UI สงบและช่วยผู้ใช้แก้ไขปัญหาได้เร็วขึ้น\n\nกฎที่เป็นมิตรกับผู้ใช้ที่พบบ่อย:\n\n- **Required** เฉพาะเมื่อผู้ใช้ต้องกรอกจริง ๆ\n- **Length** ระบุตัวเลขจริง (เช่น 2 ถึง 50 ตัวอักษร)\n- **Format** สำหรับอีเมล โทรศัพท์ ZIP โดยไม่ใช้ regex เข้มงวดที่ปฏิเสธอินพุตจริง\n- **Range** เช่น "วันที่ไม่ใช่อนาคต" หรือ "จำนวนระหว่าง 1 ถึง 999"\n\n### ทำให้การตรวจสอบแบบอะซิงก์เห็นได้ชัด\n\nการตรวจสอบอะซิงก์ (เช่น "ชื่อผู้ใช้ถูกใช้แล้ว") สับสนถ้ามันทํางานเงียบ ๆ\n\nทริก: เรียกตรวจสอบเมื่อ blur หรือตอนหยุดพิมพ์สั้น ๆ แสดงสถานะ "กำลังตรวจสอบ..." ชัดเจน และยกเลิกหรือเพิกเฉยคำขอที่ล้าสมัยเมื่อผู้ใช้พิมพ์ต่อ\n\n### ตัดสินใจว่าเมื่อไรการตรวจสอบจะรัน\n\nเวลาแสดงผลสำคัญเท่ากับกฎ การตั้งค่าที่เป็นมิตรกับผู้ใช้คือ:\n\n- **On change** สำหรับฟิลด์ที่รับประโยชน์จากข้อเสนอแนะสด (เช่น ความแข็งแรงรหัสผ่าน) แต่ใช้อย่างสุภาพ\n- **On blur** สำหรับฟิลด์ส่วนใหญ่ เพื่อให้ผู้ใช้พิมพ์โดยไม่เห็นข้อผิดพลาดตลอดเวลา\n- **On submit** สำหรับการตรวจสอบทั้งฟอร์มเป็นตาข่ายความปลอดภัยขั้นสุดท้าย\n\n## การแมปข้อผิดพลาดจากเซิร์ฟเวอร์ไปยังอินพุตที่ถูกต้อง\n\nการตรวจสอบฝั่งไคลเอนต์เป็นแค่ครึ่งเรื่อง ในแอปธุรกิจ เซิร์ฟเวอร์ปฏิเสธการบันทึกด้วยกฎที่เบราว์เซอร์ไม่รู้: ข้อมูลซ้ำ, การตรวจสอบสิทธิ์, ข้อมูลล้าสมัย, การเปลี่ยนแปลงสถานะ และอื่น ๆ UX ที่ดีขึ้นขึ้นอยู่กับการเปลี่ยนการตอบกลับนั้นเป็นข้อความชัดเจนข้างอินพุตที่ถูกต้อง\n\n### ทำ normalization ของข้อผิดพลาดเป็นรูปร่างภายในเดียว\n\nแบ็กเอนด์ไม่ค่อยจะตรงกันในรูปแบบข้อผิดพลาด บางที่คืนออบเจ็กต์ บางที่คืนรายการ บางที่คืนแผนที่ซ้อนที่มีคีย์เป็นชื่อฟิลด์ แปลงสิ่งที่คุณได้รับทั้งหมดเป็นรูปร่างภายในเดียวที่ฟอร์มของคุณสามารถเรนเดอร์ได้\n\njs
// what your form code consumes
{
fieldErrors: { "email": ["Already taken"], "address.street": ["Required"] },
formErrors: ["You do not have permission to edit this customer"]
}
\n\nรักษากฎไม่กี่ข้อให้คงที่:\n\n- เก็บข้อผิดพลาดฟิลด์เป็นอาร์เรย์ (แม้มีแค่ข้อความเดียว)\n- แปลงสไตล์ path ต่าง ๆ ให้เป็นสไตล์เดียว (dot paths สะดวก: `address.street`)\n- เก็บข้อผิดพลาดที่ไม่ใช่ฟิลด์แยกต่างหากเป็น `formErrors`\n- เก็บ payload ดิบของเซิร์ฟเวอร์สำหรับการล็อก แต่ไม่เรนเดอร์มัน\n\n### แมป path ของเซิร์ฟเวอร์ไปยังคีย์ฟิลด์ของคุณ\n\nส่วนยากคือการจัดให้ path ตามที่เซิร์ฟเวอร์คิดสอดคล้องกับคีย์ฟิลด์ของฟอร์มคุณ ตัดสินใจคีย์ของแต่ละฟิลด์แล้วใช้ต่อไป (เช่น `email`, `profile.phone`, `contacts.0.type`) แล้วเขียนแมปเปอร์เล็ก ๆ ที่จัดการกรณีทั่วไป:\n\n- `address.street` (dot notation)\n- `address[0].street` (brackets สำหรับอาร์เรย์)\n- `/address/street` (สไตล์ JSON Pointer)\n\nหลังการ normalization `\u003cField name="address.street" /\u003e` ควรอ่าน `fieldErrors["address.street"]` ได้โดยไม่ต้องมีกรณีพิเศษ\n\nรองรับ alias เมื่อต้องการ ถ้าแบ็กเอนด์คืน `customer_email` แต่ UI ใช้ `email` ให้เก็บแมปเช่น `{ customer_email: "email" }` ระหว่าง normalization\n\n### ข้อผิดพลาดระดับฟิลด์ ข้อผิดพลาดระดับฟอร์ม และการโฟกัส\n\nไม่ใช่ข้อผิดพลาดทุกอย่างจะอยู่ที่อินพุตเดียว หากเซิร์ฟเวอร์บอกว่า "เกินขีดจำกัดแพลน" หรือ "ต้องชำระเงิน" ให้แสดงเหนือฟอร์มเป็นข้อความระดับฟอร์ม\n\nสำหรับข้อผิดพลาดที่ระบุฟิลด์ ให้แสดงข้อความถัดจากอินพุตและชี้นำผู้ใช้ไปยังปัญหาแรก:\n\n- หลังตั้งข้อผิดพลาดจากเซิร์ฟเวอร์ ให้หา key แรกใน `fieldErrors` ที่มีอยู่ในฟอร์มที่เรนเดอร์ได้\n- เลื่อนมันเข้ามาในมุมมองและโฟกัสมัน (ใช้ ref ต่อฟิลด์และ `nextTick`)\n- ล้างข้อผิดพลาดจากเซิร์ฟเวอร์ของฟิลด์เมื่อผู้ใช้แก้ไขฟิลด์นั้นอีกครั้ง\n\n## ขั้นตอนทีละขั้น: นำสถาปัตยกรรมมารวมกัน\n\nฟอร์มสงบเมื่อคุณตัดสินตั้งแต่แรกว่าอะไรควรอยู่ในสถานะฟอร์ม, UI, การตรวจสอบ, และ API แล้วเชื่อมต่อด้วยฟังก์ชันเล็ก ๆ ไม่กี่ตัว\n\nลำดับที่ใช้ได้กับแอปธุรกิจส่วนใหญ่:\n\n- เริ่มด้วยโมเดลฟอร์มเดียวและคีย์ฟิลด์ที่เสถียร คีย์เหล่านั้นกลายเป็นสัญญาข้ามคอมโพเนนต์ ตัวตรวจสอบ และข้อผิดพลาดจากเซิร์ฟเวอร์\n- สร้างห่อ `BaseField` หนึ่งตัวสำหรับป้ายชื่อ ข้อความช่วย เครื่องหมาย required และการแสดงข้อผิดพลาด เก็บคอมโพเนนต์อินพุตให้เล็กและสอดคล้องกัน\n- เพิ่มเลเยอร์การตรวจสอบที่รันแบบต่อฟิลด์และรันทั้งหมดเมื่อส่ง\n- ส่งไปยัง API ถ้าล้มเหลว ให้แปลงข้อผิดพลาดจากเซิร์ฟเวอร์เป็น `{ [fieldKey]: message }` เพื่อให้ฟิลด์ที่ถูกต้องแสดงข้อความที่ถูกต้อง\n- แยกการจัดการความสำเร็จ (reset, toast, navigate) ให้ออกไปต่างหากเพื่อไม่ให้มันเล็ดลอดเข้าไปในคอมโพเนนต์และตัวตรวจสอบ\n\nจุดเริ่มต้นง่าย ๆ สำหรับสถานะ:\n\njs
const values = reactive({ email: '', name: '', phone: '' })
const touched = reactive({ email: false, name: false, phone: false })
const errors = reactive({}) // { email: '...', name: '...' }
\n\nBaseField ของคุณจะรับ `label`, `error`, และบางที `touched` แล้วเรนเดอร์ข้อความในที่เดียว คอมโพเนนต์อินพุตแต่ละตัวสนใจแค่ binding และการส่งอัปเดต\n\nสำหรับการตรวจสอบ ให้เก็บกฎไว้ใกล้โมเดลโดยใช้คีย์เดียวกัน:\n\njs
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 = rulesk
if (msg) errors[k] = msg
else delete errors[k]
touched[k] = true
})
return Object.keys(errors).length === 0
}
```\n\nเมื่อเซิร์ฟเวอร์ตอบกลับด้วยข้อผิดพลาด ให้แมปพวกมันโดยใช้คีย์เดียวกัน หาก API คืน { "field": "email", "message": "Already taken" } ให้ตั้ง errors.email = 'Already taken' และทำเครื่องหมายว่า touched หากเป็นข้อผิดพลาดทั่วไป (เช่น "permission denied") ให้แสดงเหนือฟอร์ม\n\n## สถานการณ์ตัวอย่าง: แก้ไขโปรไฟล์ลูกค้า\n\nนึกภาพหน้าภายในสำหรับแอดมินที่ตัวแทนซัพพอร์ตแก้ไขโปรไฟล์ลูกค้า ฟอร์มมีสี่ฟิลด์: name, email, phone, และ role (Customer, Manager, Admin) ขนาดเล็ก แต่แสดงปัญหาทั่วไป\n\nกฎฝั่งไคลเอนต์ควรชัดเจน:\n\n- Name: จำเป็น ต้องมีความยาวขั้นต่ำ\n- Email: จำเป็น ต้องเป็นรูปแบบอีเมลที่ถูกต้อง\n- Phone: ไม่จำเป็น แต่ถ้ามีให้ต้องตรงรูปแบบที่ยอมรับ\n- Role: จำเป็น และบางครั้งมีเงื่อนไข (เช่น ต้องมีสิทธิ์จึงจะกำหนด Admin ได้)\n\nสัญญาการใช้งานคอมโพเนนต์ช่วยได้: แต่ละฟิลด์รับค่าปัจจุบัน ข้อความข้อผิดพลาดปัจจุบัน (ถ้ามี) และ boolean สองสามตัวเช่น touched และ disabled ป้ายชื่อ เครื่องหมาย required ระยะ และสไตล์ข้อผิดพลาดไม่ควรถูกคิดซ้ำในแต่ละหน้า\n\nตอนนี้โฟลว์ UX: ตัวแทนแก้ไขอีเมล กดแท็บออก และเห็นข้อความอินไลน์ใต้ Email ถ้ารูปแบบผิด พวกเขาแก้ไข กด Save แล้วเซิร์ฟเวอร์ตอบ:\n\n- email already exists: แสดงใต้ Email และโฟกัสฟิลด์นั้น\n- phone invalid: แสดงใต้ Phone\n- permission denied: แสดงเป็นข้อความระดับฟอร์มที่ด้านบน\n\nถ้าคุณเก็บข้อผิดพลาดเป็นคีย์ตามชื่อฟิลด์ (email, phone, role) การแมปก็จะง่าย ข้อผิดพลาดของฟิลด์ไปที่อินพุต ข้อผิดพลาดระดับฟอร์มไปที่แสดงข้อความเฉพาะ\n\n## ความผิดพลาดทั่วไปและวิธีหลีกเลี่ยง\n\n### เก็บลอจิกไว้ที่เดียว\n\nการคัดลอกกฎการตรวจสอบไปทุกหน้ารู้สึกเร็วแต่เมื่อกฎเปลี่ยน (ข้อกำหนดรหัสผ่าน, ไอดีภาษีที่จำเป็น, โดเมนที่อนุญาต) คุณต้องแก้หลายที่ เก็บกฎศูนย์กลาง (สคีมา ไฟล์กฎ ฟังก์ชันแชร์) และให้ฟอร์มใช้อย่างเดียวกัน\n\nหลีกเลี่ยงไม่ให้อินพุตระดับต่ำทำมากเกินไป หาก \u003cTextField\u003e ของคุณรู้วิธีเรียก API, retry เมื่อพัง, และแยก parse payload ข้อผิดพลาดจากเซิร์ฟเวอร์ มันจะไม่สามารถนำกลับมาใช้ได้ คอมโพเนนต์ฟิลด์ควรเรนเดอร์ ส่งเหตุการณ์การเปลี่ยนค่า และแสดงข้อผิดพลาด วางการเรียก API และการแมปในคอนเทนเนอร์ของฟอร์มหรือ composable\n\nอาการที่ผสมความรับผิดชอบ:\n\n- ข้อความการตรวจสอบเดียวกันเขียนในหลายที่\n- คอมโพเนนต์ฟิลด์ import ลูกค้าของ API\n- การเปลี่ยน endpoint ทำให้ฟอร์มไม่กี่ฟอร์มพัง\n- การทดสอบต้อง mount ครึ่งแอปแค่เพื่อตรวจอินพุตหนึ่งตัว\n\n### กับดัก UX และการเข้าถึง\n\nแบนเนอร์ข้อผิดพลาดเดียวเช่น "Something went wrong" ไม่พอ ผู้ใช้ต้องรู้ว่าฟิลด์ไหนผิดและต้องทำอย่างไรต่อ ใช้แบนเนอร์สำหรับล้มเหลวระดับสากล (เน็ตเวิร์กลง, ไม่มีสิทธิ์) และแมปข้อผิดพลาดจากเซิร์ฟเวอร์ไปยังอินพุตเฉพาะเพื่อให้ผู้ใช้แก้ไขได้เร็ว\n\nการโหลดและการส่งซ้ำสร้างสถานะที่สับสน เมื่อส่งข้อมูล ให้ปิดปุ่มส่ง ปิดฟิลด์ที่ไม่ควรเปลี่ยนระหว่างบันทึก และแสดงสถานะกำลังทำงาน ช่วยให้รีเซ็ตและยกเลิกคืนฟอร์มอย่างถูกต้อง\n\nพื้นฐานการเข้าถึงที่มักถูกข้ามแต่ไม่ยาก:\n\n- ทุกอินพุตมีป้ายชื่อที่มองเห็นได้ (ไม่ใช่แค่ placeholder)\n- ข้อผิดพลาดเชื่อมต่อกับฟิลด์ด้วย aria attributes ที่เหมาะสม\n- โฟกัสย้ายไปยังฟิลด์ที่ไม่ถูกต้องตัวแรกหลังส่ง\n- ฟิลด์ที่ปิดใช้งานไม่สามารถโต้ตอบได้จริงและประกาศอย่างถูกต้อง\n- การนำทางด้วยคีย์บอร์ดใช้งานได้ครบวงจร\n\n## เช็คลิสต์ด่วนและขั้นตอนถัดไป\n\nก่อนปล่อยฟอร์มใหม่ ให้ทำเช็คลิสต์ด่วน มันจับช่องว่างเล็ก ๆ ที่กลายเป็นตั๋วซัพพอร์ตได้\n\n- ฟิลด์แต่ละตัวมีคีย์เสถียรที่ตรงกับ payload และการตอบกลับของเซิร์ฟเวอร์หรือไม่ (รวม path ซ้อนเช่น billing.address.zip)?\n- คุณสามารถเรนเดอร์ฟิลด์ใด ๆ ด้วย API คอมโพเนนต์ฟิลด์เดียวกันหรือไม่ (รับค่าเข้า, เหตุการณ์ออก, รับ error และ hint)?\n- เมื่อส่ง คุณตรวจสอบครั้งเดียว ป้องกันการส่งซ้ำ และโฟกัสฟิลด์แรกที่ไม่ถูกต้องหรือไม่?\n- คุณแสดงข้อผิดพลาดในที่ถูกต้องหรือไม่: ต่อฟิลด์ (ถัดจากอินพุต) และระดับฟอร์ม (ข้อความทั่วไปเมื่อจำเป็น)?\n- หลังความสำเร็จ คุณรีเซ็ตสถานะอย่างถูกต้อง (values, touched, dirty) เพื่อให้การแก้ไขถัดไปเริ่มสะอาดหรือไม่?\n\nถ้าตอบว่า "ไม่" ข้อนึง ให้แก้ข้อนั้นก่อน ปัญหาเรื่องฟอร์มที่พบบ่อยที่สุดคือการไม่ตรงกัน: ชื่อฟิลด์เบนออกจาก API หรือข้อผิดพลาดจากเซิร์ฟเวอร์คืนในรูปแบบที่ UI ไม่สามารถจับที่จะแสดงได้\n\nถ้าคุณสร้างเครื่องมือภายในและต้องการทำงานให้เร็วขึ้น AppMaster (appmaster.io) ทำตามพื้นฐานเดียวกัน: รักษา UI ฟิลด์ให้สอดคล้อง รวมกฎและเวิร์กโฟลว์ไว้ศูนย์กลาง และทำให้การตอบสนองจากเซิร์ฟเวอร์ปรากฏในที่ที่ผู้ใช้สามารถลงมือได้
คำถามที่พบบ่อย
มาตรฐานเมื่อคุณเห็นป้ายชื่อ คำอธิบายสั้น ๆ เครื่องหมาย required ระยะช่องว่าง และการแสดงข้อผิดพลาดซ้ำกันในหลายหน้า หากการเปลี่ยนแปลง "เล็กน้อย" หนึ่งอย่างต้องแก้หลายไฟล์ ให้สร้าง BaseField ร่วมและคอมโพเนนต์อินพุตที่สอดคล้องกันเพื่อประหยัดเวลาได้เร็วขึ้น
ให้คอมโพเนนต์ฟิลด์ทำหน้าที่เรียบง่าย: เรนเดอร์ป้ายชื่อ ควบคุม แสดงคำอธิบายและข้อผิดพลาด แล้วส่งเหตุการณ์การอัปเดตค่า ลอจิกที่ขึ้นกับฟิลด์อื่น ๆ เงื่อนไขต่าง ๆ หรือกฎข้ามฟิลด์ ควรอยู่ที่ฟอร์มพาเรนต์หรือในเลเยอร์การตรวจสอบเพื่อให้ฟิลด์คงความนำกลับมาใช้ได้
ใช้คีย์ที่เสถียรและสอดคล้องกับ payload ของ API โดยปริยาย เช่น first_name หรือ billing.address.zip วิธีนี้ทำให้การตรวจสอบและการแมปข้อผิดพลาดจากเซิร์ฟเวอร์ตรงไปตรงมาเพราะคุณไม่ต้องแปลงชื่อระหว่างเลเยอร์อยู่บ่อย ๆ
ค่าเริ่มต้นที่เรียบง่ายคือวัตถุสถานะเดียวที่เก็บ values, errors, touched, dirty และ defaults เมื่อทุกอย่างอ่านและเขียนผ่านรูปทรงเดียวกัน การรีเซ็ตและพฤติกรรมการส่งจะคาดเดาได้ และคุณจะหลีกเลี่ยงบั๊กที่รีเซ็ตไม่ครบ
เมื่อโหลดข้อมูลเพื่อแก้ไข ให้ตั้งทั้ง values และ defaults จากข้อมูลที่โหลดมา แล้วให้ reset() คัดลอก defaults กลับไปยัง values และล้าง touched, dirty, และ errors ดังนั้น UI จะกลับไปตรงกับค่าที่เซิร์ฟเวอร์คืนล่าสุด
เริ่มจากการกำหนดกฎเป็นฟังก์ชันง่าย ๆ ที่มีคีย์เดียวกับชื่อฟิลด์ในสถานะฟอร์ม คืนข้อความความผิดพลาดหนึ่งข้อความต่อฟิลด์ (ความผิดพลาดแรกที่พบ) เพื่อให้ UI ไม่รกและผู้ใช้รู้ว่าจะต้องแก้ไขอะไรก่อน
ตรวจสอบฟิลด์ส่วนใหญ่เมื่อ blur และตรวจสอบทั้งฟอร์มอีกครั้งเมื่อ submit ใช้การตรวจสอบแบบ on-change เฉพาะที่ช่วยจริง ๆ (เช่น ความแข็งแรงรหัสผ่าน) เพื่อไม่ให้ผู้ใช้เห็นข้อผิดพลาดระหว่างพิมพ์
เรียกตรวจสอบแบบอะซิงก์เมื่อ blur หรือหลังหน่วงเวลาเล็กน้อย และแสดงสถานะ "กำลังตรวจสอบ" ชัดเจน ยกเลิกหรือเพิกเฉยคำขอที่ล้าสมัยเพื่อให้การตอบกลับช้าจากเซิร์ฟเวอร์ไม่เขียนทับอินพุตล่าสุดและสร้างข้อผิดพลาดที่สับสน
ปกติแล้วให้ทำ normalization รูปแบบใด ๆ ของแบ็กเอนด์เป็นรูปแบบภายในเดียว เช่น { fieldErrors: { key: [messages] }, formErrors: [messages] } ใช้สไตล์ path เดียว (dot notation มักสะดวก) เพื่อให้ฟิลด์ชื่อ address.street สามารถอ่าน fieldErrors['address.street'] ได้เสมอโดยไม่ต้องมีกรณีพิเศษ
แสดงข้อผิดพลาดระดับฟอร์มที่ด้านบนของฟอร์ม แต่แสดงข้อผิดพลาดระดับฟิลด์ถัดจากอินพุตนั้น ๆ หลังการส่งที่ล้มเหลว ให้โฟกัสไปที่ฟิลด์แรกที่มีข้อผิดพลาด และล้างข้อผิดพลาดจากเซิร์ฟเวอร์ของฟิลด์นั้นทันทีเมื่อผู้ใช้แก้ไขค่านั้นอีกครั้ง


