Now.js Framework Documentation
TableManager
TableManager
Now.js Table Manager - จัดการ data tables แบบ dynamic พร้อม sorting, filtering, pagination และ CRUD operations
ภาพรวม
TableManager เป็น powerful component ของ Now.js ที่จัดการ HTML tables ให้มี features ขั้นสูง เช่น การเรียงลำดับ (sorting), การกรอง (filtering), การแบ่งหน้า (pagination), การเลือกแถว (row selection), และการแก้ไขข้อมูลแบบ inline พร้อมด้วย server-side และ client-side data handling
Use Cases:
- Data tables แบบ interactive พร้อม sorting และ filtering
- Server-side pagination สำหรับข้อมูลจำนวนมาก
- Client-side data manipulation สำหรับข้อมูลปานกลาง
- Inline editing และ bulk actions
- Export data และ aggregate functions
- Responsive tables บนอุปกรณ์ mobile
Browser Compatibility:
- Chrome, Firefox, Safari, Edge (modern versions)
- Touch-enabled สำหรับ mobile devices
- Responsive design support
การติดตั้งและ Initialization
การโหลด Script
TableManager ถูกรวมอยู่ใน Now.js core:
<script src="/Now/Now.js"></script>หรือโหลดแยกต่างหาก:
<script src="/Now/js/TableManager.js"></script>การเริ่มต้นใช้งานพื้นฐาน
// เริ่มต้น TableManager
await TableManager.init({
pageSizes: [10, 25, 50, 100],
urlParams: true,
showCheckbox: false
});การสร้าง Table ใน HTML
<!-- Basic table -->
<table data-table="users"
data-source="api/users"
data-page-size="25">
<thead>
<tr>
<th data-field="id" data-sort="id">ID</th>
<th data-field="name" data-sort="name">Name</th>
<th data-field="email" data-sort="email">Email</th>
<th data-field="status">Status</th>
</tr>
</thead>
<tbody>
<!-- Data will be loaded here -->
</tbody>
</table>Configuration Options
ตัวเลือกหลัก
| Option | Type | Default | Description |
|---|---|---|---|
debug |
boolean | false | เปิด debug logging |
urlParams |
boolean | true | บันทึก state ลง URL parameters |
pageSizes |
Array | [10, 25, 50, 100] | ตัวเลือกจำนวนแถวต่อหน้า |
showCaption |
boolean | true | แสดง table caption |
showCheckbox |
boolean | false | แสดง checkboxes สำหรับเลือกแถว |
showFooter |
boolean | false | แสดง table footer |
searchColumns |
Array | [] | Columns ที่จะค้นหา |
persistColumnWidths |
boolean | true | บันทึกความกว้างของ column |
allowRowModification |
boolean | false | อนุญาตให้แก้ไขแถวแบบ inline |
confirmDelete |
boolean | true | ยืนยันก่อนลบ |
source |
string | '' | URL หรือ state key สำหรับโหลดข้อมูล |
actionUrl |
string | '' | URL สำหรับส่ง bulk actions |
actionButton |
string | 'Process' | Label ของปุ่ม action |
Data Attributes บน Table Element
<table data-table="users"
data-source="api/users"
data-page-size="25"
data-show-checkbox="true"
data-show-footer="true"
data-allow-row-modification="true"
data-action-url="api/users/bulk"
data-url-params="true">
</table>Column Configuration (data-* attributes)
<th data-field="name" <!-- Field name (required) -->
data-sort="name" <!-- Enable sorting -->
data-filter="true" <!-- Enable filtering -->
data-type="text" <!-- Input type for filter (text, select, email, etc.) -->
data-format="lookup" <!-- Format function (lookup, date, datetime, number) -->
data-options='{"1":"Active","0":"Inactive"}' <!-- Options for lookup/select -->
data-class="text-center" <!-- Cell CSS class -->
data-width="200px"> <!-- Column width -->
Name
</th>Available data-format values:
lookup- แปลงค่าตาม data-options (ต้องระบุ data-options)date- แสดงวันที่ในรูปแบบ DD/MM/YYYYdatetime- แสดงวันที่และเวลาnumber- แสดงตัวเลขพร้อม comma separatorcurrency- แสดงในรูปแบบเงิน (ต้องกำหนด currency)
Footer Aggregates
await TableManager.init({
footerAggregates: {
price: 'sum', // แสดงผลรวม
quantity: 'sum', // แสดงผลรวม
rating: 'avg', // แสดงค่าเฉลี่ย
sales: 'count' // นับจำนวน
}
});Actions Configuration
// Bulk actions
actions: {
delete: 'Delete',
activate: 'Activate',
deactivate: 'Deactivate',
export: 'Export'
}
// Row actions
rowActions: {
print: 'Print',
edit: {
label: 'Edit',
submenu: {
inline: 'Inline Edit',
page: 'Open Editor'
}
},
delete: 'Delete'
}Methods และ Properties
init(options)
เริ่มต้น TableManager พร้อม configuration
Parameters:
options(Object, optional) - Configuration options
Return: (Promise) - TableManager instance
ตัวอย่างการใช้งาน:
// Basic initialization
await TableManager.init({
pageSizes: [10, 25, 50, 100],
urlParams: true
});
// With custom configuration
await TableManager.init({
debug: true,
showCheckbox: true,
showFooter: true,
footerAggregates: {
price: 'sum',
quantity: 'count'
},
confirmDelete: true
});initTable(table, options)
เริ่มต้น table เดี่ยว
Parameters:
table(HTMLElement) - Table elementoptions(Object, optional) - Table-specific options
Return: (string) - Table ID
ตัวอย่างการใช้งาน:
// Initialize specific table
const tableElement = document.querySelector('#myTable');
const tableId = TableManager.initTable(tableElement, {
pageSize: 50,
showCheckbox: true,
source: 'api/products'
});
// With data source
TableManager.initTable(tableElement, {
source: 'api/users',
params: {
status: 'active',
role: 'admin'
}
});loadTableData(tableId, params)
โหลดข้อมูลลงใน table
Parameters:
tableId(string) - Table IDparams(Object, optional) - Additional parameters
Return: (Promise) - Resolves เมื่อโหลดเสร็จ
ตัวอย่างการใช้งาน:
// Load data
await TableManager.loadTableData('users');
// Load with custom params
await TableManager.loadTableData('users', {
status: 'active',
search: 'john',
page: 1,
pageSize: 25
});
// Reload current data
await TableManager.loadTableData('users');renderTable(tableId)
Render table content
Parameters:
tableId(string) - Table ID
Return: void
ตัวอย่างการใช้งาน:
// Re-render table
TableManager.renderTable('users');
// After data update
const table = TableManager.getTable('users');
table.data.push({ id: 100, name: 'New User' });
TableManager.renderTable('users');setData(tableId, data, meta)
ตั้งค่าข้อมูลให้ table
Parameters:
tableId(string) - Table IDdata(Array) - Array of row datameta(Object, optional) - Metadata (pagination, totals)
Return: void
ตัวอย่างการใช้งาน:
// Set data
TableManager.setData('users', [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
]);
// Set data with metadata
TableManager.setData('users', data, {
totalRecords: 1000,
totalPages: 40,
currentPage: 1
});
// Client-side data
const localData = [
{ id: 1, name: 'Product A', price: 100 },
{ id: 2, name: 'Product B', price: 200 }
];
TableManager.setData('products', localData);
TableManager.renderTable('products');getData(tableId, filtered)
รับข้อมูลจาก table
Parameters:
tableId(string) - Table IDfiltered(boolean, optional) - รับข้อมูลที่ผ่านการ filter แล้วหรือไม่
Return: (Array) - Array of data
ตัวอย่างการใช้งาน:
// Get all data
const allData = TableManager.getData('users');
console.log('Total records:', allData.length);
// Get filtered data only
const filteredData = TableManager.getData('users', true);
console.log('Filtered records:', filteredData.length);
// Export data
const data = TableManager.getData('users', true);
exportToCSV(data);getTable(tableId)
รับ table object
Parameters:
tableId(string) - Table ID
Return: (Object) - Table object
ตัวอย่างการใช้งาน:
// Get table object
const table = TableManager.getTable('users');
console.log('Config:', table.config);
console.log('Data:', table.data);
console.log('Sort state:', table.sortState);
// Modify table
const table = TableManager.getTable('users');
table.config.pageSize = 100;
TableManager.renderTable('users');handleSort(table, tableId, th, event)
จัดการการ sort
Parameters:
table(Object) - Table objecttableId(string) - Table IDth(HTMLElement) - Header cell elementevent(Event, optional) - Click event
ตัวอย่างการใช้งาน:
// Sort จะถูกเรียกอัตโนมัติเมื่อคลิก header
// แต่สามารถเรียกแบบ programmatic ได้
const table = TableManager.getTable('users');
const th = document.querySelector('[data-field="name"]');
TableManager.handleSort(table, 'users', th);
// Multi-sort โดยกด Ctrl/Cmd + Click
// หรือเรียก programmatic
const table = TableManager.getTable('users');
table.sortState = {
name: 'asc',
email: 'desc'
};
TableManager.loadTableData('users');setFilter(tableId, field, value)
ตั้งค่า filter
Parameters:
tableId(string) - Table IDfield(string) - Field namevalue(any) - Filter value
Return: void
ตัวอย่างการใช้งาน:
// Set single filter
TableManager.setFilter('users', 'status', 'active');
// Set multiple filters
TableManager.setFilter('users', 'status', 'active');
TableManager.setFilter('users', 'role', 'admin');
TableManager.loadTableData('users');
// Clear filter
TableManager.setFilter('users', 'status', '');clearFilters(tableId)
ล้าง filters ทั้งหมด
Parameters:
tableId(string) - Table ID
Return: void
ตัวอย่างการใช้งาน:
// Clear all filters
TableManager.clearFilters('users');
// Clear and reload
TableManager.clearFilters('users');
await TableManager.loadTableData('users');getSelectedRows(tableId)
รับแถวที่ถูกเลือก
Parameters:
tableId(string) - Table ID
Return: (Array) - Array of selected row data
ตัวอย่างการใช้งาน:
// Get selected rows
const selected = TableManager.getSelectedRows('users');
console.log('Selected count:', selected.length);
selected.forEach(row => {
console.log('User ID:', row.id);
});
// Process selected
const selected = TableManager.getSelectedRows('users');
if (selected.length > 0) {
const ids = selected.map(row => row.id);
await deleteUsers(ids);
}clearSelection(table, tableId, options)
ล้างการเลือกแถว
Parameters:
table(Object) - Table objecttableId(string) - Table IDoptions(Object, optional) - Options {emit: boolean}
Return: void
ตัวอย่างการใช้งาน:
// Clear selection
const table = TableManager.getTable('users');
TableManager.clearSelection(table, 'users');
// Clear without emitting event
TableManager.clearSelection(table, 'users', {emit: false});exportData(tableId, format, options)
Export table data
Parameters:
tableId(string) - Table IDformat(string) - Export format ('csv', 'json', 'excel')csv- Client-side exportjson- Client-side exportexcel- Requires server-side support
options(Object, optional) - Export optionsfilename(string) - ชื่อไฟล์ที่ต้องการfiltered(boolean) - Export เฉพาะข้อมูลที่ผ่าน filter แล้ว
Return: (Promise) - Resolves เมื่อ export สำเร็จ
ตัวอย่างการใช้งาน:
// Export to CSV (client-side)
await TableManager.exportData('users', 'csv', {
filename: 'users.csv'
});
// Export to JSON (client-side)
await TableManager.exportData('users', 'json', {
filename: 'users.json'
});
// Export to Excel (requires server-side)
await TableManager.exportData('users', 'excel', {
filename: 'users.xlsx'
});
// Export filtered data only
await TableManager.exportData('users', 'csv', {
filtered: true,
filename: 'active_users.csv'
});หมายเหตุ: Excel export ต้องการ server-side support เพราะ browser ไม่สามารถสร้างไฟล์ .xlsx ได้โดยตรง
handleFieldChange(table, tableId, field, value, rowData, element, options)
จัดการการแก้ไข field แบบ inline
Parameters:
table(Object) - Table objecttableId(string) - Table IDfield(string) - Field namevalue(any) - New valuerowData(Object) - Row dataelement(HTMLElement) - Input elementoptions(Object, optional) - Options {send: boolean}
ตัวอย่างการใช้งาน:
// ปกติจะถูกเรียกอัตโนมัติเมื่อแก้ไข inline
// แต่สามารถเรียก programmatic ได้
const table = TableManager.getTable('users');
const rowData = table.data[0];
await TableManager.handleFieldChange(
table,
'users',
'status',
'active',
rowData,
null,
{send: true}
);bindToState(tableId, stateKey)
Bind table กับ state management
Parameters:
tableId(string) - Table IDstateKey(string) - State key (e.g., 'state.users')
Return: void
ตัวอย่างการใช้งาน:
// Bind to state
TableManager.bindToState('users', 'state.users');
// Table จะ auto-update เมื่อ state เปลี่ยน
Now.setState('users', newData);
// Table จะ render ใหม่อัตโนมัติState Properties
TableManager มี state object:
TableManager.state = {
initialized: false, // Initialization status
tables: Map // Map of all tables
}Table Object Structure:
{
id: 'users',
element: HTMLElement,
config: {...},
data: [],
sortState: {},
filterWrapper: HTMLElement,
actionWrapper: HTMLElement,
paginationWrapper: HTMLElement,
filterElements: Map,
columns: Map,
meta: {
totalRecords: 1000,
totalPages: 40,
currentPage: 1
}
}ตัวอย่างการใช้งานจริง
ตัวอย่างที่ 1: Basic Data Table
<!-- HTML -->
<table data-table="users"
data-source="api/users"
data-page-size="25">
<thead>
<tr>
<th data-field="id" data-sortable="true">ID</th>
<th data-field="name" data-sortable="true" data-filterable="true">Name</th>
<th data-field="email" data-sortable="true" data-filterable="true">Email</th>
<th data-field="status" data-filterable="true">Status</th>
</tr>
</thead>
<tbody></tbody>
</table>// JavaScript
await TableManager.init({
pageSizes: [10, 25, 50, 100],
urlParams: true
});ตัวอย่างที่ 2: Table with Checkboxes และ Bulk Actions
<table data-table="products"
data-source="api/products"
data-show-checkbox="true"
data-action-url="api/products/bulk"
data-actions='{"delete":"Delete","activate":"Activate"}'>
<thead>
<tr>
<th data-field="id">ID</th>
<th data-field="name" data-sortable="true">Product Name</th>
<th data-field="price" data-sortable="true" data-type="number">Price</th>
<th data-field="stock" data-sortable="true" data-type="number">Stock</th>
</tr>
</thead>
</table>await TableManager.init({
showCheckbox: true,
confirmDelete: true
});
// Handle bulk action completion
document.addEventListener('table:action-complete', (e) => {
const {tableId, action, items, response} = e.detail;
console.log(`${action} completed for ${items.length} items`);
});ตัวอย่างที่ 3: Client-Side Data Table
// Client-side data
const products = [
{ id: 1, name: 'Laptop', price: 999, category: 'Electronics' },
{ id: 2, name: 'Mouse', price: 25, category: 'Accessories' },
{ id: 3, name: 'Keyboard', price: 75, category: 'Accessories' }
];
// Initialize table
await TableManager.init();
// Set data
TableManager.setData('products', products);
// Client-side sorting จะทำงานอัตโนมัติตัวอย่างที่ 4: Inline Editing
<table data-table="users"
data-source="api/users"
data-allow-row-modification="true"
data-action-url="api/users">
<thead>
<tr>
<th data-field="id">ID</th>
<th data-field="name" data-editable="true">Name</th>
<th data-field="email" data-editable="true" data-type="email">Email</th>
<th data-field="status"
data-editable="true"
data-type="select"
data-options='["active","inactive"]'>
Status
</th>
</tr>
</thead>
</table>await TableManager.init({
allowRowModification: true
});
// Listen to field changes
document.addEventListener('table:field-changed', (e) => {
const {tableId, field, value, rowData, success} = e.detail;
if (success) {
console.log(`${field} updated to ${value}`);
}
});ตัวอย่างที่ 5: Advanced Filtering
// Programmatic filtering
TableManager.setFilter('users', 'status', 'active');
TableManager.setFilter('users', 'role', 'admin');
TableManager.setFilter('users', 'created_at', {
from: '2024-01-01',
to: '2024-12-31'
});
await TableManager.loadTableData('users');
// Custom filter function
const table = TableManager.getTable('users');
table.customFilter = (row) => {
return row.age >= 18 && row.country === 'TH';
};
TableManager.renderTable('users');ตัวอย่างที่ 6: Export Data
// Export button
document.getElementById('exportBtn').addEventListener('click', async () => {
const format = document.getElementById('exportFormat').value;
await TableManager.exportData('users', format, {
filename: `users_${new Date().toISOString().split('T')[0]}.${format}`,
filtered: true // Export filtered data only
});
});
// Export selected rows
document.getElementById('exportSelectedBtn').addEventListener('click', () => {
const selected = TableManager.getSelectedRows('users');
const csv = convertToCSV(selected);
downloadFile(csv, 'selected_users.csv');
});Events และ Callbacks
TableManager emit events ผ่าน EventManager:
table:loaded
เกิดเมื่อโหลดข้อมูลเสร็จ
document.addEventListener('table:loaded', (e) => {
const {tableId, data, meta} = e.detail;
console.log(`Table ${tableId} loaded with ${data.length} records`);
});table:sorted
เกิดเมื่อมีการ sort
document.addEventListener('table:sorted', (e) => {
const {tableId, field, direction} = e.detail;
console.log(`Sorted by ${field} ${direction}`);
});table:filtered
เกิดเมื่อมีการ filter
document.addEventListener('table:filtered', (e) => {
const {tableId, filters} = e.detail;
console.log('Active filters:', filters);
});table:page-changed
เกิดเมื่อเปลี่ยนหน้า
document.addEventListener('table:page-changed', (e) => {
const {tableId, page, pageSize} = e.detail;
console.log(`Page changed to ${page}`);
});table:selection-changed
เกิดเมื่อเปลี่ยนการเลือกแถว
document.addEventListener('table:selection-changed', (e) => {
const {tableId, selected, count} = e.detail;
console.log(`${count} rows selected`);
// Enable/disable bulk action buttons
document.getElementById('deleteBtn').disabled = count === 0;
});table:field-changed
เกิดเมื่อแก้ไข field แบบ inline
document.addEventListener('table:field-changed', (e) => {
const {tableId, field, value, oldValue, rowData, success} = e.detail;
if (success) {
showNotification(`${field} updated successfully`);
} else {
showError(`Failed to update ${field}`);
}
});table:action-complete
เกิดเมื่อ bulk action เสร็จสิ้น
document.addEventListener('table:action-complete', (e) => {
const {tableId, action, items, response} = e.detail;
if (response.success) {
showNotification(`${action} completed for ${items.length} items`);
TableManager.clearSelection(TableManager.getTable(tableId), tableId);
TableManager.loadTableData(tableId);
}
});Best Practices
✓ ควรทำ (Do's)
- ✓ ใช้
data-*attributes สำหรับ configuration - ✓ กำหนด
data-fieldให้ตรงกับ field names ในข้อมูล - ✓ ต้องระบุ
data-optionsเมื่อใช้data-format="lookup"(มิฉะนั้นจะแสดง [object Object]) - ✓ ใช้
data-sort="fieldname"ไม่ใช่data-sortable="true" - ✓ ใช้
exportData()ไม่ใช่exportTable() - ✓ ใช้ server-side pagination สำหรับข้อมูลจำนวนมาก (> 1000 rows)
- ✓ ใช้ client-side สำหรับข้อมูลปานกลาง (< 500 rows)
- ✓ เปิด
urlParamsเพื่อบันทึก state - ✓ ใช้ appropriate data types (
data-type="number",date, etc.) - ✓ กำหนด
pageSizeที่เหมาะสม - ✓ ใช้ debounce สำหรับ search/filter inputs
- ✓ Handle errors อย่างเหมาะสม
- ✓ Show loading states
✗ ไม่ควรทำ (Don'ts)
- ✗ อย่าใช้
data-format="lookup"โดยไม่มีdata-options(จะแสดง [object Object]) - ✗ อย่าใช้
data-sortable="true"(ใช้data-sort="fieldname"แทน) - ✗ อย่าใช้
data-editable="true"กับdata-type="select"(มีปัญหา compatibility) - ✗ อย่าใช้ client-side กับข้อมูลจำนวนมาก
- ✗ อย่าลืม validate input ใน inline editing
- ✗ อย่าลืม handle API errors
- ✗ อย่าใช้ table IDs ที่ซ้ำกัน
- ✗ อย่าแก้ไข DOM โดยตรงหลังจาก render
- ✗ อย่าลืม cleanup event listeners
- ✗ อย่า load ข้อมูลทั้งหมดในครั้งเดียวถ้ามีจำนวนมาก
เคล็ดลับและข้อแนะนำ
-
Performance Optimization
- ใช้ server-side pagination สำหรับข้อมูลมาก
- Enable virtual scrolling สำหรับ large datasets
- Debounce search inputs (300-500ms)
- Use appropriate page sizes
-
UX Improvements
- Show loading indicators
- Display empty states
- Provide clear error messages
- Use responsive design
- Add keyboard navigation
-
Data Management
- Validate data on both client and server
- Handle edge cases (empty data, errors)
- Cache data when appropriate
- Use optimistic updates for better UX
Common Pitfalls (ข้อผิดพลาดที่พบบ่อย)
❌ ผิด: ใช้ lookup โดยไม่มี data-options
<!-- ❌ ผิด - จะแสดง [object Object] -->
<th data-field="status" data-format="lookup">Status</th>✅ ถูก: ระบุ data-options
<!-- ✅ ถูก - แสดงข้อความที่อ่านได้ -->
<th data-field="status"
data-format="lookup"
data-options='{"1":"Active","0":"Inactive"}'>
Status
</th>❌ ผิด: Client-side กับข้อมูลจำนวนมาก
// ❌ ผิด - จะช้ามากกับ 10,000+ records
const allUsers = await fetch('api/users/all').then(r => r.json());
TableManager.setData('users', allUsers);✅ ถูก: ใช้ server-side pagination
// ✅ ถูก - โหลดทีละหน้า
TableManager.initTable(table, {
source: 'api/users', // Server จัดการ pagination
pageSize: 25
});❌ ผิด: ใช้ data-sortable แทน data-sort
<!-- ❌ ผิด - ใช้ attribute เก่าที่ไม่ทำงาน -->
<th data-field="name" data-sortable="true">Name</th>✅ ถูก: ใช้ data-sort
<!-- ✅ ถูก - attribute ที่ถูกต้อง -->
<th data-field="name" data-sort="name">Name</th>❌ ผิด: ไม่ validate inline editing
// ❌ ผิด - ไม่มี validation
<th data-field="email" data-editable="true">Email</th>✅ ถูก: Validate input
// ✅ ถูก - มี validation
<th data-field="email"
data-editable="true"
data-type="email"
data-validate="email"
data-required="true">
Email
</th>
// หรือใช้ event listener
document.addEventListener('table:before-field-change', (e) => {
const {field, value} = e.detail;
if (field === 'email' && !isValidEmail(value)) {
e.preventDefault();
showError('Invalid email format');
}
});❌ ผิด: แก้ไข DOM หลัง render
// ❌ ผิด - การเปลี่ยนแปลงจะหายเมื่อ re-render
TableManager.renderTable('users');
document.querySelector('#users tbody tr:first-child')
.classList.add('highlight');✅ ถูก: ใช้ data attributes หรือ classes
// ✅ ถูก - กำหนดใน data
const data = TableManager.getData('users');
data[0]._class = 'highlight'; // Custom class
TableManager.setData('users', data);
TableManager.renderTable('users');
// หรือใช้ callback
table.config.rowClass = (row) => {
return row.status === 'vip' ? 'highlight' : '';
};❌ ผิด: ลืม handle loading state
// ❌ ผิด - ไม่มี loading indicator
await TableManager.loadTableData('users');✅ ถูก: Show loading state
// ✅ ถูก - แสดง loading
document.addEventListener('table:loading', (e) => {
const {tableId} = e.detail;
document.querySelector(`#${tableId}_loading`).style.display = 'block';
});
document.addEventListener('table:loaded', (e) => {
const {tableId} = e.detail;
document.querySelector(`#${tableId}_loading`).style.display = 'none';
});Performance Considerations
การเพิ่มประสิทธิภาพ
-
Server-Side vs Client-Side
// Server-side (แนะนำสำหรับ > 1000 records) data-source="api/users" // Client-side (เหมาะสำหรับ < 500 records) TableManager.setData('users', localData); -
Pagination Strategy
// เลือก page size ที่เหมาะสม pageSizes: [25, 50, 100] // ไม่ใหญ่เกินไป -
Debounce Search
let searchTimeout; searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { TableManager.setFilter('users', 'search', e.target.value); TableManager.loadTableData('users'); }, 300); }); -
Lazy Loading
// โหลดข้อมูลเมื่อต้องการเท่านั้น table.config.lazyLoad = true; -
Virtual Scrolling
// สำหรับข้อมูลจำนวนมาก table.config.virtualScroll = true; table.config.rowHeight = 40; // px
ข้อควรระวัง
- ข้อมูลมากกว่า 1000 rows ควรใช้ server-side
- Inline editing ควรมี debounce
- การ export ข้อมูลจำนวนมากควรทำฝั่ง server
- ระวัง memory leaks จาก event listeners
Security Considerations
ข้อควรพิจารณาด้านความปลอดภัย
-
Input Validation
// ✅ Validate ทั้ง client และ server document.addEventListener('table:before-field-change', (e) => { const {field, value} = e.detail; if (!validateInput(field, value)) { e.preventDefault(); return; } }); // Server-side validation ต้องมีเสมอ -
XSS Protection
// ✅ TableManager escape HTML โดยอัตโนมัติ // แต่ระวังเมื่อใช้ custom renderers // ✗ อันตราย cell.innerHTML = rowData.userInput; // ✓ ปลอดภัย cell.textContent = rowData.userInput; -
CSRF Protection
// ใช้ CSRF tokens สำหรับ actions table.config.csrfToken = document.querySelector('meta[name="csrf-token"]').content; -
Authorization
// ตรวจสอบ permissions ฝั่ง server // อย่าพึ่งพา client-side hiding เพียงอย่างเดียว // Client-side (UX only) if (!user.canEdit) { th.removeAttribute('data-editable'); } // Server-side (security) // ต้องตรวจสอบ permissions ทุกครั้ง
Browser Compatibility
รองรับ Browsers
| Browser | Version | Features |
|---|---|---|
| Chrome | Latest 2 versions | ✓ All features |
| Firefox | Latest 2 versions | ✓ All features |
| Safari | Latest 2 versions | ✓ All features |
| Edge | Latest 2 versions | ✓ All features |
| iOS Safari | iOS 12+ | ✓ Touch support |
| Chrome Android | Latest | ✓ Touch support |
Touch Support
// Enable touch gestures
TableManager.init({
touchEnabled: true
});Responsive Design
/* Responsive table */
@media (max-width: 768px) {
table[data-table] {
font-size: 14px;
}
table[data-table] th,
table[data-table] td {
padding: 8px 4px;
}
}Related Classes/Methods
StateManager
- ใช้ร่วมกับ
bindToState()สำหรับ reactive updates - StateManager Documentation
HTTPManager
- ใช้สำหรับ API calls ใน server-side mode
- HTTPManager Documentation
EventManager
- TableManager emit events ผ่าน EventManager
- EventManager Documentation
FormManager
- ใช้ร่วมกันสำหรับ CRUD operations
- FormManager Documentation
Row Actions with Modal Integration
TableManager รองรับการเปิด modal ผ่าน row actions โดยใช้ Hybrid Modal Approach ที่แยก UI config (frontend) จาก data (backend)
พื้นฐาน: data-row-actions
กำหนด actions บนแถวผ่าน data-row-actions attribute:
<table data-table="users" data-source="/api/users">
<thead>
<tr>
<th data-field="id">ID</th>
<th data-field="name">Name</th>
<th data-field="email">Email</th>
<th data-field="actions"
data-row-actions='{
"edit": {
"modal": {
"template": "editprofile",
"title": "Edit User",
"className": "large-modal"
}
},
"view": {
"modal": {
"template": "viewprofile",
"title": "View User",
"size": "medium"
}
},
"delete": {
"modal": {
"template": "confirm-delete",
"title": "Confirm Delete",
"className": "danger-modal"
}
}
}'>
Actions
</th>
</tr>
</thead>
<tbody></tbody>
</table>Modal Config Properties
| Property | Type | Description | Example |
|---|---|---|---|
template |
String | Template filename (from /templates/modals/) |
"editprofile" |
templateUrl |
String | Full URL to template | "/templates/modals/edit.html" |
title |
String | Modal title | "Edit User Profile" |
className |
String | CSS class for modal | "large-modal" |
size |
String | Modal size: small, medium, large |
"large" |
ตัวอย่างแบบ Complete
HTML Table:
<table data-table="products" data-source="/api/products">
<thead>
<tr>
<th data-field="id">ID</th>
<th data-field="name">Product</th>
<th data-field="price">Price</th>
<th data-field="actions"
data-row-actions='{
"edit": {
"modal": {
"template": "product-edit",
"title": "Edit Product",
"className": "product-modal large"
}
},
"viewHistory": {
"modal": {
"template": "product-history",
"title": "View History"
}
}
}'>
Actions
</th>
</tr>
</thead>
<tbody></tbody>
</table>Template File: /templates/modals/product-edit.html
<form id="product-form" class="modal-form">
<div class="form-group">
<label>Product Name</label>
<input type="text" data-model="name" required />
</div>
<div class="form-group">
<label>Price</label>
<input type="number" data-model="price" required />
</div>
<div class="form-group">
<label>Category</label>
<select data-model="categoryID">
<option value="">Select category</option>
<option
data-for="category in options.categories"
data-attr="value:category.id,selected:categoryID==category.id"
data-text="category.name">
</option>
</select>
</div>
<div class="modal-footer">
<button type="button" class="button" onclick="Modal.hide()">Cancel</button>
<button type="submit" class="button primary">Save</button>
</div>
</form>
<script>
document.getElementById('product-form').addEventListener('submit', async (e) => {
e.preventDefault();
const modal = StateManager.getModule('modal');
const formData = modal.getData();
const response = await fetch('/api/products/edit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(formData)
});
const result = await response.json();
await ResponseHandler.process(result);
});
</script>PHP API Endpoint:
public function editAction() {
$productId = $_POST['id'];
$product = $this->db->table('products')
->where(['id', $productId])
->first();
$categories = $this->db->table('categories')
->orderBy('name')
->execute()
->fetchAll();
// Simple response - no actions needed
echo json_encode([
'success' => true,
'data' => $product, // Auto-extracted
'options' => [ // Auto-extracted
'categories' => $categories
]
]);
}การทำงาน (Flow)
- Click Action Button → TableManager ดึง
data-row-actionsconfig - Extract Modal Config → อ่าน
modalobject จาก config - Send Request → ส่ง action request พร้อม row data
- API Returns Data → Backend ส่ง
{success: true, data: {...}, options: {...}} - ResponseHandler Processing:
- ตรวจสอบ priority:
force>suggest>frontend default - Auto-extract
data.dataและdata.options - โหลด template จาก
modalConfig.template
- ตรวจสอบ priority:
- TemplateManager → Bind data กับ template
- Modal Display → แสดง modal พร้อมข้อมูล
API Response Patterns
Pattern 1: Simple Data (แนะนำ 90%)
// API แค่ส่งข้อมูล - Frontend ควบคุม UI
echo json_encode([
'success' => true,
'data' => $userData,
'options' => $dropdowns
]);Pattern 2: API Suggest (Optional)
// API suggest template (ไม่ force)
echo json_encode([
'success' => true,
'actions' => [
[
'type' => 'modal',
'template' => 'editprofile', // Suggestion
'data' => $userData,
'options' => $dropdowns
]
]
]);Pattern 3: API Force Override (10% special cases)
// API force template (role-based, special conditions)
$template = $user->isAdmin ? 'admin-edit' : 'user-edit';
echo json_encode([
'success' => true,
'actions' => [
[
'type' => 'modal',
'template' => $template,
'force' => true, // Force override frontend config
'data' => $userData
]
]
]);Advanced: Dynamic Action URLs
<!-- ใช้ {field} placeholder ใน action URL -->
<th data-field="actions"
data-row-actions='{
"edit": {
"url": "/api/users/edit/{id}",
"modal": {
"template": "editprofile"
}
},
"activate": {
"url": "/api/users/activate/{id}"
}
}'>
Actions
</th>TableManager จะแทนที่ {id} ด้วยค่าจริงจาก row data โดยอัตโนมัติ
Best Practices
1. ใช้ Frontend-Driven เป็นหลัก
<!-- ✅ Good: กำหนด modal config ใน HTML -->
<th data-row-actions='{"edit": {"modal": {"template": "editprofile"}}}'>Actions</th>// ✅ Good: API แค่ส่งข้อมูล
return ['success' => true, 'data' => $user, 'options' => $opts];2. แยก Template ตาม Feature
templates/modals/
├── users/
│ ├── edit-profile.html
│ ├── view-profile.html
│ └── change-password.html
├── products/
│ ├── product-edit.html
│ └── product-history.html
└── common/
├── confirm-delete.html
└── alert.html3. Reuse Common Templates
<!-- Delete action ใช้ template เดียวกันได้ -->
<th data-row-actions='{
"deleteUser": {
"modal": {
"template": "confirm-delete",
"title": "Delete User"
}
},
"deleteProduct": {
"modal": {
"template": "confirm-delete",
"title": "Delete Product"
}
}
}'>Actions</th>4. Handle Errors Properly
try {
$result = $model->save($data);
return [
'success' => true,
'actions' => [
['type' => 'notification', 'level' => 'success', 'message' => 'Saved'],
['type' => 'closeModal'],
['type' => 'reload', 'target' => '#users-table']
]
];
} catch (Exception $e) {
return [
'success' => false,
'actions' => [
[
'type' => 'modal',
'html' => '<div class="error">' . $e->getMessage() . '</div>',
'title' => 'Error',
'className' => 'error-modal'
]
]
];
}Integration with ResponseHandler
TableManager ส่ง context.modalConfig ไปยัง ResponseHandler โดยอัตโนมัติ:
// Internal flow (auto-handled)
const modalConfig = cfg.modal; // จาก data-row-actions
const context = {
modalConfig: modalConfig,
tableId: tableId,
rowData: item
};
const response = await this.sendAction(actionUrl, item, tableId, submitEl, context);
await ResponseHandler.process(response, context);Additional Notes
เวอร์ชันที่รองรับ
- Now.js v1.0+
- ES6+ JavaScript
Breaking Changes
v1.0:
- เปลี่ยน URL parameter format เป็นแบบ compact
- Sort state เป็น object แทน array
- Event names เปลี่ยนจาก
tableLoadedเป็นtable:loaded
Limitations
- Maximum recommended rows for client-side: 1,000
- Virtual scrolling requires fixed row heights
- Export ขนาดใหญ่ควรทำฝั่ง server
- Browser memory limits สำหรับ large datasets
API Reference
รายการ methods ทั้งหมดแบบย่อ:
| Method | Parameters | Return | Description |
|---|---|---|---|
init(options) |
Object | Promise | เริ่มต้น TableManager |
initTable(table, options) |
Element, Object | String | เริ่มต้น table เดี่ยว |
loadTableData(tableId, params) |
String, Object | Promise | โหลดข้อมูล |
renderTable(tableId) |
String | void | Render table |
setData(tableId, data, meta) |
String, Array, Object | void | ตั้งค่าข้อมูล |
getData(tableId, filtered) |
String, Boolean | Array | รับข้อมูล |
getTable(tableId) |
String | Object | รับ table object |
setFilter(tableId, field, value) |
String, String, Any | void | ตั้งค่า filter |
clearFilters(tableId) |
String | void | ล้าง filters |
getSelectedRows(tableId) |
String | Array | รับแถวที่เลือก |
clearSelection(table, tableId) |
Object, String | void | ล้างการเลือก |
exportData(tableId, format, options) |
String, String, Object | Promise | Export data |
bindToState(tableId, stateKey) |
String, String | void | Bind to state |