Now.js Framework Documentation

Now.js Framework Documentation

FormManager

TH 04 Sep 2026 01:40

FormManager

ภาพรวม

FormManager คือระบบจัดการ forms ใน Now.js Framework รองรับ validation, auto-submit และ API integration

ใช้เมื่อ:

  • ต้องการ form handling
  • ต้องการ validation
  • ต้องการ AJAX submission
  • ต้องการ auto-enhance form elements

ทำไมต้องใช้:

  • ✅ Automatic validation
  • ✅ AJAX submission
  • ✅ Loading states
  • ✅ Error display
  • ✅ Auto-enhance elements
  • ✅ Multiple submit handlers
  • ✅ Declarative result binding สำหรับฟอร์มค้นหา/รายการ

การใช้งานพื้นฐาน

HTML Declarative

<form data-form="user-create"
      data-action="/api/users"
      data-method="POST">
  <input type="text" name="name" required>
  <input type="email" name="email" required>
  <button type="submit">Submit</button>
</form>

With Success Redirect

<form data-form="user-create"
      data-action="/api/users"
      data-success-redirect="/users">
  ...
</form>

With Notification

<form data-form="contact"
      data-action="/api/contact"
      data-success-message="ส่งข้อความสำเร็จ!">
  ...
</form>

Data Attributes

Attribute Description
data-form จำเป็น เปิดใช้งานฟอร์มและตั้งชื่อให้ฟอร์ม บางชื่อถูกสงวนไว้ — ดู ชื่อฟอร์มที่ถูกสงวน
data-action API endpoint
data-method HTTP method (POST, PUT, PATCH)
data-success-redirect Redirect URL on success
data-success-message Success notification
data-error-message Error notification
data-validate Enable validation
data-confirm Confirmation message
data-load-cache เปิด cache สำหรับคำขอ GET ของ data-load-api
data-load-cache-time อายุ cache ของ data-load-api หน่วยมิลลิวินาที
data-load-options-cache เปิด cache สำหรับคำขอ GET ของ data-load-options-api
data-load-options-cache-time อายุ cache ของ data-load-options-api หน่วยมิลลิวินาที
data-watch-api endpoint ของ API ที่ต้องถูกเรียกซ้ำเมื่อ field ที่กำหนดมีการเปลี่ยนค่า
data-watch-method HTTP method ของ data-watch-api (GET เป็นค่าเริ่มต้น)
data-watch-fields รายชื่อ field name/id คั่นด้วย comma ที่ต้องส่งไปยัง watched API
data-watch-trigger รายชื่อ field name/id คั่นด้วย comma ที่จะ trigger watched API เมื่อค่าเปลี่ยน
data-watch-debounce เวลาหน่วงเป็นมิลลิวินาทีก่อนเรียก watched API
data-watch-on-load เรียก watched API หนึ่งครั้งหลัง initial form data พร้อมแล้ว (true เป็นค่าเริ่มต้น, ตั้ง false ได้ถ้าไม่ต้องการ call แรก)
data-submit-target CSS selector ของ container ที่จะถูก bind ใหม่จากผลลัพธ์ AJAX สำเร็จ
data-submit-pagination-target CSS selector ของ container สำหรับวางปุ่มแบ่งหน้า
data-submit-query-params อัปเดต query string ปัจจุบันจากค่าฟอร์มหลัง AJAX submit สำเร็จ
data-submit-query-fields รายชื่อ field name คั่นด้วย comma ที่จะเขียนกลับไปยัง query string
data-submit-page-field ชื่อ field ที่ใช้เก็บเลขหน้าเมื่อกดแบ่งหน้า

ระดับฟอร์ม — การตรวจสอบและข้อความ

