Now.js Framework Documentation

Now.js Framework Documentation

FormManager - ระบบจัดการฟอร์มและการตรวจสอบความถูกต้อง

TH 25 Nov 2025 11:00

FormManager - ระบบจัดการฟอร์มและการตรวจสอบความถูกต้อง

เอกสารฉบับนี้อธิบาย FormManager ซึ่งเป็นระบบจัดการฟอร์มแบบครบวงจรของ Now.js Framework ครอบคลุมการตั้งค่า การตรวจสอบความถูกต้อง การส่งข้อมูลแบบ AJAX การอัปโหลดไฟล์ และการผสานระบบความปลอดภัย

📋 สารบัญ

  1. ภาพรวม
  2. การติดตั้งและนำเข้า
  3. การเริ่มต้นใช้งาน
  4. การตั้งค่าฟอร์ม
  5. การตรวจสอบความถูกต้องของฟอร์ม
  6. การส่งฟอร์ม
  7. การส่งแบบ AJAX
  8. การอัปโหลดไฟล์
  9. ข้อมูลฟอร์ม
  10. การคงค่าฟิลด์
  11. การผสานระบบความปลอดภัย
  12. ตัวอย่างการใช้งาน
  13. เอกสารอ้างอิง API
  14. แนวทางปฏิบัติที่แนะนำ
  15. ปัญหาที่พบบ่อย
  16. เอกสารที่เกี่ยวข้อง
  17. สัญญาอนุญาต
  18. ข้อมูลอัปเดตล่าสุด

ภาพรวม

FormManager เป็นระบบจัดการฟอร์มแบบครบวงจร รองรับการตรวจสอบความถูกต้อง การส่งข้อมูลทั้งแบบดั้งเดิมและ AJAX การอัปโหลดไฟล์ รวมถึงมาตรการด้านความปลอดภัยที่จำเป็น

ฟีเจอร์หลัก

  • การเริ่มต้นอัตโนมัติ: ค้นหาและตั้งค่าฟอร์มที่มี data-form ให้พร้อมใช้งาน
  • การตรวจสอบความถูกต้อง: รองรับ validation ทั้งในตัวและแบบกำหนดเอง
  • การส่งแบบ AJAX: ส่งคำร้องด้วย AJAX พร้อมรอผลลัพธ์และจัดการตอบกลับ
  • การอัปโหลดไฟล์: รองรับการอัปโหลดไฟล์พร้อมแถบความคืบหน้า
  • การจำค่าฟิลด์: บันทึกค่าฟิลด์ไว้ใน localStorage หรือคุกกี้ตามต้องการ
  • ป้องกัน CSRF: แทรกและตรวจสอบ CSRF token อัตโนมัติ
  • จำกัดจำนวนการส่ง: ป้องกันการส่งฟอร์มถี่เกินไปด้วย rate limiting
  • ป้องกันการส่งซ้ำ: ปิดปุ่มส่งชั่วคราวเพื่อกันการ double submit
  • ตัวตรวจสอบแบบกำหนดเอง: เพิ่ม validator ใหม่ได้ตาม requirement
  • การจัดการข้อผิดพลาด: รวมศูนย์การจัดการ error และข้อความตอบกลับ
  • ข้อความสำเร็จ/ผิดพลาด: แสดงผลลัพธ์ให้ผู้ใช้เข้าใจได้ทันที
  • จัดการการเปลี่ยนเส้นทาง: ควบคุม redirect หลังส่งฟอร์มได้ละเอียด
  • จำผู้ใช้: รองรับฟีเจอร์จดจำชื่อผู้ใช้สำหรับฟอร์มล็อกอิน
  • จำ URL ปลายทาง: เก็บ URL ที่ผู้ใช้ตั้งใจจะเข้าก่อนล็อกอินและเติมให้อัตโนมัติ

เมื่อควรเลือกใช้ FormManager

แนะนำให้ใช้เมื่อ:

  • ต้องการระบบจัดการฟอร์มที่ตั้งค่าได้จาก HTML data attributes
  • ต้องการ validation ที่ยืดหยุ่นและการส่งข้อมูลแบบ AJAX
  • จำเป็นต้องอัปโหลดไฟล์พร้อมแสดงความคืบหน้า
  • ต้องรวมระบบความปลอดภัย เช่น CSRF และ rate limiting
  • ต้องการบันทึกค่าฟิลด์เพื่อประสบการณ์ผู้ใช้ที่ต่อเนื่อง

พิจารณาทางเลือกอื่นเมื่อ:

  • ฟอร์มมีความเรียบง่ายมากและไม่ต้องตรวจสอบความถูกต้อง
  • ต้องการส่งข้อมูลแบบรีเฟรชหน้าแบบดั้งเดิมโดยไม่มีการประมวลผลเพิ่มเติม

การติดตั้งและนำเข้า

FormManager โหลดมาพร้อมกับ Now.js Framework และพร้อมใช้งานทันทีผ่าน window object:

// ไม่ต้อง import - พร้อมใช้งานทันที
console.log(window.FormManager); // อ็อบเจ็กต์ FormManager ที่ผูกไว้กับ window

การเริ่มต้นใช้งาน

การตั้งค่าพื้นฐาน

// เริ่มต้นใช้งาน FormManager
await FormManager.init({
  debug: false,
  ajaxSubmit: true,
  autoValidate: true,
  resetAfterSubmit: false,
  preventDoubleSubmit: true,
  showLoadingOnSubmit: true,
  validateOnInput: true,
  validateOnBlur: true
});

console.log('FormManager พร้อมใช้งานแล้ว!');

ตัวอย่างฟอร์มพื้นฐาน

<!-- ฟอร์มพื้นฐานที่มี data-form (จำเป็นสำหรับ auto-init) -->
<form data-form="contact" method="POST" action="/api/contact">
  <div>
    <label>ชื่อ:</label>
    <input type="text" name="name" required>
  </div>

  <div>
    <label>อีเมล:</label>
    <input type="email" name="email" required>
  </div>

  <div>
    <label>ข้อความ:</label>
    <textarea name="message" required minlength="10"></textarea>
  </div>

  <button type="submit">ส่งข้อความ</button>
</form>

ลำดับการทำงานของฟอร์ม

ผู้ใช้กรอกข้อมูลในฟอร์ม
      ↓
กดปุ่มส่งฟอร์ม
      ↓
┌────────────────────┐
│  ยกเลิกการส่งดั้งเดิม │
│  - ป้องกันการส่งฟอร์มของเบราว์เซอร์ │
└─────────┬──────────┘
          ↓
┌────────────────────┐
│  ตรวจสอบความปลอดภัย │
│  - ตรวจสอบ rate limit │
│  - ตรวจสอบ CSRF token │
└─────────┬──────────┘
          ↓
┌────────────────────┐
│  ตรวจสอบฟอร์ม      │
│  - ตรวจสอบฟิลด์ที่ต้องกรอก │
│  - ตรวจสอบรูปแบบข้อมูล │
│  - ตรวจสอบกฎที่กำหนดเอง │
└─────────┬──────────┘
          ↓
   ผ่านหรือไม่? ─── ไม่ผ่าน ──> แสดงข้อผิดพลาด
      │
    ผ่าน
      ↓
┌────────────────────┐
│  รวบรวมข้อมูล      │
│  - รวมข้อมูลเป็น FormData │
│  - สร้างอ็อบเจ็กต์ JSON │
│  - แนบไฟล์ (ถ้ามี) │
└─────────┬──────────┘
          ↓
┌────────────────────┐
│  ส่งคำร้อง (AJAX)  │
│  - แสดงสถานะกำลังส่ง │
│  - ส่งคำร้องไปยังเซิร์ฟเวอร์ │
│  - จัดการผลลัพธ์ที่ตอบกลับ │
└─────────┬──────────┘
          ↓
┌────────────────────┐
│  ประมวลผลผลลัพธ์   │
│  - แจ้งผลสำเร็จ │
│  - จัดการข้อผิดพลาด │
│  - เปลี่ยนเส้นทางตามที่กำหนด │
└────────────────────┘

การตั้งค่าฟอร์ม

1. การใช้ Data Attributes

<form
  data-form="login"
  data-ajax-submit="true"
  data-auto-validate="true"
  data-confirm="คุณต้องการส่งข้อมูลนี้หรือไม่?"
  data-reset-after-submit="false"
  data-prevent-double-submit="true"
  data-show-loading-on-submit="true"
  data-redirect="/dashboard"
  data-success-message="เข้าสู่ระบบสำเร็จ!"
  data-error-message="เข้าสู่ระบบไม่สำเร็จ">

  <!-- ฟิลด์ของฟอร์ม -->
</form>

หมายเหตุ: data-confirm จะแสดง confirmation dialog ก่อนส่งฟอร์ม

2. การตั้งค่าความปลอดภัย

