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-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() ตามลำดับนี้:
data-redirect-after-loginที่ตั้งไว้บนฟอร์ม — ถ้ามี จะข้าม intended route ไปเลย- intended route ที่ guard เก็บไว้ตอนเด้งผู้ใช้มาหน้า login
RouterManager.config.auth.redirects.afterLogin/— ค่าเริ่มต้นของ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 เฉพาะหน้า
หลักการทำงาน
- 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
// ดึง 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>เอกสารที่เกี่ยวข้อง
- ElementManager - Form elements
- ApiService - API calls
- NotificationManager - Notifications