Attribute คำอธิบาย
data-validation เปิด/ปิดการตรวจสอบทั้งฟอร์ม (true/false)
data-validate-on-submit ตรวจสอบตอนกดส่ง (true/false)
data-error-class CSS class ที่ใส่ให้ฟิลด์ที่ไม่ผ่าน
data-error-message-class CSS class ของข้อความผิดพลาด
data-error-container CSS selector ของ container ที่จะแสดงข้อความผิดพลาดรวม
data-success-container CSS selector ของ container ที่จะแสดงข้อความสำเร็จ
data-show-errors-inline แสดงข้อความผิดพลาดใต้ฟิลด์ (true/false)
data-show-errors-in-notification แสดงข้อความผิดพลาดเป็น notification (true/false)
data-show-success-inline แสดงข้อความสำเร็จในฟอร์ม (true/false)
data-show-success-in-notification แสดงข้อความสำเร็จเป็น notification (true/false)
data-auto-clear-errors ล้างข้อความผิดพลาดอัตโนมัติ (true/false)
data-auto-clear-errors-delay หน่วงก่อนล้างข้อความผิดพลาด หน่วยมิลลิวินาที
data-auto-focus-error โฟกัสฟิลด์แรกที่ไม่ผ่านอัตโนมัติ (true/false)
data-auto-scroll-to-error เลื่อนหน้าไปยังฟิลด์แรกที่ไม่ผ่าน (true/false)
data-loading-text ข้อความบนปุ่มส่งระหว่างรอผล

ระดับฟอร์ม — ความปลอดภัยและการส่ง

Attribute คำอธิบาย
data-csrf เปิด/ปิดการแนบ CSRF token (true/false)
data-csrf-token ระบุ CSRF token เอง แทนการให้ SecurityManager หามาให้
data-csrf-header ชื่อ header ที่ใช้ส่ง CSRF token
data-sanitize-input ล้างค่าก่อนส่ง (true/false) ตั้ง false เมื่อฝั่ง server เข้ารหัสเอง
data-submit-pagination-window จำนวนเลขหน้าที่แสดงในแถบแบ่งหน้า
data-modal-options ตัวเลือกของ modal ที่ฟอร์มนี้อยู่ ในรูป JSON
data-cascade-api endpoint สำหรับโหลด option แบบ cascade ของทั้งฟอร์ม
data-load-url-params เติมค่าเริ่มต้นจาก query string (true/false)
data-url-params-required-fields รายชื่อ field ที่ต้องมีค่าใน query string คั่นด้วย comma
data-redirect URL ปลายทางหลังส่งสำเร็จ

ระดับฟิลด์ — ข้อความผิดพลาดเฉพาะฟิลด์

ใส่บน <input> <select> หรือ <textarea> เพื่อแทนข้อความมาตรฐาน

Attribute คำอธิบาย
data-error-required ข้อความเมื่อเว้นว่าง
data-error-minlength ข้อความเมื่อสั้นเกินไป รองรับ {minlength}
data-error-maxlength ข้อความเมื่อยาวเกินไป รองรับ {maxlength}
data-error-min ข้อความเมื่อค่าน้อยเกินไป รองรับ {min}
data-error-max ข้อความเมื่อค่ามากเกินไป รองรับ {max}
data-error-pattern ข้อความเมื่อรูปแบบไม่ตรง
data-error-validate ข้อความเมื่อ validator ที่กำหนดเองไม่ผ่าน
data-error-validation ข้อความสำรองเมื่อไม่ระบุข้อความเฉพาะเจาะจง
data-validate-fn ชื่อฟังก์ชันบน window ที่ใช้ตรวจค่าฟิลด์นี้
data-cascade-source ชื่อฟิลด์ต้นทางที่ฟิลด์นี้ขึ้นกับ
data-autocomplete ตั้ง true เมื่อฟิลด์นี้เป็น autocomplete เพื่อไม่ให้ระบบแย่งโฟกัส

ระดับฟิลด์ — จำค่าที่กรอกไว้