<form
  data-form="secure"
  data-csrf="true"
  data-csrf-token="..."
  data-rate-limit="true"
  data-rate-limit-limit="5"
  data-rate-limit-window="60"
  data-validation="true"
  data-sanitize-input="true">

  <!-- ฟิลด์ของฟอร์ม -->
</form>

3. การตั้งค่าการตรวจสอบความถูกต้อง

<form
  data-form="validated"
  data-validate-on-submit="true"
  data-validate-on-input="true"
  data-validate-on-blur="true"
  data-validate-only-dirty="true"
  data-auto-focus-error="true"
  data-auto-scroll-to-error="true">

  <!-- ฟิลด์ของฟอร์ม -->
</form>

4. การแสดงผลข้อผิดพลาด

<form
  data-form="errors"
  data-show-errors-inline="true"
  data-show-errors-in-notification="true"
  data-error-container=".error-container"
  data-error-class="error"
  data-auto-clear-errors="true"
  data-auto-clear-errors-delay="5000">

  <!-- ฟิลด์ของฟอร์ม -->
</form>

5. การแสดงผลเมื่อสำเร็จ

<form
  data-form="success"
  data-success-redirect="/thank-you"
  data-success-message="ส่งฟอร์มสำเร็จ!"
  data-show-success-inline="true"
  data-show-success-in-notification="true"
  data-success-container=".success-container">

  <!-- ฟิลด์ของฟอร์ม -->
</form>

การตรวจสอบความถูกต้องของฟอร์ม

1. ตัวตรวจสอบที่มีมาให้

<!-- ต้องกรอก -->
<input
  type="text"
  name="name"
  required
  data-error-required="กรุณากรอกชื่อของคุณ">

<!-- อีเมล -->
<input
  type="email"
  name="email"
  required
  data-error-email="กรุณากรอกอีเมลให้ถูกต้อง">

<!-- URL -->
<input
  type="url"
  name="website"
  data-error-url="กรุณากรอก URL ที่ถูกต้อง">

<!-- ตัวเลข -->
<input
  type="number"
  name="age"
  data-error-number="กรุณากรอกตัวเลขที่ถูกต้อง">

<!-- ค่า Min/Max -->
<input
  type="number"
  name="age"
  min="18"
  max="100"
  data-error-min="อายุต้องไม่ต่ำกว่า 18 ปี"
  data-error-max="อายุต้องไม่เกิน 100 ปี">

<!-- ความยาวขั้นต่ำ/สูงสุด -->
<input
  type="text"
  name="username"
  minlength="3"
  maxlength="20"
  data-error-minlength="ชื่อผู้ใช้ต้องยาวอย่างน้อย 3 ตัวอักษร"
  data-error-maxlength="ชื่อผู้ใช้ต้องไม่เกิน 20 ตัวอักษร">

<!-- รูปแบบ -->
<input
  type="text"
  name="phone"
  pattern="^[0-9]{10}$"
  data-error-pattern="กรุณากรอกหมายเลขโทรศัพท์ 10 หลัก">

<!-- ตรวจสอบให้ตรงกัน (ยืนยันรหัสผ่าน) -->
<input
  type="password"
  name="password"
  id="password">

<input
  type="password"
  name="confirm_password"
  data-validate-match="password"
  data-error-match="รหัสผ่านทั้งสองไม่ตรงกัน">

2. ตัวตรวจสอบแบบกำหนดเอง

// ลงทะเบียนตัวตรวจสอบที่กำหนดเอง
FormManager.registerValidator('username', (value, element) => {
  // อนุญาตเฉพาะตัวอักษรและขีดล่าง
  return /^[a-zA-Z0-9_]+$/.test(value);
}, 'ชื่อผู้ใช้ต้องประกอบด้วยตัวอักษร ตัวเลข หรือขีดล่างเท่านั้น');

HTML

<input
  type="text"
  name="username"
  data-validate-username="true"
  data-error-username="รูปแบบชื่อผู้ใช้ไม่ถูกต้อง">

3. ตัวตรวจสอบแบบอะซิงโครนัส

// ลงทะเบียนตัวตรวจสอบแบบอะซิงโครนัส
FormManager.registerValidator('unique-email', async (value, element) => {
  const response = await fetch(`/api/check-email?email=${value}`);
  const data = await response.json();
  return data.available;
}, 'อีเมลนี้ถูกใช้ไปแล้ว');
<input
  type="email"
  name="email"
  data-validate-unique-email="true"
  data-error-unique-email="อีเมลนี้ถูกลงทะเบียนไว้แล้ว">

4. ฟังก์ชันตรวจสอบแบบกำหนดเอง

<input
  type="text"
  name="code"
  data-validate-fn="validateCode">

<script>
async function validateCode(value, field, instance) {
  // เขียนเงื่อนไขตรวจสอบเอง
  const response = await fetch(`/api/validate-code?code=${value}`);
  const result = await response.json();

  if (!result.valid) {
    return result.message || 'รหัสไม่ถูกต้อง';
  }

  return true;
}
</script>

5. เหตุการณ์ที่เกี่ยวข้องกับการตรวจสอบ

// รับฟังเหตุการณ์การตรวจสอบ

// การตรวจสอบระดับฟิลด์
document.addEventListener('form:field:change', (e) => {
  const { formId, field, name, value } = e.detail;
  console.log(`ฟิลด์ ${name} เปลี่ยนค่าเป็น:`, value);
});

// การตรวจสอบทั้งฟอร์ม
document.addEventListener('form:validate', (e) => {
  const { formId, isValid, errors } = e.detail;

  if (isValid) {
    console.log('ฟอร์มผ่านการตรวจสอบ');
  } else {
    console.log('พบข้อผิดพลาดในการตรวจสอบ:', errors);
  }
});

// กรณีตรวจสอบไม่ผ่าน
document.addEventListener('form:validation:failed', (e) => {
  const { formId, errors } = e.detail;
  console.log('การตรวจสอบไม่ผ่าน:', errors);
});

การส่งฟอร์ม

1. การส่งแบบดั้งเดิม

<!-- ส่งแบบไม่ใช้ AJAX -->
<form
  data-form="traditional"
  data-ajax-submit="false"
  method="POST"
  action="/submit">

  <input type="text" name="name" required>
  <button type="submit">ส่ง</button>
</form>

2. การส่งแบบ AJAX

<!-- การส่งแบบ AJAX (ค่าเริ่มต้น) -->
<form
  data-form="ajax"
  data-ajax-submit="true"
  data-action="/api/submit"
  data-method="POST">

  <input type="text" name="name" required>
  <button type="submit">ส่ง</button>
</form>

3. การกำหนด HTTP Method เอง

<!-- ส่งคำร้องแบบ PUT -->
<form
  data-form="update"
  data-method="PUT"
  data-action="/api/users/123">

  <input type="text" name="name">
  <button type="submit">อัปเดต</button>
</form>

<!-- ส่งคำร้องแบบ DELETE -->
<form
  data-form="delete"
  data-method="DELETE"
  data-action="/api/users/123">

  <button type="submit">ลบ</button>
</form>

4. เหตุการณ์ที่เกี่ยวข้องกับการส่งฟอร์ม

// ก่อนส่งฟอร์ม
document.addEventListener('form:submitting', (e) => {
  const { formId, form } = e.detail;
  console.log('กำลังส่งฟอร์ม:', formId);
});

// หลังส่งสำเร็จ
document.addEventListener('form:submitted', (e) => {
  const { formId, response } = e.detail;
  console.log('ส่งฟอร์มเรียบร้อย:', formId);
  console.log('ผลลัพธ์ที่ได้รับ:', response);
});

// เมื่อเกิดข้อผิดพลาด
document.addEventListener('form:error', (e) => {
  const { formId, response, error } = e.detail;
  console.log('เกิดข้อผิดพลาดในฟอร์ม:', formId);
  console.log('รายละเอียด:', error || response);
});

5. ป้องกันการส่งซ้ำ

<!-- ป้องกันการกดส่งซ้ำ -->
<form
  data-form="secure"
  data-prevent-double-submit="true"
  data-double-submit-timeout="2000">

  <!-- ป้องกันไม่ให้ส่งซ้ำภายใน 2 วินาที -->
  <button type="submit">ส่ง</button>
</form>

การส่งแบบ AJAX

1. การส่งข้อมูลแบบ JSON

<form data-form="json" data-action="/api/users">
  <input type="text" name="name" value="John">
  <input type="email" name="email" value="john@example.com">
  <button type="submit">ส่ง</button>
</form>

ข้อมูลที่ส่งออกไป:

{
  "name": "John",
  "email": "john@example.com",
  "_token": "csrf_token_here"
}

2. การส่งข้อมูลแบบ FormData

<form data-form="upload" enctype="multipart/form-data">
  <input type="file" name="file">
  <input type="text" name="title">
  <button type="submit">อัปโหลด</button>
</form>

รูปแบบที่ส่ง: multipart/form-data

3. การจัดการผลลัพธ์ที่ตอบกลับ

