Now.js Framework Documentation
FormManager
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 เฉพาะหน้า
หลักการทำงาน
- FormManager อ่านค่าจาก field ที่ระบุไว้ใน
data-watch-fields - เมื่อ field ใน
data-watch-triggerเปลี่ยนค่า FormManager จะเรียกdata-watch-apiและถ้าdata-watch-on-loadไม่ได้ตั้งเป็นfalseจะเรียกอีกครั้งหนึ่งหลังจาก initial load เสร็จ - payload ที่ตอบกลับจะถูก merge กลับเข้า form state ผ่าน
setFormData() - 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 สร้างปุ่มแบ่งหน้าให้อัตโนมัติ
หลักการทำงาน
- ฟอร์ม submit ผ่าน AJAX
- FormManager จะ normalize payload ให้เป็น schema กลางคล้าย TableManager:
{
data: [...],
meta: {
page: 1,
pageSize: 20,
total: 23,
totalPages: 2
},
filters: {},
options: {}
}- payload เต็มจะอยู่ใน
context.state - data หลักสำหรับแสดงรายการจะอยู่ใน
context.data - ปุ่มแบ่งหน้าจะถูก 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>เอกสารที่เกี่ยวข้อง
- ElementManager - Form elements
- ApiService - API calls
- NotificationManager - Notifications