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="create_date 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="create_date" data-sort="create_date" 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-page-size |
จำนวนรายการต่อหน้า |
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 |
เปิด/ปิดการลากแถว (ค่าเริ่มต้น: true เมื่อเปิดแก้ไขแถว) |
data-sortable-rows |
เปิดใช้ลากวางเรียงลำดับ |
data-url-params |
บันทึกสถานะใน URL |
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 | รูปแบบการแสดงผล |
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 |
Data Binding (data-attr)
ใช้ data-attr เพื่อ bind ข้อมูลจาก nested object ใน API response:
รูปแบบ: data-attr="data:path.to.data"
ตัวอย่าง:
<!-- Simple binding -->
<table data-attr="data:users"></table>
<!-- API: {"users": [...]} -->
<!-- Nested binding -->
<table data-attr="data:translate"></table>
<!-- API: {"translate": {"columns": [...], "data": [...]}} -->
<!-- Deep nested -->
<table data-attr="data:report.sales.monthly"></table>
<!-- API: {"report": {"sales": {"monthly": [...]}}} -->ข้อดี
✅ ยืดหยุ่น - 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
- สามารถใช้แค่ตัวใดตัวหนึ่ง หรือทั้งคู่ก็ได้
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 จะแสดงเมื่อ allowRowModification เป็น true ค่าเริ่มต้นเปิดอยู่ เว้นแต่กำหนด rowSortable เป็น false หรือใช้ data-row-sortable="false"
TableManager.disableRowSort(tableId)
ปิดการลากวางและซ่อน drag handle
การลากเรียงแถว (drag & drop)
- เปิดใช้: ใส่
data-editable-rows="true"(alias ของallowRowModification: true) เพื่อแสดงปุ่มเพิ่ม/ลบและ drag handle เมื่อrowSortableไม่ได้ตั้งเป็นfalse. - ปิดเป็นรายตาราง: ใช้
data-row-sortable="false"หรือเรียกTableManager.disableRowSort(tableId). - UX ล่าสุด: ghost ขนาดเล็กตามเคอร์เซอร์, placeholder เป็นแถวเส้นประ, การลากล็อกแนวตั้งเพื่อให้ reorder แม่นยำ
- อีเวนต์:
table:row-sorted→{tableId, item, oldIndex, newIndex}
<table data-table="rows"
data-source="/api/rows"
data-editable-rows="true"
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. Row Data ต้องมี id Field (สำคัญมาก!)
สำหรับตารางที่มี data-editable-rows="true" ข้อมูลแต่ละแถวต้องมี field id เสมอ มิฉะนั้นการเพิ่ม/ลบแถวจะไม่ทำงาน
// ❌ ไม่มี id - ปุ่ม +/- จะทำงานผิดพลาด
{
"userstatus": [
{"status": 0, "color": "#000", "topic": "Member"},
{"status": 1, "color": "#333", "topic": "Admin"}
]
}
// ✅ มี id - ทำงานถูกต้อง
{
"userstatus": [
{"id": 0, "status": 0, "color": "#000", "topic": "Member"},
{"id": 1, "status": 1, "color": "#333", "topic": "Admin"}
]
}เหตุผล: TableManager ใช้ id เพื่อ:
- ติดตาม row ใน DOM (
tr[data-id="${item.id}"]) - ค้นหา row ที่จะ copy/delete
- อัปเดตข้อมูลแถวที่ถูกต้อง
วิธีแก้ใน Backend:
// ใช้ field ที่มีอยู่แล้วเป็น id (ถ้ามันเป็น unique)
foreach ($data as $key => $value) {
$result[] = [
'id' => $key, // ← เพิ่มบรรทัดนี้
'status' => $key,
'name' => $value
];
}⚠️ 2. รูปแบบ Element ID สำหรับ Error Highlighting
Element ภายในเซลล์ใช้รูปแบบ ID แบบ tableId_field_rowId เพื่อให้ API สามารถ highlight ฟิลด์ที่มี error ได้:
// ✅ Element ID (predictable)
"userStatus_topic_2" // tableId_field_rowId
// ✅ 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