// โครงสร้างการตอบกลับจากเซิร์ฟเวอร์
{
  "success": true,
  "message": "ดำเนินการสำเร็จ",
  "data": {
    "user": {...},
    "token": "..."
  },
  "redirectUrl": "/dashboard"
}

// หรือกรณีเกิดข้อผิดพลาด
{
  "success": false,
  "message": "การตรวจสอบไม่ผ่าน",
  "errors": {
    "email": "อีเมลนี้ถูกใช้แล้ว",
    "password": "รหัสผ่านคาดว่าปลอดภัยไม่เพียงพอ"
  }
}

4. การประมวลผลผลลัพธ์เพิ่มเติม

// รับฟังผลลัพธ์จากการส่งฟอร์ม
document.addEventListener('form:submitted', async (e) => {
  const { formId, response } = e.detail;

  if (response.success) {
    // จัดการกรณีสำเร็จ
    if (response.data) {
      console.log('ข้อมูลผู้ใช้:', response.data.user);
    }

    // กรณีต้องการเปลี่ยนเส้นทางเอง
    if (response.redirectUrl) {
      await Router.navigate(response.redirectUrl);
    }
  } else {
    // จัดการข้อผิดพลาด
    if (response.errors) {
      Object.entries(response.errors).forEach(([field, message]) => {
        console.log(`ฟิลด์ ${field}:`, message);
      });
    }
  }
});

5. Response Actions Format

FormManager รองรับการประมวลผล actions จากเซิร์ฟเวอร์ผ่าน ResponseHandler โดยใช้รูปแบบ single action object (ไม่ใช่ array):

รูปแบบที่ถูกต้อง:

// ✅ ถูกต้อง - Single action object
return $this->successResponse([
    'data' => [...],
    'actions' => [
        'type' => 'reload',
        'closeModal' => true,
        'reload' => '[data-table="users"]'
    ]
], 'บันทึกสำเร็จ');

รูปแบบที่ไม่ถูกต้อง:

// ❌ ผิด - Array of actions (ไม่รองรับ)
return $this->successResponse([
    'data' => [...],
    'actions' => [
        ['type' => 'notification', 'message' => 'Success'],
        ['type' => 'closeModal'],
        ['type' => 'reload']
    ]
]);

Action Types ที่รองรับ:

1. Reload Table

'actions' => [
    'type' => 'reload',
    'closeModal' => true,  // ปิด modal ก่อน reload
    'reload' => '[data-table="table-name"]'  // selector ของตาราง
]

2. Redirect

'actions' => [
    'type' => 'redirect',
    'url' => '/dashboard',  // URL ปลายทาง
    'closeModal' => true    // optional
]

3. Close Modal Only

'actions' => [
    'type' => 'closeModal'
]

4. Show Notification

'actions' => [
    'type' => 'notification',
    'message' => 'ดำเนินการสำเร็จ',
    'level' => 'success'  // success, error, warning, info
]

5. Combined Actions

// แสดง toast + ปิด modal + reload table
'actions' => [
    'type' => 'reload',
    'message' => 'บันทึกสำเร็จ',  // แสดง toast อัตโนมัติ
    'closeModal' => true,
    'reload' => '[data-table="users"]'
]

Flow การทำงาน:

1. Server ส่ง response พร้อม actions
   ↓
2. FormManager รับ response
   ↓
3. ส่งต่อไปยัง ResponseHandler.process()
   ↓
4. ResponseHandler ประมวลผล actions:
   - แสดง toast (ถ้ามี message)
   - ปิด modal (ถ้ามี closeModal: true)
   - Reload table (ถ้ามี reload)
   - Redirect (ถ้ามี url)

การอัปโหลดไฟล์

1. อัปโหลดไฟล์เดี่ยว

<form data-form="upload" data-action="/api/upload">
  <input type="file" name="avatar" accept="image/*" required>
  <button type="submit">อัปโหลด</button>
</form>

2. อัปโหลดหลายไฟล์

<form data-form="multi-upload" data-action="/api/upload">
  <input type="file" name="files" multiple accept="image/*">
  <button type="submit">อัปโหลดไฟล์</button>
</form>

3. แสดงความคืบหน้าการอัปโหลด

<form data-form="upload-progress" data-action="/api/upload">
  <input type="file" name="file">

  <!-- แถบความคืบหน้า (สร้างให้อัตโนมัติ) -->
  <div class="upload-progress" style="display:none">
    <div class="progress">
      <div class="progress-bar" role="progressbar"></div>
    </div>
    <div class="progress-text"></div>
  </div>

  <button type="submit">อัปโหลด</button>
</form>

4. เหตุการณ์ระหว่างอัปโหลด

// ติดตามความคืบหน้าการอัปโหลด
document.addEventListener('form:upload-progress', (e) => {
  const { loaded, total, percent } = e.detail;

  console.log(`อัปโหลดไปแล้ว: ${percent}%`);
  console.log(`ข้อมูลที่ส่งแล้ว: ${loaded} / ทั้งหมด: ${total}`);

  // อัปเดต UI ที่สร้างเอง
  updateProgressBar(percent);
});

5. การตรวจสอบไฟล์ก่อนอัปโหลด

<input
  type="file"
  name="avatar"
  accept="image/jpeg,image/png"
  data-max-size="5242880"
  data-error-file-size="ไฟล์ต้องมีขนาดไม่เกิน 5MB"
  data-error-file-type="อนุญาตเฉพาะไฟล์ JPEG และ PNG">

<script>
// ตัวตรวจสอบไฟล์แบบกำหนดเอง
FormManager.registerValidator('file-size', (value, element, param) => {
  if (!element.files || element.files.length === 0) return true;

  const maxSize = parseInt(param);
  const file = element.files[0];

  return file.size <= maxSize;
}, 'ขนาดไฟล์เกินกว่าที่กำหนด');
</script>

ข้อมูลฟอร์ม

1. ดึงข้อมูลจากฟอร์ม

// เข้าถึงอินสแตนซ์ของฟอร์ม
const form = FormManager.getInstance('contact');

// ดึงค่าฟอร์มในรูปแบบ JSON
const data = FormManager.getValues('contact');
console.log(data);
// { name: "John", email: "john@example.com" }

// หรืออ้างอิงจาก element โดยตรง
const formElement = document.querySelector('form[data-form="contact"]');
const data2 = FormManager.getValues(formElement);

2. ตั้งค่าข้อมูลในฟอร์ม

// เข้าถึงอินสแตนซ์ของฟอร์ม
const instance = FormManager.getInstance('profile');

// กำหนดค่าฟิลด์ต่างๆ
instance.elements.get('name').value = 'John Doe';
instance.elements.get('email').value = 'john@example.com';
instance.state.data.name = 'John Doe';
instance.state.data.email = 'john@example.com';

// กระตุ้นให้เกิดเหตุการณ์ change
instance.elements.get('name').dispatchEvent(new Event('change'));

3. รีเซ็ตฟอร์ม

// รีเซ็ตฟอร์มกลับสู่สถานะเริ่มต้น
const instance = FormManager.getInstance('contact');
FormManager.resetForm(instance);

// หรือผ่าน element โดยตรง
const formElement = document.querySelector('form[data-form="contact"]');
const instance2 = FormManager.getInstanceByElement(formElement);
FormManager.resetForm(instance2);

4. ตรวจสอบสถานะของฟอร์ม

const instance = FormManager.getInstance('contact');

// ตรวจสอบว่ามีการแก้ไขข้อมูลหรือไม่
console.log(instance.state.modified); // true/false

// ตรวจสอบว่าฟอร์มผ่านการตรวจสอบหรือไม่
console.log(instance.state.valid); // true/false

// ตรวจสอบว่าฟอร์มกำลังส่งอยู่หรือไม่
console.log(instance.state.submitting); // true/false

// ดึงรายการข้อผิดพลาด
console.log(instance.state.errors);
// { email: "อีเมลไม่ถูกต้อง", password: "สั้นเกินไป" }

// จำนวนครั้งที่ส่งฟอร์ม
console.log(instance.state.submitCount); // 2

// เวลาในการส่งฟอร์มครั้งล่าสุด (timestamp)
console.log(instance.state.lastSubmitTime); // 1635789012345

การคงค่าฟิลด์

1. บันทึกลง localStorage

<form data-form="preferences">
  <!-- บันทึกลง localStorage อัตโนมัติ -->
  <input
    type="text"
    name="username"
    data-persist="true"
    data-persist-on="submit">

  <select
    name="language"
    data-persist="true"
    data-persist-on="submit">
    <option value="en">English</option>
    <option value="th">ไทย</option>
  </select>

  <button type="submit">บันทึก</button>
</form>

2. บันทึกลงคุกกี้

<form data-form="settings">
  <!-- บันทึกลงคุกกี้ -->
  <input
    type="checkbox"
    name="remember"
    data-persist="cookie"
    data-persist-ttl-days="30">

  <button type="submit">บันทึก</button>
</form>

