Now.js Framework Documentation
TableManager
TableManager
ภาพรวม
TableManager คือระบบจัดการ data tables ใน Now.js Framework รองรับ sorting, filtering, pagination และ API integration
ใช้เมื่อ:
- ต้องการ data tables
- ต้องการ sorting และ filtering
- ต้องการ pagination
- ต้องการ CRUD operations
- ต้องการ row selection
ทำไมต้องใช้:
- ✅ Auto-load from API
- ✅ Sorting (client/server)
- ✅ Filtering
- ✅ Pagination
- ✅ Row selection (checkbox)
- ✅ Inline editing
- ✅ URL state persistence
- ✅ Sortable rows (drag & drop)
- ✅ UX ลากแถว: ghost เป็นไอคอนติดเมาส์, placeholder เส้นประ, ล็อกการลากแนวตั้ง
การใช้งานพื้นฐาน
HTML Declarative
<table data-component="table"
data-source="/api/users"
data-page-size="10">
<thead>
<tr>
<th data-field="id">ID</th>
<th data-field="name" data-sortable>Name</th>
<th data-field="email" data-sortable>Email</th>
<th data-field="created_at" data-sortable>Created</th>
</tr>
</thead>
<tbody>
<!-- Data will be rendered here -->
</tbody>
</table>With Filters
<div data-table-toolbar="users-table">
<input data-filter="name" placeholder="Search name...">
<select data-filter="status">
<option value="">All</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<table id="users-table" data-component="table" data-source="/api/users">
...
</table>ฟอร์มกรองภายนอก (data-table-filter)
คุณสามารถสร้างฟอร์มกรองแยกจากตาราง แล้วเชื่อมด้วย data-table-filter="tableId" เพื่อออกแบบ UI ได้อิสระ (รองรับตัวกรองช่วง เช่น วันที่/ราคา) โดยตารางยังจัดการโหลด/ซิงก์สถานะ, URL params และ options จาก API ให้อัตโนมัติ
<!-- External Filter Form -->
<form data-table-filter="usage" class="table_nav">
<div>
<label>Show</label>
<select name="pageSize">
<option value="10">10 entries</option>
<option value="25">25 entries</option>
<option value="50">50 entries</option>
</select>
</div>
<!-- ตัวกรองช่วงวันที่ -->
<input type="date" name="from">
<input type="date" name="to">
<!-- ค้นหา -->
<input type="search" name="search" placeholder="Search...">
<button type="submit">Apply</button>
</form>
<!-- Table -->
<table data-table="usage"
data-source="api/index/usage"
data-default-sort="created_at desc"
data-page-size="25"
data-search-columns="name,topic,reason">
<thead>
<tr>
<th data-field="id" data-sort="id">ID</th>
<th data-field="created_at" data-sort="created_at" data-format="date">Created</th>
<th data-field="topic" data-sort="topic">Topic</th>
<th data-field="name" data-sort="name">Name</th>
</tr>
</thead>
<tbody></tbody>
</table>หลักการทำงาน
data-table-filter="tableId"ต้องตรงกับdata-tableของตาราง- กด submit ฟอร์มจะรีโหลดข้อมูลตาราง (ไม่มี page reload)
- ตัวกรองช่วงใช้คู่ชื่อ เช่น
from/to,price_min/price_maxและส่งค่า non-empty ทั้งหมดไปยัง API - URL ซิงก์อัตโนมัติและกู้ค่าฟอร์มเมื่อโหลดหน้า
- ถ้า API ส่ง
optionsกลับมา (เช่น{ status: [{value:'active', text:'Active'}] }) จะเติมลง select ที่nameตรงกันอัตโนมัติ
ตัวอย่างคำขอ API
GET /api/index/usage?page=1&pageSize=25&from=2024-01-01&to=2024-12-31&search=testหมายเหตุ
- ฟิลเตอร์ใน
<th>ยังใช้งานได้; เมื่อมี external form ระบบยังอ่าน metadata จาก<th>(ค่า default / options) แต่ใช้ UI จากฟอร์มภายนอกแทน - ตั้งชื่อฟิลด์ให้ตรงกับพารามิเตอร์ที่ API รองรับ (เช่น
from,to,search)
Data Attributes
| Attribute | Description |
|---|---|
data-component="table" |
Initialize table |
data-table |
Table identifier (required for all tables) |
data-source |
API endpoint |
data-cache |
เปิด cache สำหรับคำขอ GET จาก data-source |
data-cache-time |
อายุ cache ของ data-source หน่วยมิลลิวินาที |
data-load-target |
CSS selector ของ container ที่จะถูก bind ใหม่จาก payload ของตารางหลังโหลดข้อมูลสำเร็จ |
data-page-size |
จำนวนรายการต่อหน้า |
data-show-footer |
เปิดการประมวลผล footer |
data-footer-aggregates |
mapping แบบ legacy สำหรับสร้าง aggregate อัตโนมัติใน tfoot ที่ว่าง |
เมื่อมีการแก้ไขข้อมูลผ่าน TableManager แล้วต้อง reload แหล่งข้อมูลระยะไกล การ reload รอบนั้นจะข้าม cache อัตโนมัติ เพื่อให้ข้อมูลหลัง action สดเสมอ
| data-page-sizes | ตัวเลือกจำนวนรายการต่อหน้า |
| data-default-sort | การเรียงลำดับเริ่มต้น (เช่น "name asc" หรือ "created_at desc,name asc") |
| data-show-checkbox | แสดง checkbox เลือกแถว |
| data-show-caption | แสดง/ซ่อน caption (ค่าเริ่มต้น: true, แต่ถูกปิดอัตโนมัติเมื่อใช้ data-editable-rows="true") |
| data-editable-rows | เปิดใช้การแก้ไขแถว (เพิ่ม/ลบ) |
| data-row-sortable | เปิดการลากเรียงลำดับแถว (ทำงานอิสระจาก data-editable-rows) |
| data-sortable-rows | เปิดใช้ลากวางเรียงลำดับ |
| data-url-params | บันทึกสถานะใน URL |
Declarative Load Target Binding
ใช้ data-load-target เมื่อต้องการให้ response ของตารางไป hydrate container อื่นต่อโดยไม่ต้องเขียน JavaScript เฉพาะหน้าเอง หลังจาก TableManager normalize payload แล้ว ระบบจะส่ง payload เดียวกันเข้า target ผ่าน TemplateManager.processTemplate() และ processDataOnLoad()
เหมาะกับกรณี header, summary card, ตัวนับผลลัพธ์, empty state หรือ side panel ที่ควรเปลี่ยนตาม response ชุดเดียวกับตาราง
state ที่ target ได้รับ
target จะได้รับ normalized payload บน context.state เช่น:
{
data: [...],
meta: {
page: 1,
pageSize: 25,
total: 120,
totalPages: 5
},
filters: {},
options: {},
columns: [],
params: {type: 'customer'},
tableId: 'customers',
tableSource: 'api/customer/customers',
isServerSide: true,
hasData: true,
empty: false,
raw: {/* payload ต้นฉบับ */}
}array หลักของแถวข้อมูลจะถูกเปิดให้ใช้บน context.data ด้วย
ตัวอย่าง: ให้ header เปลี่ยนตาม payload ของตาราง
<header id="customer-table-header">
<div>
<h1 data-text="params.type === 'partner' ? '{LNG_Partner}' : '{LNG_Customer}'">Customer</h1>
<p data-text="params.type === 'partner' ? 'Manage partner information' : 'Manage customer information'">
Manage customer information
</p>
</div>
<a class="btn btn-primary icon-new"
href="/customer"
data-attr="href:'/customer?type=' + (params.type || 'customer')"
data-text="params.type === 'partner' ? '{LNG_Add} {LNG_Partner}' : '{LNG_Add} {LNG_Customer}'">
{LNG_Add} {LNG_Customer}
</a>
</header>
<table data-table="customers"
data-source="api/customer/customers"
data-load-target="#customer-table-header"
data-page-size="25">
<thead>...</thead>
<tbody></tbody>
</table>หมายเหตุ
data-load-targetทำงานหลัง normalize ข้อมูลแล้ว ดังนั้น target ใช้data,meta,filters,optionsและค่าปัจจุบันใน table params ได้ทันที- target ใช้ directive ปกติของ TemplateManager ได้ เช่น
data-text,data-attr,data-class,data-if,data-forและdata-on-load - target จะถูก bind ใหม่ทุกครั้งที่ตาราง reload จาก API หรือ state ผ่าน TableManager
- ถ้า API รู้ label หรือ summary ที่ถูกต้องอยู่แล้ว ควรส่งมาใช้ตรงๆ จาก server; ใช้
params.*สำหรับค่าที่มาจาก state ของตารางหรือ URL params เป็นหลัก
ซ่อน/แสดงคอลัมน์
คุณสามารถซ่อนคอลัมน์ได้ 2 วิธี:
1. ใช้ data-visible="false" หรือ data-hidden="true" ใน HTML
<table data-table="users" data-source="/api/users">
<thead>
<tr>
<th data-field="id" data-visible="false">ID</th>
<th data-field="internal_code" data-hidden="true">Internal Code</th>
<th data-field="name">Name</th>
<th data-field="email">Email</th>
</tr>
</thead>
</table>data-hidden="true" เป็น alias ของ data-visible="false" ใช้แบบไหนใน HTML declarative ก็ได้ และทั้งคู่จะซ่อนทั้ง header กับ body cells ของคอลัมน์นั้น
2. ใช้ JavaScript แบบ dynamic
// ซ่อนคอลัมน์
TableManager.toggleColumnVisibility('users', 'email', false);
// แสดงคอลัมน์
TableManager.toggleColumnVisibility('users', 'email', true);หมายเหตุ: คอลัมน์ที่ซ่อนจะไม่แสดงทั้ง <th> และ <td> แต่ยังคงข้อมูลไว้ใน memory
Built-in Cell Formatting
ใช้ data-format บน <th> ของคอลัมน์ เมื่อต้องการให้ TableManager จัดรูปแบบข้อความที่แสดงผลโดยไม่ต้องเขียน custom formatter เอง
| Attribute | ใช้กับ | คำอธิบาย |
|---|---|---|
data-format |
HTML headers และ dynamic columns | รูปแบบ built-in เช่น text, number, currency, percent, lookup, date, time, datetime, bytes, duration, boolean, humanize, truncate |
data-decimals |
number, currency, percent |
จำนวนตำแหน่งทศนิยมแบบ fix |
data-locale |
number, currency, percent |
locale ที่ส่งต่อเข้า Intl.NumberFormat |
data-currency |
currency |
currency code/label ที่ส่งเข้า shared currency formatter |
data-pattern |
date, time, datetime |
รูปแบบการแสดงวันที่/เวลา ที่ส่งให้ Utils.date.format() |
data-options |
lookup |
fallback options สำหรับกรณีที่ไม่มี options จาก API/filter |
คอลัมน์แบบ currency ตอนนี้ใช้ formatter contract เดียวกับ template bindings แล้ว ดังนั้นพฤติกรรมเรื่องทศนิยมจะสอดคล้องกันระหว่าง data-text="amount | currency:THB:th-TH:4" และคอลัมน์ของตาราง
ตัวอย่างแบบ Static HTML
<table data-table="stockMovements" data-source="api/inventory/stockmovements">
<thead>
<tr>
<th data-field="quantity" data-format="currency" data-decimals="4">Quantity</th>
<th data-field="unit_cost" data-format="currency" data-currency="THB" data-locale="th-TH" data-decimals="2">Unit Cost</th>
<th data-field="occurred_at" data-format="date" data-pattern="D MMM YYYY">Date</th>
</tr>
</thead>
</table>ตัวอย่างแบบ Dynamic Columns
{
"columns": [
{
"field": "quantity",
"label": "Quantity",
"format": "currency",
"decimals": 4
},
{
"field": "total_cost",
"label": "Total",
"format": "currency",
"currency": "THB",
"locale": "th-TH",
"decimals": 2
}
],
"data": []
}หมายเหตุ:
lookupยัง resolve options จาก API response, filter options หรือdata-optionsตามลำดับเดิม- built-in formats ที่ไม่ใช่
lookupจะอ่าน attributes ของคอลัมน์โดยตรง จึงประกาศdecimals,currency,locale,patternแยกต่อคอลัมน์ได้ - ใช้
formatterเมื่อเซลล์ต้องการ logic ที่อิงทั้งแถว, custom HTML หรือ pipeline การ render ที่กำหนดเอง
Element ในเซลล์ (ElementManager)
เรนเดอร์ form control ภายในเซลล์ด้วย data-cell-element (หรือใช้ data-type เป็น shorthand) โดย TableManager จะส่ง attributes เข้า ElementManager และเก็บ data-element-id ไว้ที่เซลล์
Attribute ที่รองรับ: placeholder, class, read-only, disabled, required, wrapper, options (select), multiple, allow-empty, rows/cols (textarea), min/max/step (number), datalist, pattern, min-length, max-length (ใช้เมื่อค่ามากกว่า 0 เท่านั้น)
ตัวอย่าง
<table data-table="element-table" data-source="/api/users" data-editable-rows="true">
<thead>
<tr>
<th data-field="name" data-cell-element="text" data-placeholder="Full name"></th>
<th data-field="email" data-type="email" data-placeholder="user@example.com"></th>
<th data-field="status" data-cell-element="select" data-options='{"1":"Active","0":"Inactive"}' data-allow-empty="true"></th>
<th data-field="note" data-cell-element="textarea" data-rows="2" data-max-length="200" data-placeholder="Internal note"></th>
</tr>
</thead>
<tbody></tbody>
</table>Dynamic Columns
สร้าง table headers อัตโนมัติจาก API response โดยไม่ต้องกำหนด <thead> ใน HTML
Basic Usage
<table data-table="langEditor"
data-attr="data:translate"
data-dynamic-columns="true"
data-editable-rows="true">
<tbody></tbody>
</table>API Response Format
{
"translate": {
"columns": [
{
"field": "key",
"label": "Key",
"cellElement": "text",
"placeholder": "Enter key",
"i18n": true
},
{
"field": "th",
"label": "Thai",
"cellElement": "textarea",
"rows": 3,
"i18n": true
}
],
"data": [
{"key": "Welcome", "th": "ยินดีต้อนรับ"},
{"key": "Goodbye", "th": "ลาก่อน"}
]
}
}Column Metadata Attributes
สามารถกำหนด attributes ต่อไปนี้ใน column definition:
| Attribute | Type | Description |
|---|---|---|
field |
string | Required - ชื่อ field |
label |
string | Required - หัวข้อคอลัมน์ |
cellElement |
string | ประเภท element (text, textarea, select, color, etc) |
type |
string | ชนิดข้อมูล (text, number, date) |
placeholder |
string | Placeholder text |
class |
string | CSS class สำหรับ <th> |
cellClass |
string | CSS class สำหรับ <td> |
i18n |
boolean | เปิดใช้งานการแปลภาษา |
sort |
boolean | เปิดใช้งานการเรียงลำดับ |
filter |
boolean | เปิดใช้งานตัวกรอง |
formatter |
string | ชื่อ formatter function |
format |
string | รูปแบบการแสดงผล |
locale |
string | locale สำหรับ number, currency และ percent |
currency |
string | currency code/label สำหรับ format: 'currency' |
decimals |
number | จำนวนตำแหน่งทศนิยมแบบ fix สำหรับ number, currency, percent |
options |
object | ตัวเลือกสำหรับ select |
rows/cols |
number | สำหรับ textarea |
min/max/step |
number | สำหรับ number input |
minLength/maxLength |
number | ความยาวข้อความ |
pattern |
string | RegEx pattern |
readOnly |
boolean | Read-only mode |
disabled |
boolean | Disabled state |
required |
boolean | Required field |
autoNumber |
boolean | สร้างเลขลำดับแถวแบบอัตโนมัติให้คอลัมน์นี้ |
visible |
boolean | แสดง/ซ่อนคอลัมน์ (false = ซ่อน) |
hidden |
boolean | alias ของ visible: false ใช้เพื่อความเข้ากันได้กับ data-hidden="true" |
Data Binding (data-attr)
ใช้ data-attr เพื่อ bind ข้อมูลจาก nested object ใน API response หรือจาก state ของ form/page:
รูปแบบ: data-attr="data:path.to.data"
ตัวอย่าง:
<!-- Simple binding -->
<table data-table="users" data-attr="data:users"></table>
<!-- API: {"users": [...]} -->
<!-- Nested binding -->
<table data-table="langEditor" data-attr="data:translate"></table>
<!-- API: {"translate": {"columns": [...], "data": [...]}} -->
<!-- Deep nested -->
<table data-table="monthlySales" data-attr="data:report.sales.monthly"></table>
<!-- API: {"report": {"sales": {"monthly": [...]}}} -->เมื่อ bind ผ่าน data-attr นั้น TableManager รองรับได้ทั้ง:
- array ของ rows ตรงๆ โดยใช้
<thead>ที่ประกาศไว้ใน HTML - object มาตรฐานแบบ
{data: [...], columns: [...], meta: {...}}สำหรับกรณี dynamic columns และ metadata
รูปแบบนี้เหมาะกับตารางย่อยแบบ read-only ภายในฟอร์ม เช่น payment history หรือ status log เมื่อ parent form โหลด payload มาอยู่แล้ว และต้องการให้รูปแบบการ render เหมือนตารางอื่นในระบบ admin
เลขลำดับแถวอัตโนมัติ
ใช้ data-auto-number="true" บน <th> เมื่อต้องการให้ TableManager แสดงคอลัมน์เลขลำดับแบบ reusable โดยไม่ต้องให้ backend ส่ง field อย่าง row_no, payment_no หรือชื่อใกล้เคียงมาเอง
<table data-table="payments" data-attr="data:payments">
<thead>
<tr>
<th data-auto-number="true" class="center" data-cell-class="center" data-format="number">#</th>
<th data-field="payment_method">Method</th>
<th data-field="amount" data-format="number" class="right" data-cell-class="right">Amount</th>
</tr>
</thead>
<tbody></tbody>
</table>พฤติกรรม:
- ใช้เลขลำดับแบบเริ่มจาก 1
- ถ้ามี pagination จะนับต่อข้ามหน้าให้อัตโนมัติ
- ถ้าไม่มี pagination จะนับตามลำดับ row ที่ render อยู่ในหน้านั้น
ข้อดี
✅ ยืดหยุ่น - Columns ปรับตามข้อมูลจาก API
✅ Clean HTML - ไม่ต้องเขียน <th> ซ้ำๆ
✅ Dynamic - เพิ่ม/ลด columns ได้โดยไม่แก้ frontend
✅ Type-safe - กำหนด element types ได้จาก backend
ตัวอย่างการใช้งานจริง
// PHP API Response
return [
'translate' => [
'columns' => [
['field' => 'key', 'label' => 'Key', 'cellElement' => 'text'],
['field' => 'th', 'label' => 'ไทย', 'cellElement' => 'textarea'],
['field' => 'en', 'label' => 'English', 'cellElement' => 'textarea']
],
'data' => $translations
]
];TableManager จะสร้าง header อัตโนมัติ:
<thead>
<tr>
<th data-field="key" data-cell-element="text">Key</th>
<th data-field="th" data-cell-element="textarea">ไทย</th>
<th data-field="en" data-cell-element="textarea">English</th>
</tr>
</thead>
---
### การกำหนด CSS Class แยกระหว่าง Header และ Body Cells
TableManager รองรับการกำหนด CSS class แยกกันสำหรับ `<th>` (header) และ `<td>` (body cells) โดยเด็ดขาด ไม่มี fallback
#### Static HTML Table
สำหรับ table ที่กำหนด `<thead>` ใน HTML:
```html
<table data-table="myTable" data-source="/api/data">
<thead>
<tr>
<!-- ใช้ class ปกติสำหรับ <th> -->
<th data-field="id" class="header-mono">ID</th>
<!-- ใช้ data-cell-class สำหรับ <td> -->
<th data-field="name" class="header-bold" data-cell-class="text-bold">Name</th>
<!-- สามารถใช้แค่ data-cell-class อย่างเดียว -->
<th data-field="status" data-cell-class="badge center">Status</th>
</tr>
</thead>
<tbody></tbody>
</table>ผลลัพธ์:
<!-- Header -->
<th class="header-mono">ID</th>
<th class="header-bold">Name</th>
<th>Status</th>
<!-- Body -->
<td>001</td> <!-- ไม่มี class -->
<td class="text-bold">John Doe</td>
<td class="badge center">Active</td>Dynamic Columns (API Response)
สำหรับ table ที่ใช้ data-dynamic-columns="true":
{
"columns": [
{
"field": "id",
"label": "ID",
"class": "header-mono",
"cellClass": "mono small"
},
{
"field": "name",
"label": "Name",
"class": "header-bold",
"cellClass": "text-bold"
},
{
"field": "status",
"label": "Status",
"cellClass": "badge center"
}
],
"data": [...]
}ผลลัพธ์:
<!-- Header -->
<th class="header-mono">ID</th>
<th class="header-bold">Name</th>
<th>Status</th>
<!-- Body -->
<td class="mono small">001</td>
<td class="text-bold">John Doe</td>
<td class="badge center">Active</td>สรุป
| ประเภท | TH Class | TD Class |
|---|---|---|
| Static HTML | class="..." (attribute ปกติ) |
data-cell-class="..." |
| Dynamic API | "class": "..." (ใน JSON) |
"cellClass": "..." (ใน JSON) |
หมายเหตุสำคัญ:
- ⚠️ ไม่มี
data-classattribute - แยกกันเด็ดขาด ไม่มี fallback ระหว่าง th และ td
- สามารถใช้แค่ตัวใดตัวหนึ่ง หรือทั้งคู่ก็ได้
Footer และ Aggregate
แนวทางที่แนะนำคือเขียน tfoot แบบ explicit ไปเลย แล้วให้แต่ละ footer cell บอกหน้าที่ของตัวเอง วิธีนี้อ่านง่ายกว่า footer แบบอิงลำดับคอลัมน์ และเหมาะเมื่อคุณต้องการข้อความคงที่อย่าง Total, ตำแหน่งที่กำหนดเอง, colspan หรือ class เฉพาะใน footer
<table data-table="orders"
data-source="/api/orders"
data-show-footer="true">
<thead>
<tr>
<th data-field="id">ID</th>
<th data-field="customer">Customer</th>
<th data-field="status">Status</th>
<th data-field="amount" data-format="number">Amount</th>
<th data-field="created_at" data-format="date">Created</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<th data-field="id" data-aggregate="count" data-format="number" data-cell-class="center"></th>
<th colspan="2" class="right">Total</th>
<th data-field="amount" data-aggregate="sum" data-format="number" data-cell-class="right"></th>
<th data-field="created_at" data-aggregate="count" data-format="number" data-cell-class="center"></th>
</tr>
</tfoot>
</table>พฤติกรรมของ footer
tfootใช้ได้ทั้ง<th>และ<td>- ข้อความคงที่ใน cell จะถูกเก็บไว้ เช่น
Totalจะไม่ถูกคำนวณทับ - ถ้า footer cell ไม่ระบุ
data-fieldระบบจะ fallback ไปใช้คอลัมน์ปลายทางจากtheadตามตำแหน่ง sum,avg,min,maxจะทำงานเฉพาะเมื่อข้อมูลต้นทางเป็นตัวเลขทั้งหมดจริง ๆ- ถ้าข้อมูลเป็น text, date หรือปนกันจนไม่น่าเชื่อถือ ระบบจะข้าม cell นั้นแทนการเดาค่า
countจะนับค่าที่ไม่ว่างdata-cell-classใช้กับ footer ได้ด้วย จึงจัดแนว footer ให้ต่างจาก header ได้colspanและrowspanในtfootได้รับการรองรับ
Attribute สำหรับ footer cell
| Attribute | Description |
|---|---|
data-field |
field ต้นทางของ footer cell นี้ |
data-aggregate |
ประเภท aggregate: sum, avg, count, min, max หรือ custom |
data-format |
รูปแบบการแสดงผลของค่า aggregate |
data-formatter |
ชื่อ formatter function แบบกำหนดเอง |
data-cell-class |
CSS classes ที่จะใช้กับ footer cell หลัง render |
data-class |
ไม่รองรับ ให้ใช้ HTML class ปกติแทน |
data-prefix |
ข้อความหน้าค่าที่ format แล้ว |
data-suffix |
ข้อความท้ายค่าที่ format แล้ว |
data-align |
การจัดแนวข้อความเฉพาะ footer |
data-custom |
ชื่อ custom aggregate function เมื่อใช้ data-aggregate="custom" |
Custom formatter และ custom aggregate
footer cell ใช้ formatter แบบเดียวกับ body cell ได้:
<th data-field="amount"
data-aggregate="sum"
data-formatter="renderFooterMoney"></th>
<th data-field="amount"
data-aggregate="custom"
data-custom="calculateGrandTotal"></th>function renderFooterMoney(cell, value, rows, attrs) {
cell.textContent = `USD ${Number(value).toLocaleString()}`;
}
function calculateGrandTotal(rows, cell, table, attrs) {
return rows.reduce((sum, row) => sum + Number(row.amount || 0), 0);
}data-formatter จะได้รับ (cell, value, rows, attrs) โดยในโหมด footer นั้น attrs จะมี isFooter, field, aggregateType, tableId, column, table และ aggregate
Legacy auto mode
ถ้าไม่ต้องการข้อความหรือ layout แบบ explicit ยังสามารถใช้รูปแบบเก่าได้:
<table data-table="orders"
data-source="/api/orders"
data-show-footer="true"
data-footer-aggregates='{"amount":"sum","id":"count"}'>
<thead>...</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>ในโหมดนี้ TableManager จะเติมค่าให้อัตโนมัติเฉพาะ footer cell ที่ว่างและกินพื้นที่เพียง 1 คอลัมน์เท่านั้น ถ้า cell มีข้อความอยู่แล้ว หรือถูกครอบด้วย colspan ระบบจะไม่แตะ
Column Attributes
| Attribute | Description |
|---|---|
data-field |
Field name |
data-sortable |
Enable sorting |
data-type |
Data type (text, number, date) |
data-format |
Display format |
การตั้งค่า
TableManager.init({
debug: false,
urlParams: true,
pageSizes: [10, 25, 50, 100],
showCaption: true,
showCheckbox: false,
allowRowModification: false,
rowSortable: true // แสดง drag handle เมื่อ allowRowModification เป็น true
});API อ้างอิง
TableManager.initTable(element, options)
Initialize table
| Parameter | Type | Description |
|---|---|---|
element |
HTMLTableElement | Table element |
options |
object | Configuration |
TableManager.setData(tableId, data)
Set table data
| Parameter | Type | Description |
|---|---|---|
tableId |
string | Table ID |
data |
array | Data array |
TableManager.setData('users-table', [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
]);TableManager.loadTableData(tableId)
Reload data from source
await TableManager.loadTableData('users-table');TableManager.renderTable(tableId)
Re-render table
TableManager.renderTable('users-table');TableManager.setFilters(tableId, filters)
Set filter values
TableManager.setFilters('users-table', {
name: 'John',
status: 'active'
});TableManager.getSelectedRows(tableId)
Get selected row data
Returns: Array
const selected = TableManager.getSelectedRows('users-table');
console.log('Selected:', selected.length);TableManager.clearSelection(tableId)
Clear row selection
TableManager.enableRowSort(tableId)
เปิดการลากวางเรียงแถว Drag handle จะแสดงเมื่อ data-row-sortable="true" ถูกกำหนด (ไม่จำเป็นต้องเปิด data-editable-rows)
TableManager.disableRowSort(tableId)
ปิดการลากวางและซ่อน drag handle
TableManager.toggleColumnVisibility(tableId, fieldName, visible)
ซ่อน/แสดงคอลัมน์แบบ dynamic
พารามิเตอร์:
tableId(string) - รหัสตารางfieldName(string) - ชื่อฟิลด์ของคอลัมน์visible(boolean) -true= แสดง,false= ซ่อน
ตัวอย่าง:
// ซ่อนคอลัมน์ email
TableManager.toggleColumnVisibility('users', 'email', false);
// แสดงคอลัมน์ email กลับมา
TableManager.toggleColumnVisibility('users', 'email', true);การลากเรียงแถว (drag & drop)
- เปิดใช้: ใส่
data-row-sortable="true"เพื่อแสดง drag handle (⋮⋮) และเปิดการลากเรียงลำดับแถว - ไม่จำเป็นต้องใช้
data-editable-rows: คุณสมบัติทั้งสองแยกทำงานกันอย่างอิสระdata-editable-rows="true"= เปิดปุ่มเพิ่ม/ลบแถวdata-row-sortable="true"= เปิดลากเรียงลำดับแถว
- ปิดเป็นรายตาราง: เรียก
TableManager.disableRowSort(tableId) - UX ล่าสุด: ghost ขนาดเล็กตามเคอร์เซอร์, placeholder เป็นแถวเส้นประ, การลากล็อกแนวตั้งเพื่อให้ reorder แม่นยำ
- อีเวนต์:
table:row-sorted→{tableId, item, oldIndex, newIndex}
<table data-table="rows"
data-source="/api/rows"
data-row-sortable="true">
<thead>
<tr>
<th data-field="name">Name</th>
<th data-field="position">Position</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="controls">
<button onclick="TableManager.disableRowSort('rows')">ปิด drag</button>
<button onclick="TableManager.enableRowSort('rows')">เปิด drag</button>
</div>TableManager.exportData(tableId, format, options)
ส่งออกข้อมูลตารางเป็นไฟล์
| Parameter | Type | Description |
|---|---|---|
tableId |
string | Table ID |
format |
string | รูปแบบไฟล์: 'csv', 'json', หรือ 'excel' |
options |
object | ตัวเลือกเพิ่มเติม |
| Options: | Option | Type | Description |
|---|---|---|---|
filename |
string | ชื่อไฟล์ที่จะบันทึก |
หมายเหตุ:
- Export ใช้ข้อมูลที่ผ่านการกรอง/เรียงลำดับแล้ว (ตรงกับที่ผู้ใช้เห็น)
- ค่าที่ export จะถูก format แล้ว (เช่น แสดง "Engineering" แทน "1")
- Header ใช้ชื่อคอลัมน์จาก
<th>แทน field name
// Export เป็น CSV
TableManager.exportData('users-table', 'csv', {
filename: 'employees.csv'
});
// Export เป็น JSON
TableManager.exportData('users-table', 'json', {
filename: 'employees.json'
});Bulk Actions (Checkbox Selection)
ระบบจัดการ bulk actions สำหรับแถวที่ถูกเลือกผ่าน checkbox พร้อม dropdown เลือก action และปุ่ม submit
การใช้งาน
<table data-table="deals"
data-source="api/crm/deals"
data-show-checkbox="true"
data-actions='{"delete":"Delete","activate":"Activate","stage|lead":"Lead","stage|won":"Won"}'
data-action-url="api/crm/deals/action"
data-action-button="Process|btn-success">
<!-- table content -->
</table>Attributes
| Attribute | Description |
|---|---|
data-show-checkbox |
แสดง checkbox สำหรับเลือกแถว |
data-actions |
JSON object ของ actions (key: action value, value: label) |
data-action-url |
URL สำหรับส่ง action (POST) |
data-action-button |
ข้อความปุ่ม หรือ "label\|className" |
รูปแบบ Action Key
Simple Action:
{"delete": "Delete", "activate": "Activate"}ส่ง: {action: "delete", ids: [...]}
Compound Action (ใช้ | separator):
{"stage|lead": "Lead", "stage|won": "Won", "stage|lost": "Lost"}ส่ง: {action: "stage", stage: "lead", ids: [...]}
รูปแบบ action|value จะแยก key เป็น:
action= ส่วนแรก (ก่อน|)- ส่วนแรกเป็นชื่อ parameter เพิ่มเติม โดยมี value = ส่วนหลัง (หลัง
|)
ตัวอย่างการใช้งานจริง
<table data-table="deals"
data-source="api/crm/deals"
data-show-checkbox="true"
data-actions='{
"stage|lead":"Lead",
"stage|qualified":"Qualified",
"stage|proposal":"Proposal",
"stage|negotiation":"Negotiation",
"stage|won":"Won",
"stage|lost":"Lost",
"delete":"Delete"
}'
data-action-url="api/crm/deals/action"
data-action-button="Process|btn-success">
<thead>
<tr>
<th data-field="title">Title</th>
<th data-field="stage">Stage</th>
</tr>
</thead>
<tbody></tbody>
</table>Backend Handler (PHP)
public function action()
{
$action = $this->request->post('action');
$ids = $this->request->post('ids');
switch ($action) {
case 'stage':
$stage = $this->request->post('stage'); // "lead", "won", etc.
$this->model->updateStage($ids, $stage);
break;
case 'delete':
$this->model->deleteByIds($ids);
break;
}
return ['success' => true];
}Event
EventManager.on('table:actionComplete', (e) => {
const {tableId, action, ids, response} = e.detail;
console.log(`Action "${action}" completed for ${ids.length} rows`);
});Filter Actions (Table-Level Actions)
ปุ่มและลิงก์ที่ส่ง filter parameters ไปยัง action URL - สำหรับ export, report generation, หรือ bulk operations
การใช้งาน
<table data-table="my-table"
data-source="api/users"
data-filter-actions='{
"export": {
"label": "Export Filtered",
"url": "api/export",
"type": "button",
"className": "btn-primary"
},
"report": {
"label": "View Report",
"url": "/reports",
"type": "link",
"className": "btn-info",
"target": "_blank"
}
}'>
<!-- table content -->
</table>ตัวเลือก Filter Actions
| Property | Type | Default | Description |
|---|---|---|---|
label |
string | key name | ข้อความแสดงบน button/link |
url |
string | required | URL สำหรับ action |
type |
string | "button" |
"button" (POST) หรือ "link" (GET redirect) |
className |
string | "" |
CSS classes เพิ่มเติม |
target |
string | "_self" |
สำหรับ link: "_blank" เปิด tab ใหม่ |
confirm |
string | - | ข้อความยืนยันก่อนทำ action |
Button Behavior
เมื่อกดปุ่ม จะส่ง POST request ไปยัง URL พร้อมข้อมูล:
{
"action": "export",
"tableId": "my-table",
"filters": {"status": "1", "department": "5"},
"sort": {"name": "asc"}
}Link Behavior
เมื่อกดลิงก์ จะ redirect ไปยัง URL พร้อม filter params ใน query string:
/reports?status=1&department=5Event
document.addEventListener('table:filterAction', (e) => {
const {tableId, action, type, params, response} = e.detail;
console.log(`Filter action: ${action} (${type})`);
console.log('Filters:', params);
});เหตุการณ์
| Event | เมื่อเกิด | Detail |
|---|---|---|
table:loaded |
Data loaded | {tableId, data} |
table:sorted |
Column sorted | {tableId, field, direction} |
table:filtered |
Filter applied | {tableId, filters} |
table:selected |
Row selection changed | {tableId, selected} |
table:row-sorted |
Row drag-drop | {tableId, item, oldIndex, newIndex} |
table:export |
หลัง export เสร็จ | {tableId, format, success, count} |
table:filterAction |
หลังกด filter action | {tableId, action, type, params, response} |
EventManager.on('table:loaded', (e) => {
console.log(`Loaded ${e.detail.data.length} rows`);
});Row Template
<table data-component="table" data-source="/api/users">
<thead>
<tr>
<th data-field="name">Name</th>
<th data-field="email">Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<template>
<tr data-id="{{id}}">
<td>{{name}}</td>
<td>{{email}}</td>
<td>
<button data-action="edit" data-id="{{id}}">Edit</button>
<button data-action="delete" data-id="{{id}}">Delete</button>
</td>
</tr>
</template>
</tbody>
</table>ตัวอย่างการใช้งานจริง
Users Table
<div class="table-container">
<div class="table-toolbar" data-table-toolbar="users">
<input type="search" data-filter="search" placeholder="Search...">
<select data-filter="role">
<option value="">All Roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div>
<table id="users"
data-component="table"
data-source="/api/users"
data-page-size="10"
data-show-checkbox="true">
<thead>
<tr>
<th data-checkbox></th>
<th data-field="name" data-sortable>Name</th>
<th data-field="email" data-sortable>Email</th>
<th data-field="role">Role</th>
<th data-field="created_at" data-sortable data-type="date">Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="table-footer" data-table-pagination="users"></div>
</div>With Actions
// Handle row actions
document.getElementById('users').addEventListener('click', (e) => {
const action = e.target.dataset.action;
const id = e.target.dataset.id;
if (action === 'edit') {
openEditModal(id);
} else if (action === 'delete') {
confirmDelete(id);
}
});
// Bulk delete
document.getElementById('bulk-delete').onclick = async () => {
const selected = TableManager.getSelectedRows('users');
if (confirm(`Delete ${selected.length} items?`)) {
for (const row of selected) {
await ApiService.delete(`/api/users/${row.id}`);
}
TableManager.loadTableData('users');
}
};Server-Side Pagination
<table data-component="table"
data-source="/api/users"
data-server-side="true"
data-page-size="25">
...
</table>API จะได้รับ parameters:
GET /api/users?page=1&pageSize=25&sort=name&order=asc&search=johnการจัดรูปแบบ CSS
/* Table container */
.table-container {
background: white;
border-radius: 8px;
overflow: hidden;
}
/* Table */
[data-component="table"] {
width: 100%;
border-collapse: collapse;
}
[data-component="table"] th,
[data-component="table"] td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
/* Sortable header */
th[data-sortable] {
cursor: pointer;
user-select: none;
}
th[data-sortable]:hover {
background: #f3f4f6;
}
th[data-sortable].asc::after { content: ' ↑'; }
th[data-sortable].desc::after { content: ' ↓'; }
/* Selected row */
tr.selected {
background: rgba(59, 130, 246, 0.1);
}ข้อควรระวัง
⚠️ 1. อย่าใช้ business id เป็น row identity ภายใน
สำหรับตารางที่มี data-editable-rows="true" ตอนนี้ TableManager จะสร้าง row key ภายในที่คงที่เอง เพื่อให้การเพิ่ม/ลบ/คัดลอกแถวไม่พึ่งค่าจากฟิลด์ id ที่ผู้ใช้กำลังแก้ไข
// ✅ ไม่มี business id ก็ยังเพิ่ม/ลบแถวได้
{
"userstatus": [
{"status": 0, "color": "#000", "topic": "Member"},
{"status": 1, "color": "#333", "topic": "Admin"}
]
}
// ✅ มี business id ก็ได้ และแก้ค่า id ในตารางได้โดยไม่ทำให้ row เพี้ยน
{
"userstatus": [
{"id": 0, "status": 0, "color": "#000", "topic": "Member"},
{"id": 1, "status": 1, "color": "#333", "topic": "Admin"}
]
}เหตุผล: ถ้าใช้ค่าจากคอลัมน์ id ที่ผู้ใช้แก้ได้โดยตรงเป็นตัวชี้แถวใน DOM พอกด +/- หลังแก้ id แล้วอาจหาแถวผิดตัวได้ ปัจจุบัน TableManager จึงใช้ row key ภายในที่คงที่แทน
ข้อแนะนำ:
- ถ้า backend ของคุณต้องมี field
idก็ส่งมาได้ตามปกติ แต่ให้มองว่าเป็น business data ไม่ใช่ DOM identity - ถ้า backend ไม่ต้องใช้
idสำหรับ schema ของข้อมูล ก็ไม่จำเป็นต้องเติมidแค่เพื่อให้ปุ่ม+/-ทำงาน
กรณี backend ต้องใช้ field id:
// ส่ง id ตาม business schema ได้ตามปกติ
foreach ($data as $key => $value) {
$result[] = [
'id' => $key,
'status' => $key,
'name' => $value
];
}⚠️ 2. รูปแบบ Element ID สำหรับ Error Highlighting
Element ภายในเซลล์ใช้รูปแบบ ID แบบ tableId_field_rowKey เพื่อให้ API สามารถ highlight ฟิลด์ที่มี error ได้อย่างคงที่ แม้ผู้ใช้จะแก้ค่าคอลัมน์ id ในแถวเดียวกัน:
// ✅ Element ID (predictable)
"userStatus_topic_row_2" // tableId_field_rowKey
// ✅ API Error Response
{
"errors": {
"userStatus_topic_2": "Name is required" // ตรงกับ element ID
}
}การทำงาน:
- FormManager จะอ่าน error keys และ highlight element ที่ id ตรงกัน
- รูปแบบ:
{tableId}_{fieldName}_{rowId} - ถ้า API ส่ง key ที่ตรงกับ element ID จะเห็น error highlight ที่ฟิลด์นั้น
⚠️ 3. Caption ปิดอัตโนมัติสำหรับ Editable Tables
ตารางที่มี data-editable-rows="true" จะปิด caption อัตโนมัติ:
<!-- ✅ Caption ปิดอัตโนมัติ ไม่ต้องระบุ data-show-caption -->
<table data-table="userStatus" data-editable-rows="true">
<!-- ✅ บังคับให้แสดง caption (ถ้าต้องการ) -->
<table data-table="userStatus" data-editable-rows="true" data-show-caption="true">เหตุผล: Editable tables แสดงข้อมูลทั้งหมดโดยไม่มี pagination ดังนั้น caption ที่บอก "page 1 of 1" จึงไม่จำเป็น
หมายเหตุ: คำเตือนนี้ปรับใหม่เนื่องจาก caption ถูกปิดอัตโนมัติแล้ว ไม่ต้องตั้งค่าเอง
⚠️ 2. Caption กับตารางแบบแก้ไขได้
ตารางที่มี data-editable-rows="true" และไม่มี pagination ควรปิด caption:
<!-- ✅ ปิด caption เพราะไม่มี pagination -->
<table data-table="userStatus"
data-editable-rows="true"
data-show-caption="false">เหตุผล: Caption แสดงข้อความแบบ "All 5 entries, displayed 1 to 5, page 1 of 1 pages" ซึ่งไม่เหมาะกับตารางแก้ไขได้ที่แสดงข้อมูลทั้งหมด
⚠️ 3. Field Names ต้องตรงกับ Data
<!-- ❌ Field ไม่ตรง -->
<th data-field="userName">Name</th>
<!-- data: { name: 'John' } -->
<!-- ✅ Field ตรง -->
<th data-field="name">Name</th>⚠️ 2. Template ใน tbody
- ⚠️ ช่องกรอกข้อความจะใช้
maxLengthเมื่อค่ามากกว่า 0 เท่านั้น; ให้ตั้งเลขบวกหรือไม่ระบุเพื่อไม่จำกัด
<!-- ❌ ไม่มี template -->
<tbody>
<tr><td>Static row</td></tr>
</tbody>
<!-- ✅ มี template -->
<tbody>
<template>
<tr><td>{{field}}</td></tr>
</template>
</tbody>เอกสารที่เกี่ยวข้อง
- ApiService - API calls
- Sortable - Drag-drop