Attribute คำอธิบาย
data-persist เก็บค่าที่กรอกไว้เพื่อเติมกลับให้ครั้งหน้า
data-persist-key คีย์ที่ใช้เก็บ ถ้าไม่ระบุจะสร้างจาก data-form กับ path ปัจจุบัน
data-persist-on จังหวะที่เก็บ (submit เป็นค่าเริ่มต้น)
data-persist-ttl-days อายุของค่าที่เก็บ หน่วยวัน
data-persist-ttl อายุของค่าที่เก็บ ใช้เมื่อไม่ได้ตั้ง data-persist-ttl-days
data-persist-allow-password ยอมให้เก็บค่าของช่องรหัสผ่านด้วย ปกติถูกกันไว้

หมายเหตุ: ถ้า sort, filter, keyword, category หรือ hidden paging fields อยู่ในฟอร์มเดียวกันและมี name อยู่แล้ว ค่าพวกนี้จะถูกส่งซ้ำอัตโนมัติทุกครั้งที่ submit แบบ AJAX หรือกดแบ่งหน้า

ชื่อฟอร์มที่ถูกสงวน

data-form ทำสองอย่างพร้อมกัน คือเปิดใช้งานฟอร์มและตั้งชื่อให้ฟอร์มนั้น โดยมีชื่อที่ถูกสงวนไว้ 2 ชื่อ
ซึ่ง FormManager จะจำได้และใส่พฤติกรรมของระบบ authentication ให้เอง แม้ไม่ได้สั่งผ่าน attribute ใดเลย

data-form="login"

ฟอร์มที่ชื่อ login จะถูกถือว่าเป็นฟอร์ม login ของหน้านั้น:

สิ่งที่ FormManager ทำให้ ทำเมื่อไร
เติมชื่อผู้ใช้ที่จำไว้กลับเข้าฟอร์ม — อ่าน remember_username จาก localStorage ใส่ input[name="username"] (ถ้าไม่มีจะใช้ input[type="email"] ตัวแรก แล้วจึง input[type="text"] ตัวแรก) และติ๊ก input[name="remember"] / input#remember ให้ ทั้งนี้จะไม่เก็บรหัสผ่านไว้เลย ตอนฟอร์มเริ่มทำงาน
ทำให้ session เข้าสู่ระบบ ผ่าน AuthManager.setAuthenticatedUser() เมื่อ submit สำเร็จ และผลลัพธ์มีทั้ง data.user และ data.token
บันทึกหรือลบ remember_username ตามสถานะของช่อง remember เมื่อ submit สำเร็จ
เปลี่ยนหน้าให้เอง ผ่าน RedirectManager.afterLogin() เมื่อ submit สำเร็จ

เรื่องที่มักทำให้งง คือการเปลี่ยนหน้า: ฟอร์ม login จะไม่สนใจ data-success-redirect,
data-redirect และ data-use-intended-url
เพราะ attribute เหล่านั้นถูกอ่านใน
determineRedirectUrl() ซึ่งฟอร์ม login ไม่เคยวิ่งไปถึง ปลายทางจริงถูกเลือกโดย
RedirectManager.afterLogin() ตามลำดับนี้:

  1. data-redirect-after-login ที่ตั้งไว้บนฟอร์ม — ถ้ามี จะข้าม intended route ไปเลย
  2. intended route ที่ guard เก็บไว้ตอนเด้งผู้ใช้มาหน้า login
  3. RouterManager.config.auth.redirects.afterLogin
  4. / — ค่าเริ่มต้นของ after_login

การส่ง action ชนิด redirect กลับมาก็ไม่ได้แทนที่ลำดับนี้เช่นกัน ฟอร์มอื่นจะข้ามการ redirect ของ
ตัวเองเมื่อผลลัพธ์มี action นี้ แต่ฟอร์ม login จะเรียก RedirectManager.afterLogin() เสมอ และ
เนื่องจาก action ต้องรอ delay ของมันก่อน (ค่าเริ่มต้น 1000 ms) ส่วน afterLogin() เปลี่ยนหน้าทันที
ผลคือ action จากเซิร์ฟเวอร์แพ้ ถ้าต้องการกำหนดปลายทางเอง ให้ส่งผ่าน data-redirect-after-login
หรือตั้งค่าที่ RouterManager.config.auth.redirects.afterLogin