3. ตั้งชื่อคีย์เอง

<input
  type="text"
  name="email"
  data-persist="true"
  data-persist-key="user_email_pref">

4. กำหนดอายุข้อมูล (TTL)

<!-- หมดอายุหลังจาก 7 วัน -->
<input
  type="text"
  name="search"
  data-persist="true"
  data-persist-ttl-days="7">

5. จำฉันไว้ (สำหรับฟอร์มล็อกอิน)

<form data-form="login" data-action="/api/auth/login">
  <input type="text" name="username" required>
  <input type="password" name="password" required>

  <!-- ช่องเลือกเพื่อจำชื่อผู้ใช้ -->
  <label>
    <input type="checkbox" name="remember" id="remember">
    จำฉันไว้
  </label>

  <button type="submit">เข้าสู่ระบบ</button>
</form>

พฤติกรรม:

  • เมื่อเลือก: จะบันทึกชื่อผู้ใช้ไว้ใน localStorage
  • ครั้งถัดไปจะเติมชื่อผู้ใช้อัตโนมัติ
  • ไม่บันทึกรหัสผ่าน เพื่อความปลอดภัย

การผสานระบบความปลอดภัย

1. การป้องกัน CSRF

<form data-form="secure" data-csrf="true">
  <!-- ระบบจะเพิ่ม CSRF token ให้อัตโนมัติ -->
  <input type="hidden" name="_token" value="...">

  <input type="text" name="data">
  <button type="submit">ส่ง</button>
</form>

การตรวจจับอัตโนมัติ:

<!-- อ่าน CSRF token จาก meta tag -->
<meta name="csrf-token" content="your_csrf_token">

<!-- หรือจากคุกกี้ -->
<!-- คุกกี้ XSRF-TOKEN -->

2. การจำกัดความถี่ในการส่ง

<form
  data-form="contact"
  data-rate-limit="true"
  data-rate-limit-limit="5"
  data-rate-limit-window="60">

  <!-- ส่งได้สูงสุด 5 ครั้งใน 60 วินาที -->
  <button type="submit">ส่ง</button>
</form>

จัดการเมื่อถูกจำกัด:

document.addEventListener('form:rateLimit', (e) => {
  const { formId, rateLimitResult } = e.detail;
  const { retryAfter } = rateLimitResult;

  showNotification(
    `ส่งคำร้องมากเกินไป กรุณาลองใหม่ในอีก ${retryAfter} วินาที`,
    'warning'
  );
});

3. การทำความสะอาดข้อมูลก่อนส่ง

<form
  data-form="secure"
  data-sanitize-input="true">

  <!-- ระบบจะทำความสะอาดข้อมูลก่อนส่ง -->
  <input type="text" name="comment">
  <button type="submit">ส่ง</button>
</form>

4. เหตุการณ์ด้านความปลอดภัย

// เมื่อเกิดข้อผิดพลาดเกี่ยวกับ CSRF
document.addEventListener('security:error', (e) => {
  const { message, status, code } = e.detail;

  if (code === 'csrf_invalid') {
    console.log('การตรวจสอบ CSRF ไม่ผ่าน');
    // รีเฟรช CSRF token ใหม่
  }
});

// เมื่อรีเฟรช CSRF สำเร็จ
document.addEventListener('csrf:refreshed', (e) => {
  const { token } = e.detail;
  console.log('ได้รับ CSRF token ใหม่:', token);
});

ตัวอย่างการใช้งาน

1. ฟอร์มเข้าสู่ระบบ

<form
  data-form="login"
  data-action="/api/auth/login"
  data-method="POST"
  data-ajax-submit="true"
  data-use-intended-url="true"
  data-redirect="/dashboard"
  data-auto-fill-intended-url="true">

  <div class="form-group">
    <label>อีเมล:</label>
    <input
      type="email"
      name="username"
      required
      autofocus
      data-error-required="กรุณากรอกอีเมลของคุณ">
  </div>

  <div class="form-group">
    <label>รหัสผ่าน:</label>
    <input
      type="password"
      name="password"
      required
      minlength="6"
      data-error-required="กรุณากรอกรหัสผ่าน"
      data-error-minlength="รหัสผ่านต้องมีอย่างน้อย 6 ตัวอักษร">
  </div>

  <div class="form-group">
    <label>
      <input type="checkbox" name="remember" id="remember">
      จำฉันไว้
    </label>
  </div>

  <!-- Intended URL (auto-filled) -->
  <input type="hidden" name="intended_url">

  <button type="submit">เข้าสู่ระบบ</button>
</form>

<script>
// จัดการเมื่อเข้าสู่ระบบสำเร็จ
document.addEventListener('form:submitted', async (e) => {
  const { formId, response } = e.detail;

  if (formId === 'login' && response.success) {
    // AuthManager จะจัดการเก็บโทเค็นให้เอง
    showNotification('เข้าสู่ระบบสำเร็จ!', 'success');

    // FormManager จะเปลี่ยนหน้าให้เองตามที่ตั้งค่าไว้
  }
});

// จัดการกรณีเข้าสู่ระบบไม่สำเร็จ
document.addEventListener('form:error', (e) => {
  const { formId, response } = e.detail;

  if (formId === 'login') {
    if (response.status === 401) {
      showNotification('ข้อมูลเข้าสู่ระบบไม่ถูกต้อง', 'error');
    } else {
      showNotification(response.message || 'เข้าสู่ระบบไม่สำเร็จ', 'error');
    }
  }
});
</script>

2. ฟอร์มลงทะเบียน

<form
  data-form="register"
  data-action="/api/auth/register"
  data-redirect="/welcome"
  data-success-message="สร้างบัญชีสำเร็จ!">

  <div class="form-group">
    <label>ชื่อ:</label>
    <input
      type="text"
      name="name"
      required
      minlength="2"
      data-error-required="กรุณากรอกชื่อของคุณ"
      data-error-minlength="ชื่อต้องมีอย่างน้อย 2 ตัวอักษร">
  </div>

  <div class="form-group">
    <label>อีเมล:</label>
    <input
      type="email"
      name="email"
      required
      data-validate-unique-email="true"
      data-error-required="กรุณากรอกอีเมลของคุณ"
      data-error-email="กรุณากรอกอีเมลให้ถูกต้อง"
      data-error-unique-email="อีเมลนี้ถูกลงทะเบียนไว้แล้ว">
  </div>

  <div class="form-group">
    <label>ชื่อผู้ใช้:</label>
    <input
      type="text"
      name="username"
      required
      minlength="3"
      maxlength="20"
      pattern="^[a-zA-Z0-9_]+$"
      data-validate-unique-username="true"
      data-error-required="กรุณาเลือกชื่อผู้ใช้"
      data-error-pattern="ชื่อผู้ใช้ต้องเป็นตัวอักษร ตัวเลข หรือขีดล่าง"
      data-error-unique-username="ชื่อผู้ใช้นี้ถูกใช้แล้ว">
  </div>

  <div class="form-group">
    <label>รหัสผ่าน:</label>
    <input
      type="password"
      name="password"
      id="password"
      required
      minlength="8"
      data-validate-strong-password="true"
      data-error-required="กรุณากรอกรหัสผ่าน"
      data-error-minlength="รหัสผ่านต้องมีอย่างน้อย 8 ตัวอักษร"
      data-error-strong-password="รหัสผ่านต้องมีตัวพิมพ์ใหญ่ ตัวพิมพ์เล็ก ตัวเลข และอักขระพิเศษ">
  </div>

  <div class="form-group">
    <label>ยืนยันรหัสผ่าน:</label>
    <input
      type="password"
      name="confirm_password"
      required
      data-validate-match="password"
      data-error-required="กรุณายืนยันรหัสผ่าน"
      data-error-match="รหัสผ่านทั้งสองไม่ตรงกัน">
  </div>

  <div class="form-group">
    <label>
      <input
        type="checkbox"
        name="terms"
        required
        data-error-required="กรุณายอมรับเงื่อนไขการใช้งาน">
      ฉันยอมรับ <a href="/terms">ข้อกำหนดการให้บริการ</a>
    </label>
  </div>

  <button type="submit">สร้างบัญชี</button>
</form>

<script>
// ตัวตรวจสอบเพิ่มเติมสำหรับฟอร์มลงทะเบียน
FormManager.registerValidator('unique-email', async (value) => {
  const response = await fetch(`/api/check-email?email=${value}`);
  const data = await response.json();
  return data.available;
}, 'อีเมลนี้ถูกลงทะเบียนไว้แล้ว');

FormManager.registerValidator('unique-username', async (value) => {
  const response = await fetch(`/api/check-username?username=${value}`);
  const data = await response.json();
  return data.available;
}, 'ชื่อผู้ใช้นี้ถูกใช้แล้ว');

