Now.js Framework Documentation

Now.js Framework Documentation

FormManager

TH 15 Apr 2026 12:31

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-component="form"
      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-component="form"
      data-action="/api/users"
      data-success-redirect="/users">
  ...
</form>

With Notification

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

Data Attributes

Attribute Description
data-component="form" Initialize 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 ที่ใช้เก็บเลขหน้าเมื่อกดแบ่งหน้า

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

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

// Get form instance
const form = FormManager.getInstance(element);

// Submit programmatically
await form.submit();

// Reset form
form.reset();

// Set values
form.setValues({
  name: 'John',
  email: 'john@example.com'
});

// Get values
const data = form.getValues();

// Validate
const isValid = form.validate();

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.addValidator('username', (value) => {
  return /^[a-zA-Z0-9_]{3,20}$/.test(value);
});

Async Validation

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

เหตุการณ์

Event เมื่อเกิด Detail
form:submit Form submitted {form, data}
form:success Submission success {form, response}
form:error Submission error {form, error}
form:validate Validation run {form, valid}
form:reset Form reset {form}
document.getElementById('my-form').addEventListener('form:success', (e) => {
  console.log('Form submitted:', e.detail.response);
});

API อ้างอิง

FormManager.getInstance(element)

รับ form instance

FormManager.submit(element)

Submit form

FormManager.validate(element)

Validate form

Returns: boolean

FormManager.reset(element)

Reset form

FormManager.setValues(element, data)

Set form values

FormManager.getValues(element)

Get form values

Returns: Object

FormManager.addValidator(name, fn)

Add custom validator

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

Contact Form

<form data-component="form"
      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-component="form"
      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-component="form"
      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

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

form.addEventListener('form: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>

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