Now.js Framework Documentation

Now.js Framework Documentation

TableManager

TH 29 Nov 2025 03:12

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/YYYY
  • datetime - แสดงวันที่และเวลา
  • number - แสดงตัวเลขพร้อม comma separator
  • currency - แสดงในรูปแบบเงิน (ต้องกำหนด currency)
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 element
  • options (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 ID
  • params (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 ID
  • data (Array) - Array of row data
  • meta (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 ID
  • filtered (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 object
  • tableId (string) - Table ID
  • th (HTMLElement) - Header cell element
  • event (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 ID
  • field (string) - Field name
  • value (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 object
  • tableId (string) - Table ID
  • options (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 ID
  • format (string) - Export format ('csv', 'json', 'excel')
    • csv - Client-side export
    • json - Client-side export
    • excel - Requires server-side support
  • options (Object, optional) - Export options
    • filename (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 object
  • tableId (string) - Table ID
  • field (string) - Field name
  • value (any) - New value
  • rowData (Object) - Row data
  • element (HTMLElement) - Input element
  • options (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 ID
  • stateKey (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 ข้อมูลทั้งหมดในครั้งเดียวถ้ามีจำนวนมาก

เคล็ดลับและข้อแนะนำ

  1. Performance Optimization

    • ใช้ server-side pagination สำหรับข้อมูลมาก
    • Enable virtual scrolling สำหรับ large datasets
    • Debounce search inputs (300-500ms)
    • Use appropriate page sizes
  2. UX Improvements

    • Show loading indicators
    • Display empty states
    • Provide clear error messages
    • Use responsive design
    • Add keyboard navigation
  3. 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

การเพิ่มประสิทธิภาพ

  1. Server-Side vs Client-Side

    // Server-side (แนะนำสำหรับ > 1000 records)
    data-source="api/users"
    
    // Client-side (เหมาะสำหรับ < 500 records)
    TableManager.setData('users', localData);
  2. Pagination Strategy

    // เลือก page size ที่เหมาะสม
    pageSizes: [25, 50, 100] // ไม่ใหญ่เกินไป
  3. Debounce Search

    let searchTimeout;
    searchInput.addEventListener('input', (e) => {
       clearTimeout(searchTimeout);
       searchTimeout = setTimeout(() => {
           TableManager.setFilter('users', 'search', e.target.value);
           TableManager.loadTableData('users');
       }, 300);
    });
  4. Lazy Loading

    // โหลดข้อมูลเมื่อต้องการเท่านั้น
    table.config.lazyLoad = true;
  5. 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

ข้อควรพิจารณาด้านความปลอดภัย

  1. 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 ต้องมีเสมอ
  2. XSS Protection

    // ✅ TableManager escape HTML โดยอัตโนมัติ
    // แต่ระวังเมื่อใช้ custom renderers
    
    // ✗ อันตราย
    cell.innerHTML = rowData.userInput;
    
    // ✓ ปลอดภัย
    cell.textContent = rowData.userInput;
  3. CSRF Protection

    // ใช้ CSRF tokens สำหรับ actions
    table.config.csrfToken = document.querySelector('meta[name="csrf-token"]').content;
  4. 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;
    }
}

StateManager

HTTPManager

EventManager

FormManager

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>
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)

  1. Click Action Button → TableManager ดึง data-row-actions config
  2. Extract Modal Config → อ่าน modal object จาก config
  3. Send Request → ส่ง action request พร้อม row data
  4. API Returns Data → Backend ส่ง {success: true, data: {...}, options: {...}}
  5. ResponseHandler Processing:
    • ตรวจสอบ priority: force > suggest > frontend default
    • Auto-extract data.data และ data.options
    • โหลด template จาก modalConfig.template
  6. TemplateManager → Bind data กับ template
  7. 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.html

3. 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

  1. Maximum recommended rows for client-side: 1,000
  2. Virtual scrolling requires fixed row heights
  3. Export ขนาดใหญ่ควรทำฝั่ง server
  4. 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