FormManager.registerValidator('strong-password', (value) => {
  // ต้องประกอบด้วย: ตัวพิมพ์ใหญ่ ตัวพิมพ์เล็ก ตัวเลข และอักขระพิเศษ
  const hasUpper = /[A-Z]/.test(value);
  const hasLower = /[a-z]/.test(value);
  const hasNumber = /[0-9]/.test(value);
  const hasSpecial = /[!@#$%^&*]/.test(value);

  return hasUpper && hasLower && hasNumber && hasSpecial;
}, 'รหัสผ่านต้องมีตัวพิมพ์ใหญ่ ตัวพิมพ์เล็ก ตัวเลข และอักขระพิเศษ');
</script>

3. ฟอร์มติดต่อ

<form
  data-form="contact"
  data-action="/api/contact"
  data-reset-after-submit="true"
  data-success-message="ขอบคุณค่ะ! เราจะติดต่อกลับโดยเร็ว"
  data-show-success-in-notification="true">

  <div class="row">
    <div class="col-md-6">
      <label>ชื่อ:</label>
      <input
        type="text"
        name="name"
        required
        data-error-required="กรุณากรอกชื่อของคุณ">
    </div>

    <div class="col-md-6">
      <label>อีเมล:</label>
      <input
        type="email"
        name="email"
        required
        data-error-required="กรุณากรอกอีเมลของคุณ"
        data-error-email="กรุณากรอกอีเมลให้ถูกต้อง">
    </div>
  </div>

  <div class="form-group">
    <label>หัวข้อ:</label>
    <select name="subject" required data-error-required="กรุณาเลือกหัวข้อที่ต้องการติดต่อ">
      <option value="">-- เลือกหัวข้อ --</option>
      <option value="general">สอบถามทั่วไป</option>
      <option value="support">ต้องการความช่วยเหลือทางเทคนิค</option>
      <option value="sales">ติดต่อฝ่ายขาย</option>
      <option value="feedback">ข้อเสนอแนะ</option>
    </select>
  </div>

  <div class="form-group">
    <label>ข้อความ:</label>
    <textarea
      name="message"
      rows="5"
      required
      minlength="10"
      maxlength="1000"
      data-error-required="กรุณากรอกข้อความของคุณ"
      data-error-minlength="ข้อความต้องมีอย่างน้อย 10 ตัวอักษร"
      data-error-maxlength="ข้อความต้องไม่เกิน 1,000 ตัวอักษร"></textarea>
    <div class="form-text">
      <span id="charCount">0</span> / 1,000 ตัวอักษร
    </div>
  </div>

  <button type="submit">ส่งข้อความ</button>
</form>

<script>
// ตัวนับจำนวนตัวอักษร
const textarea = document.querySelector('textarea[name="message"]');
const charCount = document.getElementById('charCount');

textarea.addEventListener('input', () => {
  charCount.textContent = textarea.value.length;

  if (textarea.value.length > 1000) {
    charCount.style.color = 'red';
  } else {
    charCount.style.color = '';
  }
});
</script>

4. ฟอร์มอัปเดตโปรไฟล์

<form
  data-form="profile"
  data-action="/api/profile"
  data-method="PUT"
  data-success-message="อัปเดตโปรไฟล์สำเร็จ!">

  <div class="form-group">
    <label>รูปโปรไฟล์:</label>
    <input
      type="file"
      name="avatar"
      accept="image/*"
      data-max-size="5242880"
      data-error-file-size="ไฟล์ต้องมีขนาดไม่เกิน 5MB">

    <div class="current-avatar">
      <img id="avatarPreview" src="/uploads/avatar.jpg" alt="ภาพโปรไฟล์">
    </div>
  </div>

  <div class="form-group">
    <label>ชื่อ:</label>
    <input
      type="text"
      name="name"
      value="John Doe"
      required>
  </div>

  <div class="form-group">
    <label>อีเมล:</label>
    <input
      type="email"
      name="email"
      value="john@example.com"
      required>
  </div>

  <div class="form-group">
    <label>ประวัติย่อ:</label>
    <textarea
      name="bio"
      rows="4"
      maxlength="500">นักพัฒนาเว็บและนักออกแบบ</textarea>
  </div>

  <div class="form-group">
    <label>เว็บไซต์:</label>
    <input
      type="url"
      name="website"
      value="https://johndoe.com"
      data-error-url="กรุณากรอก URL ให้ถูกต้อง">
  </div>

  <button type="submit">อัปเดตโปรไฟล์</button>
</form>

<script>
// แสดงตัวอย่างรูปก่อนอัปโหลด
const avatarInput = document.querySelector('input[name="avatar"]');
const avatarPreview = document.getElementById('avatarPreview');

avatarInput.addEventListener('change', (e) => {
  const file = e.target.files[0];
  if (file) {
    const reader = new FileReader();
    reader.onload = (event) => {
      avatarPreview.src = event.target.result;
    };
    reader.readAsDataURL(file);
  }
});

// จัดการเมื่ออัปเดตสำเร็จ
document.addEventListener('form:submitted', (e) => {
  const { formId, response } = e.detail;

  if (formId === 'profile' && response.success) {
    // ปรับ UI ด้วยข้อมูลใหม่
    if (response.data.avatar) {
      avatarPreview.src = response.data.avatar;
    }
  }
});
</script>

5. ฟอร์มหลายขั้นตอน

<form
  data-form="wizard"
  data-action="/api/onboarding"
  data-validate-on-submit="false">

  <!-- ขั้นที่ 1: ข้อมูลส่วนตัว -->
  <div class="step" data-step="1">
    <h3>ขั้นที่ 1: ข้อมูลส่วนบุคคล</h3>

    <div class="form-group">
      <label>ชื่อ:</label>
      <input type="text" name="name" required>
    </div>

    <div class="form-group">
      <label>อีเมล:</label>
      <input type="email" name="email" required>
    </div>

    <button type="button" class="btn-next">ถัดไป</button>
  </div>

  <!-- ขั้นที่ 2: ข้อมูลบัญชี -->
  <div class="step" data-step="2" style="display:none">
    <h3>ขั้นที่ 2: ตั้งค่าบัญชี</h3>

    <div class="form-group">
      <label>ชื่อผู้ใช้:</label>
      <input type="text" name="username" required>
    </div>

    <div class="form-group">
      <label>รหัสผ่าน:</label>
      <input type="password" name="password" id="password" required>
    </div>

    <div class="form-group">
      <label>ยืนยันรหัสผ่าน:</label>
      <input
        type="password"
        name="confirm_password"
        data-validate-match="password"
        required>
    </div>

    <button type="button" class="btn-prev">ย้อนกลับ</button>
    <button type="button" class="btn-next">ถัดไป</button>
  </div>

  <!-- ขั้นที่ 3: การตั้งค่าความชอบ -->
  <div class="step" data-step="3" style="display:none">
    <h3>ขั้นที่ 3: การตั้งค่าความชอบ</h3>

    <div class="form-group">
      <label>ภาษา:</label>
      <select name="language">
        <option value="en">English</option>
        <option value="th">ไทย</option>
      </select>
    </div>

    <div class="form-group">
      <label>การแจ้งเตือน:</label>
      <label>
        <input type="checkbox" name="notifications[]" value="email">
        อีเมล
      </label>
      <label>
        <input type="checkbox" name="notifications[]" value="sms">
        SMS
      </label>
    </div>

    <button type="button" class="btn-prev">ย้อนกลับ</button>
    <button type="submit">เสร็จสิ้น</button>
  </div>

  <!-- ตัวบอกความคืบหน้า -->
  <div class="progress-steps">
    <div class="step-indicator active" data-step="1">1</div>
    <div class="step-indicator" data-step="2">2</div>
    <div class="step-indicator" data-step="3">3</div>
  </div>
</form>

<script>
let currentStep = 1;
const totalSteps = 3;

// ปุ่มถัดไป
document.querySelectorAll('.btn-next').forEach(btn => {
  btn.addEventListener('click', async () => {
    // ตรวจสอบขั้นตอนปัจจุบัน
    const stepElement = document.querySelector(`.step[data-step="${currentStep}"]`);
    const fields = stepElement.querySelectorAll('input, select, textarea');

    let valid = true;
    for (const field of fields) {
      const instance = FormManager.getInstance('wizard');
      const isValid = await FormManager.validateField(instance, field);
      if (!isValid) {
        valid = false;
        break;
      }
    }

    if (valid && currentStep < totalSteps) {
      // ซ่อนขั้นตอนปัจจุบัน
      stepElement.style.display = 'none';

      // แสดงขั้นตอนถัดไป
      currentStep++;
      document.querySelector(`.step[data-step="${currentStep}"]`).style.display = 'block';

      // อัปเดตตัวบอกความคืบหน้า
      document.querySelector(`.step-indicator[data-step="${currentStep}"]`).classList.add('active');
    }
  });
});

// ปุ่มย้อนกลับ
document.querySelectorAll('.btn-prev').forEach(btn => {
  btn.addEventListener('click', () => {
    if (currentStep > 1) {
      // ซ่อนขั้นตอนปัจจุบัน
      document.querySelector(`.step[data-step="${currentStep}"]`).style.display = 'none';

      // แสดงขั้นตอนก่อนหน้า
      currentStep--;
      document.querySelector(`.step[data-step="${currentStep}"]`).style.display = 'block';

      // อัปเดตตัวบอกความคืบหน้า
      document.querySelector(`.step-indicator[data-step="${currentStep + 1}"]`).classList.remove('active');
    }
  });
});
</script>

6. ฟิลด์แบบไดนามิก

<form data-form="dynamic" data-action="/api/contacts">
  <div id="contactsContainer">
    <div class="contact-row">
      <input type="text" name="contacts[0][name]" placeholder="ชื่อ" required>
      <input type="email" name="contacts[0][email]" placeholder="อีเมล" required>
      <button type="button" class="btn-remove" style="display:none">ลบ</button>
    </div>
  </div>

  <button type="button" id="addContact">เพิ่มผู้ติดต่อ</button>
  <button type="submit">ส่ง</button>
</form>

<script>
let contactIndex = 1;

document.getElementById('addContact').addEventListener('click', () => {
  const container = document.getElementById('contactsContainer');

  const row = document.createElement('div');
  row.className = 'contact-row';
  row.innerHTML = `
    <input type="text" name="contacts[${contactIndex}][name]" placeholder="ชื่อ" required>
    <input type="email" name="contacts[${contactIndex}][email]" placeholder="อีเมล" required>
    <button type="button" class="btn-remove">ลบ</button>
  `;

  container.appendChild(row);
  contactIndex++;

  // แสดงปุ่มลบ
  document.querySelectorAll('.btn-remove').forEach(btn => {
    btn.style.display = 'inline-block';
  });

  // สแกนฟอร์มใหม่เพื่อเตรียมฟิลด์ที่เพิ่มขึ้น
  FormManager.scan(container);
});

// ลบผู้ติดต่อ
document.addEventListener('click', (e) => {
  if (e.target.classList.contains('btn-remove')) {
    e.target.closest('.contact-row').remove();

    // ซ่อนปุ่มลบเมื่อเหลือเพียง 1 รายการ
    const rows = document.querySelectorAll('.contact-row');
    if (rows.length === 1) {
      rows[0].querySelector('.btn-remove').style.display = 'none';
    }
  }
});
</script>

เอกสารอ้างอิง API

Methods

init(options)

เริ่มต้น FormManager พร้อมตัวเลือกการตั้งค่า

พารามิเตอร์:

  • options (Object) - ตัวเลือกการตั้งค่า

ค่าที่ส่งกลับ: Promise<FormManager>

ตัวอย่าง:

await FormManager.init({
  debug: false,
  ajaxSubmit: true,
  autoValidate: true
});

initForm(form)

เริ่มต้นฟอร์มตามที่ระบุ

พารามิเตอร์:

  • form (HTMLFormElement) - อิลิเมนต์ฟอร์ม

ค่าที่ส่งกลับ: Object - อินสแตนซ์ของฟอร์ม

ตัวอย่าง:

const formElement = document.querySelector('form[data-form="contact"]');
const instance = FormManager.initForm(formElement);

getInstance(formId)

ดึงอินสแตนซ์ของฟอร์มด้วยรหัส

พารามิเตอร์:

  • formId (string) - ค่าจากแอตทริบิวต์ data-form

ค่าที่ส่งกลับ: Object|undefined - อินสแตนซ์ของฟอร์ม

ตัวอย่าง:

const instance = FormManager.getInstance('contact');
console.log(instance.state.valid);

getInstanceByElement(formElement)

ดึงอินสแตนซ์จากอิลิเมนต์ฟอร์ม

พารามิเตอร์:

  • formElement (HTMLFormElement) - อิลิเมนต์ฟอร์ม

ค่าที่ส่งกลับ: Object|null - อินสแตนซ์ของฟอร์ม

ตัวอย่าง:

const form = document.querySelector('form');
const instance = FormManager.getInstanceByElement(form);

getValues(identifier)

ดึงข้อมูลฟอร์มในรูปแบบอ็อบเจ็กต์ปกติ

พารามิเตอร์:

  • identifier (string|HTMLFormElement|Object) - รหัสฟอร์ม อิลิเมนต์ หรืออินสแตนซ์

ค่าที่ส่งกลับ: Object|null - ข้อมูลฟอร์ม

ตัวอย่าง:

const data = FormManager.getValues('contact');
console.log(data);
// { name: "John", email: "john@example.com" }

validateForm(instance)

ตรวจสอบความถูกต้องของฟอร์มทั้งชุด

พารามิเตอร์:

  • instance (Object) - อินสแตนซ์ของฟอร์ม

ค่าที่ส่งกลับ: Promise<boolean> - เป็นจริงเมื่อข้อมูลถูกต้อง

ตัวอย่าง:

const instance = FormManager.getInstance('contact');
const isValid = await FormManager.validateForm(instance);

if (isValid) {
  console.log('ฟอร์มผ่านการตรวจสอบ');
} else {
  console.log('พบข้อผิดพลาดในการตรวจสอบ:', instance.state.errors);
}

validateField(instance, field)

ตรวจสอบความถูกต้องของฟิลด์เดี่ยว

พารามิเตอร์:

  • instance (Object) - อินสแตนซ์ของฟอร์ม
  • field (HTMLElement) - อิลิเมนต์ฟิลด์

ค่าที่ส่งกลับ: Promise<boolean> - เป็นจริงเมื่อฟิลด์ถูกต้อง

ตัวอย่าง:

const instance = FormManager.getInstance('contact');
const emailField = document.querySelector('input[name="email"]');
const isValid = await FormManager.validateField(instance, emailField);

registerValidator(name, fn, defaultMessage)

ลงทะเบียนตัวตรวจสอบแบบกำหนดเอง

พารามิเตอร์:

  • name (string) - ชื่อตัวตรวจสอบ
  • fn (Function) - ฟังก์ชันตรวจสอบ
  • defaultMessage (string) - ข้อความผิดพลาดเริ่มต้น

ค่าที่ส่งกลับ: void

ตัวอย่าง:

FormManager.registerValidator('username', (value, element) => {
  return /^[a-zA-Z0-9_]+$/.test(value);
}, 'ชื่อผู้ใช้ต้องประกอบด้วยตัวอักษร ตัวเลข หรือขีดล่างเท่านั้น');

resetForm(instance)

รีเซ็ตฟอร์มกลับสู่สถานะเริ่มต้น

พารามิเตอร์:

  • instance (Object) - อินสแตนซ์ของฟอร์ม

ค่าที่ส่งกลับ: void

ตัวอย่าง:

const instance = FormManager.getInstance('contact');
FormManager.resetForm(instance);

destroyForm(instance)

ยกเลิกอินสแตนซ์ของฟอร์มและทำความสะอาดทรัพยากร

พารามิเตอร์:

  • instance (Object) - อินสแตนซ์ของฟอร์ม

ค่าที่ส่งกลับ: void

ตัวอย่าง:

const instance = FormManager.getInstance('contact');
FormManager.destroyForm(instance);

scan(container)

สแกนหาฟอร์มภายในคอนเทนเนอร์และเริ่มต้นการทำงาน

พารามิเตอร์:

  • container (HTMLElement) - คอนเทนเนอร์ (ค่าเริ่มต้น: document)

ค่าที่ส่งกลับ: Array<HTMLFormElement> - ฟอร์มที่พบ

ตัวอย่าง:

// สแกนทั้งเอกสาร
FormManager.scan();

// สแกนเฉพาะคอนเทนเนอร์
const modal = document.getElementById('modal');
FormManager.scan(modal);

แนวทางปฏิบัติที่แนะนำ

1. ระบุแอตทริบิวต์ data-form เสมอ

<!-- ✅ ดี - ระบุว่าฟอร์มนี้ต้องให้ FormManager จัดการ -->
<form data-form="contact">
  <!-- ฟิลด์ของฟอร์ม -->
</form>

<!-- ❌ ไม่ดี - จะไม่ถูกเริ่มต้น -->
<form>
  <!-- FormManager จะไม่จัดการฟอร์มนี้ -->
</form>

2. ระบุข้อความผิดพลาดให้ชัดเจน

<!-- ✅ ดี - กำหนดข้อความผิดพลาดเฉพาะเจาะจง -->
<input
  type="email"
  name="email"
  required
  data-error-required="กรุณากรอกอีเมลของคุณ"
  data-error-email="กรุณากรอกอีเมลให้ถูกต้อง">

<!-- ❌ ไม่ดี - ใช้ข้อความทั่วไป -->
<input type="email" name="email" required>

3. ใช้ตัวตรวจสอบให้เหมาะสม

<!-- ✅ ดี - เลือกตัวตรวจสอบที่ตรงกับข้อมูล -->
<input
  type="email"
  name="email"
  required
  data-validate-email="true">

<input
  type="url"
  name="website"
  data-validate-url="true">

<!-- ❌ ไม่ดี - ไม่ตรวจสอบเลย -->
<input type="text" name="email">
<input type="text" name="website">

4. จัดการผลลัพธ์ให้ครบทั้งสำเร็จและผิดพลาด

// ✅ ดี - รองรับทั้งกรณีสำเร็จและผิดพลาด
document.addEventListener('form:submitted', (e) => {
  const { formId, response } = e.detail;

  if (response.success) {
    showSuccess(response.message);
  } else {
    showError(response.message);
  }
});

document.addEventListener('form:error', (e) => {
  const { formId, error } = e.detail;
  showError(error.message);
});

// ❌ ไม่ดี - จัดการเฉพาะกรณีสำเร็จ
document.addEventListener('form:submitted', () => {
  showSuccess('ดำเนินการเรียบร้อย!');
});

5. ทำความสะอาดฟอร์มไดนามิกเมื่อไม่ใช้งาน

// ✅ ดี - ทำลายอินสแตนซ์ก่อนนำออก
function removeForm(formElement) {
  FormManager.destroyFormByElement(formElement);
  formElement.remove();
}

// ❌ ไม่ดี - ลบอิลิเมนต์อย่างเดียว
function removeForm(formElement) {
  formElement.remove();  // อาจเกิด memory leak
}

Auto-Load Form Data from API

FormManager รองรับการโหลดข้อมูลจาก API อัตโนมัติเมื่อเปิดฟอร์ม โดยใช้ URL parameters เป็นตัวกำหนด

พื้นฐาน: Single Parameter

<!-- URL: /form.html?id=1 -->
<form data-form="edit-user"
      data-action="/api/v1/users"
      data-method="PUT"
      data-load-url="/api/v1/users/{id}"
      data-auto-load="true">

  <!-- Developer ต้องสร้าง input fields เอง -->
  <input type="hidden" name="id" data-from-url="id" />

  <div class="form-group">
    <label>Name</label>
    <input type="text" name="name" required />
  </div>

  <div class="form-group">
    <label>Email</label>
    <input type="email" name="email" required />
  </div>

  <div class="form-group">
    <label>Phone</label>
    <input type="tel" name="phone" />
  </div>

  <button type="submit">Update User</button>
</form>

Flow:

  1. FormManager ตรวจจับ data-auto-load="true"
  2. อ่าน data-load-url="/api/v1/users/{id}"
  3. หา input ที่มี data-from-url="id" → ดึงค่าจาก URL param ?id=1
  4. แทนที่ {id} ใน URL → /api/v1/users/1
  5. Request GET /api/v1/users/1
  6. Populate form fields ตาม name attribute

API Response:

{
  "success": true,
  "data": {
    "id": 1,
    "name": "John Doe",
    "email": "john@example.com",
    "phone": "0812345678"
  }
}

Multiple Parameters

<!-- URL: /form.html?userId=5&addressId=12 -->
<form data-form="edit-address"
      data-action="/api/v1/users/{userId}/addresses"
      data-method="PUT"
      data-load-url="/api/v1/users/{userId}/addresses/{addressId}"
      data-auto-load="true">

  <!-- Developer ต้องสร้าง input fields เอง - ไม่ต้องกังวลเรื่อง duplicate -->
  <input type="hidden" name="userId" data-from-url="userId" />
  <input type="hidden" name="addressId" data-from-url="addressId" />

  <div class="form-group">
    <label>Street</label>
    <input type="text" name="street" required />
  </div>

  <div class="form-group">
    <label>City</label>
    <input type="text" name="city" required />
  </div>

  <div class="form-group">
    <label>Postal Code</label>
    <input type="text" name="zipcode" />
  </div>

  <button type="submit">Update Address</button>
</form>

Result:

  • Load from: GET /api/v1/users/5/addresses/12
  • Submit to: PUT /api/v1/users/5/addresses/12

Optional Parameters with Defaults

<!-- URL: /form.html?reportId=5 หรือ ?reportId=5&version=2&lang=en -->
<form data-form="edit-report"
      data-load-url="/api/v1/reports/{reportId}?version={version}&lang={lang}"
      data-auto-load="true">

  <input type="hidden" name="reportId" data-from-url="reportId" data-required="true" />
  <input type="hidden" name="version" data-from-url="version" data-default="latest" />
  <input type="hidden" name="lang" data-from-url="lang" data-default="th" />

  <div class="form-group">
    <label>Report Title</label>
    <input type="text" name="title" required />
  </div>

  <div class="form-group">
    <label>Content</label>
    <textarea name="content" rows="10"></textarea>
  </div>

  <button type="submit">Save Report</button>
</form>

URL Examples:

  • ?reportId=5GET /api/v1/reports/5?version=latest&lang=th
  • ?reportId=5&version=2GET /api/v1/reports/5?version=2&lang=th
  • ?reportId=5&version=2&lang=enGET /api/v1/reports/5?version=2&lang=en

Data Attributes สำหรับ Auto-Load

บน form element:

Attribute Type Description Example
data-auto-load Boolean เปิดใช้งาน auto-load "true"
data-load-url String URL pattern (รองรับ {param}) "/api/users/{id}"
data-load-method String HTTP method สำหรับโหลด (default: GET) "GET"
data-on-load-error String Callback function เมื่อโหลดผิดพลาด "handleLoadError"

บน input element:

Attribute Type Description Example
data-from-url String ชื่อ URL parameter ที่จะดึงค่า "userId"
data-default String ค่า default ถ้าไม่มีใน URL "latest"
data-required Boolean บังคับต้องมีค่า (ถ้าไม่มีจะไม่ load) "true"

Error Handling

<form data-load-url="/api/v1/users/{userId}/orders/{orderId}"
      data-auto-load="true"
      data-on-load-error="handleLoadError">

  <input type="hidden" name="userId" data-from-url="userId" data-required="true" />
  <input type="hidden" name="orderId" data-from-url="orderId" data-required="true" />

  <!-- form fields -->
</form>

<script>
function handleLoadError(error) {
  if (error.code === 'MISSING_REQUIRED_PARAM') {
    NotificationManager.error('กรุณาระบุ User ID และ Order ID ใน URL');
  } else if (error.code === 'NOT_FOUND') {
    NotificationManager.error('ไม่พบข้อมูลที่ต้องการแก้ไข');
    setTimeout(() => {
      window.location.href = '/orders';
    }, 2000);
  } else {
    NotificationManager.error('เกิดข้อผิดพลาด: ' + error.message);
  }
}
</script>

Error Codes:

  • MISSING_REQUIRED_PARAM - ขาด parameter ที่มี data-required="true"
  • NOT_FOUND - API ตอบกลับ 404
  • UNAUTHORIZED - API ตอบกลับ 401/403
  • NETWORK_ERROR - ปัญหาการเชื่อมต่อ
  • INVALID_RESPONSE - API response format ไม่ถูกต้อง

ตัวอย่างแบบสมบูรณ์: Edit Order Item

<!DOCTYPE html>
<html>
<head>
  <title>Edit Order Item</title>
  <link rel="stylesheet" href="/Now/css/Now.css">
</head>
<body>
  <!-- URL: /edit-order-item.html?orderId=1001&itemId=3 -->

  <div class="container">
    <h1>Edit Order Item</h1>

    <form data-form="edit-order-item"
          data-action="/api/v1/orders/{orderId}/items"
          data-method="PUT"
          data-load-url="/api/v1/orders/{orderId}/items/{itemId}"
          data-auto-load="true"
          data-ajax-submit="true"
          data-on-load-error="handleLoadError">

      <!-- Hidden fields - Developer สร้างเอง -->
      <input type="hidden" name="orderId" data-from-url="orderId" data-required="true" />
      <input type="hidden" name="itemId" data-from-url="itemId" data-required="true" />

      <div class="form-group">
        <label>Product Name</label>
        <input type="text" name="productName" readonly class="readonly" />
      </div>

      <div class="form-group">
        <label>Quantity *</label>
        <input type="number" name="quantity" min="1" required />
      </div>

      <div class="form-group">
        <label>Price *</label>
        <input type="number" name="price" step="0.01" min="0" required />
      </div>

      <div class="form-group">
        <label>Discount (%)</label>
        <input type="number" name="discount" min="0" max="100" value="0" />
      </div>

      <div class="form-actions">
        <button type="button" class="button" onclick="history.back()">Cancel</button>
        <button type="submit" class="button primary">Update Item</button>
      </div>
    </form>
  </div>

  <script src="/Now/Now.js"></script>
  <script>
    // Initialize FormManager
    await FormManager.init({
      debug: true,
      ajaxSubmit: true
    });

    // Error handler
    function handleLoadError(error) {
      console.error('Load error:', error);

      if (error.code === 'MISSING_REQUIRED_PARAM') {
        NotificationManager.error('Missing order ID or item ID in URL');
        setTimeout(() => window.location.href = '/orders', 2000);
      } else if (error.code === 'NOT_FOUND') {
        NotificationManager.error('Order item not found');
        setTimeout(() => window.location.href = '/orders', 2000);
      } else {
        NotificationManager.error('Failed to load data: ' + error.message);
      }
    }

    // Success handler
    document.addEventListener('form:submitted', (e) => {
      const { formId, response } = e.detail;

      if (formId === 'edit-order-item' && response.success) {
        NotificationManager.success('Order item updated successfully');
        setTimeout(() => {
          window.location.href = '/orders?id=' + response.data.orderId;
        }, 1000);
      }
    });
  </script>
</body>
</html>

API Endpoint (PHP):

// GET /api/v1/orders/{orderId}/items/{itemId}
public function show(Request $request) {
    $orderId = $request->get('orderId')->toInt();
    $itemId = $request->get('itemId')->toInt();

    $item = $this->db->table('order_items')
        ->join('products', 'products.id', 'order_items.product_id')
        ->where([
            ['order_items.id', $itemId],
            ['order_items.order_id', $orderId]
        ])
        ->select(
            'order_items.*',
            'products.name as productName'
        )
        ->first();

    if (!$item) {
        return $this->errorResponse('Order item not found', 404);
    }

    return $this->successResponse([
        'data' => $item
    ]);
}

// PUT /api/v1/orders/{orderId}/items/{itemId}
public function update(Request $request) {
    $orderId = $request->get('orderId')->toInt();
    $itemId = $request->post('itemId')->toInt();
    $quantity = $request->post('quantity')->toInt();
    $price = $request->post('price')->toFloat();
    $discount = $request->post('discount', 0)->toFloat();

    // Validation
    if ($quantity < 1) {
        return $this->errorResponse('Quantity must be at least 1', 400);
    }

    // Update
    $updated = $this->db->update('order_items', [
        ['id', $itemId],
        ['order_id', $orderId]
    ], [
        'quantity' => $quantity,
        'price' => $price,
        'discount' => $discount,
        'updated_at' => date('Y-m-d H:i:s')
    ]);

    if ($updated) {
        return $this->successResponse([
            'data' => [
                'orderId' => $orderId,
                'itemId' => $itemId
            ],
            'actions' => [
                'type' => 'reload',
                'closeModal' => true,
                'reload' => '[data-table="orders"]'
            ]
        ], 'อัพเดทสำเร็จ');
    }

    return $this->errorResponse('Failed to update', 500);
}

สรุป Auto-Load Features

Developer Control - Developer สร้าง input fields เอง (ไม่มี auto-generation)
Multiple Parameters - รองรับหลาย URL parameters
Optional Parameters - รองรับ default values
Error Handling - Error codes และ callbacks ที่ชัดเจน
Path + Query Params - รองรับทั้ง /users/{id} และ ?version={v}
Validation - ตรวจสอบ required parameters ก่อน load
Integration - ทำงานร่วมกับ ResponseHandler และ AJAX submit

All Form Data Attributes Reference

Core Attributes

Attribute Type Default Description
data-form String - Required - Form ID สำหรับ identify
data-ajax-submit Boolean true ส่งฟอร์มแบบ AJAX
data-action String form.action URL endpoint สำหรับส่งข้อมูล
data-method String POST HTTP method (GET, POST, PUT, DELETE, PATCH)
data-confirm String - ข้อความยืนยันก่อนส่งฟอร์ม

Validation Attributes

Attribute Type Default Description
data-auto-validate Boolean true Validate ก่อนส่งฟอร์ม
data-validate-on-input Boolean true Validate ขณะพิมพ์
data-validate-on-blur Boolean true Validate เมื่อออกจาก field
data-validate-only-dirty Boolean true Validate เฉพาะ field ที่มีการแก้ไข
data-auto-focus-error Boolean true Focus ไปที่ error field แรก

Auto-Load Attributes

Attribute Type Default Description
data-auto-load Boolean false เปิดใช้งาน auto-load จาก API
data-load-url String - URL pattern สำหรับโหลด (รองรับ {param})
data-load-method String GET HTTP method สำหรับโหลด
data-on-load-error String - Callback function เมื่อโหลดผิดพลาด

Security Attributes

Attribute Type Default Description
data-csrf Boolean true เปิดใช้ CSRF protection
data-csrf-token String auto CSRF token (auto-detect ถ้าไม่ระบุ)
data-rate-limit Boolean true เปิดใช้ rate limiting
data-sanitize-input Boolean true ทำ input sanitization

UI/UX Attributes

Attribute Type Default Description
data-prevent-double-submit Boolean true ป้องกันการส่งซ้ำ
data-show-loading-on-submit Boolean true แสดง loading state
data-loading-text String "Processing..." ข้อความขณะ processing
data-reset-after-submit Boolean false Reset ฟอร์มหลังส่งสำเร็จ
data-auto-clear-errors Boolean true ล้าง errors อัตโนมัติ
data-auto-clear-errors-delay Number 5000 ระยะเวลาก่อนล้าง errors (ms)

Response Handling Attributes

Attribute Type Default Description
data-redirect String - URL สำหรับ redirect หลังส่งสำเร็จ
data-redirect-delay Number 1000 ระยะเวลาก่อน redirect (ms)
data-success-message String auto ข้อความแสดงเมื่อสำเร็จ
data-error-message String auto ข้อความแสดงเมื่อผิดพลาด
data-show-errors-inline Boolean true แสดง errors ใกล้ field
data-show-errors-in-notification Boolean false แสดง errors ใน notification

File Upload Attributes

Attribute Type Default Description
data-max-file-size Number - ขนาดไฟล์สูงสุด (bytes)
data-allowed-extensions String - นามสกุลไฟล์ที่อนุญาต (comma-separated)
data-show-progress Boolean true แสดง upload progress

Input Element Attributes (Auto-Load)

Attribute Type Default Description
data-from-url String - ชื่อ URL parameter ที่จะดึงค่า
data-default String - ค่า default ถ้าไม่มีใน URL
data-required Boolean false บังคับต้องมีค่า (ถ้าไม่มีจะไม่ load)

ปัญหาที่พบบ่อย

1. ลืมเพิ่มแอตทริบิวต์ data-form

<!-- ❌ ไม่ดี - จะไม่ถูกเริ่มต้น -->
<form action="/submit" method="POST">
  <input type="text" name="name">
  <button type="submit">ส่ง</button>
</form>

<!-- ✅ ดี - FormManager จะเริ่มต้นให้ -->
<form data-form="contact" action="/submit" method="POST">
  <input type="text" name="name">
  <button type="submit">ส่ง</button>
</form>

2. ไม่จัดการข้อผิดพลาด

// ❌ ไม่ดี - ไม่มีการจัดการข้อผิดพลาด
document.addEventListener('form:submitted', () => {
  showSuccess('ส่งเรียบร้อย!');
});

// ✅ ดี - ตรวจสอบผลลัพธ์ก่อนแสดงข้อความ
document.addEventListener('form:submitted', (e) => {
  const { response } = e.detail;

  if (response.success) {
    showSuccess(response.message);
  } else {
    showError(response.message);
  }
});

3. ไม่ตรวจสอบก่อนทำงานพิเศษ

// ❌ ไม่ดี - ส่งข้อมูลโดยไม่ตรวจสอบก่อน
document.querySelector('button.custom').addEventListener('click', async () => {
  const data = FormManager.getValues('contact');
  await sendData(data);
});

// ✅ ดี - ตรวจสอบก่อนทุกครั้ง
document.querySelector('button.custom').addEventListener('click', async () => {
  const instance = FormManager.getInstance('contact');
  const isValid = await FormManager.validateForm(instance);

  if (isValid) {
    const data = FormManager.getValues('contact');
    await sendData(data);
  }
});

4. ตั้งค่าการส่งฟอร์มไม่สอดคล้องกัน

<!-- ❌ ไม่ดี - การตั้งค่าขัดกัน -->
<form
  data-form="mixed"
  data-ajax-submit="true"
  action="/submit"
  method="POST">
  <!-- จะส่งแบบ AJAX แต่ไม่ใช้ action/method ที่ตั้งไว้ -->
</form>

<!-- ✅ ดี - ตั้งค่าให้สอดคล้อง -->
<form
  data-form="ajax"
  data-ajax-submit="true"
  data-action="/api/submit"
  data-method="POST">
  <!-- ชัดเจนว่าใช้การส่งแบบ AJAX -->
</form>

เอกสารที่เกี่ยวข้อง

  • SecurityManager.md - การป้องกัน CSRF และการจำกัดความถี่
  • AuthManager.md - การผสานงานกับระบบยืนยันตัวตน
  • RouterManager.md - การนำทางหลังส่งฟอร์ม
  • ResponseHandler.md - การประมวลผลผลลัพธ์จากเซิร์ฟเวอร์
  • NotificationManager.md - ระบบแจ้งเตือนผู้ใช้