<!-- ฟอร์ม login จริง: ไม่ต้องใส่ attribute redirect และถึงใส่ก็ไม่มีผล -->
<form data-form="login" data-action="/api/auth/login" data-method="POST"
      data-ajax-submit="true" data-auto-fill-intended-url="true">
  <input type="email" name="username" required>
  <input type="password" name="password" required>
  <label><input type="checkbox" name="remember"> จำฉันไว้</label>
  <button type="submit">เข้าสู่ระบบ</button>
</form>
Attribute คำอธิบาย
data-redirect-after-login ปลายทางหลังเข้าสู่ระบบ ใช้แทน intended route และหน้าแรกที่ตั้งค่าไว้
data-auto-fill-intended-url เพิ่ม hidden field intended_url จาก ?redirect= / ?return_to= หรือจากเส้นทางที่เก็บไว้ใน sessionStorage.auth_intended_route แล้วใช้ค่านั้นเป็น data-redirect-after-login

data-form="register"

ถ้าผลลัพธ์ของฟอร์มชื่อ register มีทั้ง data.user และ data.token FormManager จะพาผู้ใช้ใหม่
เข้าสู่ระบบให้ด้วยวิธีเดียวกัน คือเรียก AuthManager.setAuthenticatedUser() โดยไม่ต้อง login ซ้ำ
ส่วนการเปลี่ยนหน้ายังเป็นแบบปกติ data-success-redirect และ data-redirect จึงใช้ได้ตามเดิม

ฟอร์มอื่นที่ส่ง credentials

ไม่ว่าจะเป็นฟอร์มตัวอย่าง, กล่อง sign-in ที่ฝังอยู่ในหน้าใหญ่ หรือ modal ที่ยืนยันตัวตนของอีกบัญชี
ให้ตั้งชื่ออื่นแทน เช่น signin, login-demo, account-login แล้วฟอร์มนั้นจะเป็นฟอร์มธรรมดา
ที่เปลี่ยนหน้าเฉพาะตามที่สั่งไว้เท่านั้น

Declarative Watched API Binding

ใช้รูปแบบนี้เมื่อฟอร์มมี UI ที่คำนวณต่อจากหลาย field และต้องการให้ server ส่ง payload กลับมาเพื่อ bind ผ่าน directives ปกติของ TemplateManager โดยไม่ต้องเขียน JavaScript เฉพาะหน้า

หลักการทำงาน

  1. FormManager อ่านค่าจาก field ที่ระบุไว้ใน data-watch-fields
  2. เมื่อ field ใน data-watch-trigger เปลี่ยนค่า FormManager จะเรียก data-watch-api และถ้า data-watch-on-load ไม่ได้ตั้งเป็น false จะเรียกอีกครั้งหนึ่งหลังจาก initial load เสร็จ
  3. payload ที่ตอบกลับจะถูก merge กลับเข้า form state ผ่าน setFormData()
  4. bindings เดิม เช่น data-text, data-attr, data-if และ data-for จะอัปเดตเองอัตโนมัติ

ตัวอย่าง: Derived Leave Preview

<form data-form="leave-request"
      data-load-api="api/eleave/request/get"
      data-watch-api="api/eleave/request/policy"
      data-watch-fields="id,leave_id,start_date,start_period,end_date,end_period"
      data-watch-trigger="leave_id,start_date,start_period,end_date,end_period"
      data-watch-debounce="150">

  <select name="leave_id" data-options-key="leave_id" data-attr="value:leave_id"></select>
  <input type="date" name="start_date" data-attr="value:start_date">
  <input type="date" name="end_date" data-attr="value:end_date">

  <aside data-text="preview.leave_type_detail"></aside>
  <input type="text" data-attr="value:preview.days" readonly>
  <div class="comment" data-text="preview.days_note"></div>

  <div data-if="preview.balance_summary">
    <div data-for="year in preview.balance_summary.years">
      <template>
        <div>
          <strong data-text="year.heading_text"></strong>
          <div data-text="year.summary_text"></div>
        </div>
      </template>
    </div>
  </div>
</form>

รูปแบบ Response

watched API สามารถส่ง payload ใดก็ได้ที่ setFormData() bind ได้ ตัวอย่างทั่วไปคือส่ง object ชื่อ preview พร้อม option collections ที่เกี่ยวข้อง

{
  "success": true,
  "data": {
    "preview": {
      "leave_type_detail": "Vacation • 10 days/year",
      "days": "1.5",
      "days_note": "Calculated automatically from the selected date range",
      "balance_message": "",
      "balance_summary": {
        "years": []
      }
    }
  }
}

ใช้ watched API binding เมื่อ UI เป็นฟังก์ชันตรงของค่าฟอร์มปัจจุบัน ถ้าต้องการให้ response สั่ง notification, redirect, modal หรือ form ให้ใช้ requestApi แทน

data-watch-on-load เป็น true โดย default ถ้า data-load-api ส่ง derived state ที่ต้องใช้กลับมาอยู่แล้ว ให้ตั้ง data-watch-on-load="false" เพื่อตัด initial request รอบที่สอง

ถ้าผลลัพธ์ควรถูก bind ไปยัง target ที่อยู่นอกฟอร์ม หรือต้องการควบคุม params ที่ส่งอย่าง explicit ให้ใช้ requestApi คู่กับ data-response-bind="template"

Declarative Result Binding

ใช้รูปแบบนี้เมื่อฟอร์มควร submit แบบ AJAX แล้ว bind response ลงใน result container พร้อมให้ FormManager สร้างปุ่มแบ่งหน้าให้อัตโนมัติ

หลักการทำงาน

  1. ฟอร์ม submit ผ่าน AJAX
  2. FormManager จะ normalize payload ให้เป็น schema กลางคล้าย TableManager:
{
  data: [...],
  meta: {
    page: 1,
    pageSize: 20,
    total: 23,
    totalPages: 2
  },
  filters: {},
  options: {}
}
  1. payload เต็มจะอยู่ใน context.state
  2. data หลักสำหรับแสดงรายการจะอยู่ใน context.data
  3. ปุ่มแบ่งหน้าจะถูก render ลง data-submit-pagination-target และจะอัปเดต field ตามชื่อใน data-submit-page-field ก่อน submit ฟอร์มเดิมซ้ำ

ตัวอย่าง: ฟอร์มค้นหา + card results

<form data-form="partSearch"
      action="api/parts/search/get"
      method="get"
      data-ajax-submit="true"
      data-submit-target="#partResults"
      data-submit-pagination-target="#partResultsPagination"
      data-submit-page-field="page">

  <input type="text" name="q" placeholder="ค้นหา...">
  <select name="category_id">
    <option value="">ทุกหมวดหมู่</option>
  </select>
  <input type="hidden" name="page" value="1">
  <input type="hidden" name="limit" value="20">

  <button type="submit">ค้นหา</button>
</form>

<section id="partResults" class="hidden" data-class="hidden:!submitted" data-on-load="hydratePartResults">
  <header>
    <p data-if="hasData">
      แสดง <strong data-text="pagination.from"></strong>
      -
      <strong data-text="pagination.to"></strong>
      จาก <strong data-text="meta.total"></strong> รายการ
    </p>
  </header>

  <div class="grid" data-if="hasData">
    <div data-for="item in data">
      <template>
        <article class="card">
          <h3 data-text="item.name"></h3>
          <p data-text="item.part_no"></p>
        </article>
      </template>
    </div>
  </div>

  <p data-if="empty">ไม่พบข้อมูล</p>
</section>

<div id="partResultsPagination"></div>

รูปแบบ API Response ที่คาดหวัง

{
  "success": true,
  "data": {
    "data": [{"id": 1, "name": "Gear", "part_no": "GEAR-001"}],
    "total": 23,
    "page": 1,
    "limit": 20,
    "pages": 2
  }
}

data-on-load สำหรับ hydrate เพิ่มเติม

function hydratePartResults(element, context) {
  const rows = Array.isArray(context.data) ? context.data : [];
  const meta = context.state?.meta || {};

  console.log('rows', rows);
  console.log('page', meta.page, 'of', meta.totalPages);
}

JavaScript API

// ดึง instance ของฟอร์มจาก id — คืน state object ไม่ใช่อ็อบเจกต์ที่มีเมธอด
const instance = FormManager.getInstance('my-form');

// อ่านค่าในฟอร์ม (รับ id หรือ element)
const data = FormManager.getValues('my-form');

// เติมค่าลงฟอร์ม — silent = true คือไม่ยิง event
FormManager.setFormData(instance, {
  name: 'John',
  email: 'john@example.com'
}, false);

// ล้างฟอร์ม
FormManager.resetForm(instance);

// ตรวจความถูกต้องทั้งฟอร์ม
const isValid = await FormManager.validateForm(instance);

// ส่งฟอร์มแบบ ajax
await FormManager.submitAjax(instance, FormManager.getFormData(instance));

เมธอดทุกตัวอยู่บน FormManager ไม่ใช่บนตัว instance — getInstance() คืน
state object ที่เก็บใน Map เท่านั้น เรียก instance.submit() ไม่ได้

Validation

HTML5 Validation

<input type="text" name="name" required minlength="2" maxlength="50">
<input type="email" name="email" required>
<input type="number" name="age" min="18" max="100">
<input type="url" name="website" pattern="https?://.+">

Custom Validation

<input type="text" name="username"
       data-validate="username"
       data-validate-message="Username ต้องมี 3-20 ตัวอักษร">
FormManager.registerValidator('username', (value) => {
  return /^[a-zA-Z0-9_]{3,20}$/.test(value);
});

Async Validation

FormManager.registerValidator('unique-email', async (value) => {
  const response = await ApiService.get(`/api/check-email?email=${value}`);
  return response.data.available;
});

เหตุการณ์

Event เมื่อเกิด Detail
form:init เตรียมฟอร์มเสร็จ {formId, instance}
form:submitting เริ่มส่งฟอร์ม {formId, form}
form:submitted ส่งสำเร็จ {formId, response}
form:error ส่งไม่สำเร็จ {formId, response, errors}
form:validate ตรวจฟอร์มทั้งใบเสร็จ {formId, isValid, errors, invalidFields, invalidFieldDetails}
form:validation:failed ตรวจแล้วไม่ผ่าน {formId, errors, invalidFieldDetails}
form:field:change ค่าในฟิลด์เปลี่ยน {formId, field, name, value}
form:reset รีเซ็ตฟอร์ม {formId}
form:data:set เติมข้อมูลลงฟอร์มด้วย setFormData() {formId, data}
form:destroy ทำลายอินสแตนซ์ฟอร์ม {formId}
form:upload-progress ระหว่างอัปโหลดไฟล์ {loaded, total, percent}
form:urlParamsMissing พารามิเตอร์ใน URL ที่ฟอร์มต้องใช้ขาด {formId, missingParams}
form:watch:loading เริ่มเรียก data-watch-api {formId, params}
form:watch:loaded ได้ผลจาก data-watch-api {formId, params, response}
form:watch:error เรียก data-watch-api ไม่สำเร็จ {formId, params, error}
redirect:start กำลังจะ redirect หลังส่งสำเร็จ {formId, url, delay}

เหตุการณ์ทั้งหมดนี้ส่งผ่าน EventManager.emit() ซึ่งไม่ได้ dispatch DOM event
จึงต้องดักด้วย EventManager.on() ไม่ใช่ addEventListener():

EventManager.on('form:submitted', (data) => {
  console.log('Form submitted:', data.formId, data.response);
});

API อ้างอิง

FormManager.getInstance(id)

รับ form instance

FormManager.submit(element)

Submit form

FormManager.validate(element)

Validate form

Returns: boolean

FormManager.reset()

Reset form

FormManager.getValues(identifier)

Get form values

Returns: Object

FormManager.registerValidator(name, fn, defaultMessage)

Add custom validator

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

Contact Form

<form data-form="contact"
      data-action="/api/contact"
      data-method="POST"
      data-success-message="ส่งข้อความสำเร็จ!"
      data-success-reset="true">

  <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>

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

  <button type="submit">ส่ง</button>
</form>

Edit Form

<form data-form="user-edit"
      data-action="/api/users/{{id}}"
      data-method="PUT"
      data-success-redirect="/users"
      data-confirm="ยืนยันการบันทึก?">

  <input type="hidden" name="id" value="{{id}}">
  <input type="text" name="name" value="{{name}}">
  <input type="email" name="email" value="{{email}}">

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

Search Form พร้อมแบ่งหน้าอัตโนมัติ

<form data-form="usersSearch"
      action="/api/users/search"
      method="get"
      data-ajax-submit="true"
      data-submit-target="#userResults"
      data-submit-pagination-target="#userResultsPagination"
      data-submit-page-field="page">

  <input type="text" name="search" placeholder="คำค้นหา">
  <select name="status">
    <option value="">ทุกสถานะ</option>
    <option value="active">Active</option>
    <option value="inactive">Inactive</option>
  </select>
  <input type="hidden" name="page" value="1">
  <input type="hidden" name="limit" value="20">

  <button type="submit">กรอง</button>
</form>

<div id="userResults">
  <div data-for="user in data">
    <template>
      <article>
        <strong data-text="user.name"></strong>
      </article>
    </template>
  </div>
</div>

<div id="userResultsPagination"></div>

File Upload

<form data-form="upload"
      data-action="/api/upload"
      data-enctype="multipart/form-data">

  <input type="file" name="document"
         accept=".pdf,.doc,.docx"
         required>

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

With Custom Handler

form:submitting ถูกส่งผ่าน EventManager.emit() ผู้ฟังจึงได้แค่ สังเกต
การส่งฟอร์ม แต่ ยกเลิกไม่ได้ เพราะไม่มี DOM event ให้เรียก preventDefault():

EventManager.on('form:submitting', (data) => {
  console.log('Submitting form:', data.formId);
});

ถ้าต้องการเข้าคุมการส่งฟอร์มเองทั้งหมด ให้ไม่ต้องใส่ data-form ที่อีลิเมนต์นั้น
แล้วจัดการ submit event ปกติเอง:

const form = document.getElementById('custom-form');

form.addEventListener('submit', async (e) => {
  e.preventDefault();

  const formData = FormManager.getValues(form);

  // Custom processing
  formData.processed = true;

  try {
    const response = await ApiService.post('/api/custom', formData);
    NotificationManager.success('Success!');
  } catch (error) {
    NotificationManager.error(error.message);
  }
});

CSS for Validation

/* Invalid field */
.form-group.invalid input,
.form-group.invalid textarea,
.form-group.invalid select {
  border-color: #ef4444;
}

/* Error message */
.form-group .error-message {
  color: #ef4444;
  font-size: 0.875rem;
  margin-top: 4px;
}

/* Valid field */
.form-group.valid input {
  border-color: #22c55e;
}

/* Loading state */
form.loading button[type="submit"] {
  opacity: 0.7;
  pointer-events: none;
}

form.loading button[type="submit"]::after {
  content: ' ⏳';
}

ข้อควรระวัง

⚠️ 1. ต้องมี name Attribute

<!-- ❌ ไม่มี name -->
<input type="text" id="username">

<!-- ✅ มี name -->
<input type="text" name="username">

⚠️ 2. Button type

<!-- ❌ Default สามารถ submit ได้ -->
<button>Click</button>

<!-- ✅ ระบุ type -->
<button type="submit">Submit</button>
<button type="button">Cancel</button>

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