Now.js Framework Documentation

Now.js Framework Documentation

TableManager

EN 30 Nov 2025 13:56

TableManager

Now.js Table Manager - Manage dynamic data tables with sorting, filtering, pagination and CRUD operations

Overview

TableManager is a powerful component of Now.js that manages HTML tables with advanced features such as sorting, filtering, pagination, row selection, and inline editing, along with server-side and client-side data handling capabilities.

Use Cases:

  • Interactive data tables with sorting and filtering
  • Server-side pagination for large datasets
  • Client-side data manipulation for medium datasets
  • Inline editing and bulk actions
  • Data export and aggregate functions
  • Responsive tables on mobile devices

Browser Compatibility:

  • Chrome, Firefox, Safari, Edge (modern versions)
  • Touch-enabled for mobile devices
  • Responsive design support

Installation and Initialization

Loading the Script

TableManager is included in Now.js core:

<script src="/Now/Now.js"></script>

Or load separately:

<script src="/Now/js/TableManager.js"></script>

Basic Initialization

// Initialize TableManager
await TableManager.init({
    pageSizes: [10, 25, 50, 100],
    urlParams: true,
    showCheckbox: false
});

Creating Table in 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

Main Options

Option Type Default Description
debug boolean false Enable debug logging
urlParams boolean true Persist state in URL parameters
pageSizes Array [10, 25, 50, 100] Available page size options
showCaption boolean true Show table caption
showCheckbox boolean false Show checkboxes for row selection
showFooter boolean false Show table footer
searchColumns Array [] Columns to search
persistColumnWidths boolean true Persist column widths
allowRowModification boolean false Allow inline row editing
confirmDelete boolean true Confirm before delete
source string '' URL or state key for loading data
actionUrl string '' URL for bulk actions
actionButton string 'Process' Action button label
actionSelectLabel string 'Please select' Placeholder label for action dropdown (translatable)

Data Attributes on 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-action-select-label="Please select an action"
       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 - Convert values using options (see below for priority order)
  • date - Display date in DD/MM/YYYY format
  • datetime - Display date and time
  • number - Display numbers with comma separator
  • currency - Display as currency (requires currency configuration)

Options for data-format="lookup" (Priority Order):

TableManager will look for options in the following order:

  1. API Response options object (highest priority)

    {
     "data": [...],
     "options": {
       "status": {
         "active": "Active",
         "inactive": "Inactive"
       }
     }
    }
  2. API Response filters array

    {
     "data": [...],
     "filters": {
       "status": [
         {"value": "active", "label": "Active"},
         {"value": "inactive", "label": "Inactive"}
       ]
     }
    }
  3. HTML data-options attribute (fallback)

    <th data-field="status"
       data-format="lookup"
       data-options='{"active":"Active","inactive":"Inactive"}'>
     Status
    </th>

Best Practice: Use API-based options (methods 1-2) instead of hardcoding in HTML for better maintainability and internationalization support.

await TableManager.init({
    footerAggregates: {
        price: 'sum',      // Show sum
        quantity: 'sum',   // Show sum
        rating: 'avg',     // Show average
        sales: 'count'     // Show 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 and Properties

init(options)

Initialize TableManager with configuration

Parameters:

  • options (Object, optional) - Configuration options

Return: (Promise) - TableManager instance

Usage Examples:

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

Initialize single table

Parameters:

  • table (HTMLElement) - Table element
  • options (Object, optional) - Table-specific options

Return: (string) - Table ID

Usage Examples:

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

Load data into table

Parameters:

  • tableId (string) - Table ID
  • params (Object, optional) - Additional parameters

Return: (Promise) - Resolves when loading completes

Usage Examples:

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

Usage Examples:

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

Set table data with optional filters and options

Parameters:

  • tableId (string) - Table ID
  • data (Array|Object) - Array of row data OR object with data/filters/options
    • If Array: Simple data array
    • If Object: {data: [], meta: {}, filters: {}, options: {}}
  • meta (Object, optional) - Metadata (pagination, totals) - deprecated, use object format instead

Return: void

Canonical Data Format (Recommended):

{
  data: [],           // Array of row objects (required)
  meta: {             // Pagination metadata (optional)
    page: 1,
    pageSize: 25,
    total: 100,
    totalPages: 4
  },
  filters: {          // Filter options as arrays (optional)
    status: [
      {value: "active", label: "Active"},
      {value: "inactive", label: "Inactive"}
    ]
  },
  options: {          // Lookup options as objects (optional)
    role: {
      admin: "Administrator",
      user: "Regular User"
    }
  }
}

Usage Examples:

// Simple array format (legacy)
TableManager.setData('users', [
    { id: 1, name: 'John', email: 'john@example.com' },
    { id: 2, name: 'Jane', email: 'jane@example.com' }
]);

// Canonical format with options (recommended)
TableManager.setData('users', {
    data: [
        { id: 1, name: 'John', status: 'active', role: 'admin' },
        { id: 2, name: 'Jane', status: 'inactive', role: 'user' }
    ],
    meta: {
        page: 1,
        pageSize: 25,
        total: 100,
        totalPages: 4
    },
    filters: {
        status: [
            {value: 'active', label: 'Active'},
            {value: 'inactive', label: 'Inactive'}
        ]
    },
    options: {
        role: {
            admin: 'Administrator',
            user: 'Regular User'
        }
    }
});

// API response format (auto-handled)
const response = await fetch('/api/users').then(r => r.json());
// response.data contains: {data: [], meta: {}, filters: {}, options: {}}
TableManager.setData('users', response.data);

Notes:

  • filters are used for both dropdown options AND data-format="lookup"
  • options provide alternative format for lookup values (object notation)
  • Priority: options[field] > filters[field] > data-options attribute
  • Both filters and options are optional; you can use either or both

getData(tableId, filtered)

Get data from table

Parameters:

  • tableId (string) - Table ID
  • filtered (boolean, optional) - Get filtered data only

Return: (Array) - Array of data

Usage Examples:

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

Get table object

Parameters:

  • tableId (string) - Table ID

Return: (Object) - Table object

Usage Examples:

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

Handle sorting

Parameters:

  • table (Object) - Table object
  • tableId (string) - Table ID
  • th (HTMLElement) - Header cell element
  • event (Event, optional) - Click event

Usage Examples:

// Sort is triggered automatically on header click
// But can be called programmatically

const table = TableManager.getTable('users');
const th = document.querySelector('[data-field="name"]');
TableManager.handleSort(table, 'users', th);

// Multi-sort by Ctrl/Cmd + Click
// Or programmatically
const table = TableManager.getTable('users');
table.sortState = {
    name: 'asc',
    email: 'desc'
};
TableManager.loadTableData('users');

setFilter(tableId, field, value)

Set filter

Parameters:

  • tableId (string) - Table ID
  • field (string) - Field name
  • value (any) - Filter value

Return: void

Usage Examples:

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

Clear all filters

Parameters:

  • tableId (string) - Table ID

Return: void

Usage Examples:

// Clear all filters
TableManager.clearFilters('users');

// Clear and reload
TableManager.clearFilters('users');
await TableManager.loadTableData('users');

getSelectedRows(tableId)

Get selected rows

Parameters:

  • tableId (string) - Table ID

Return: (Array) - Array of selected row data

Usage Examples:

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

Clear row selection

Parameters:

  • table (Object) - Table object
  • tableId (string) - Table ID
  • options (Object, optional) - Options {emit: boolean}

Return: void

Usage Examples:

// 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) - Desired filename
    • filtered (boolean) - Export only filtered data

Return: (Promise) - Resolves when export is complete

Usage Examples:

// 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'
});

Note: Excel export requires server-side support as browsers cannot create .xlsx files directly.

handleFieldChange(table, tableId, field, value, rowData, element, options)

Handle inline field editing

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}

Usage Examples:

// Usually called automatically when inline editing
// But can be called programmatically

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 to state management

Parameters:

  • tableId (string) - Table ID
  • stateKey (string) - State key (e.g., 'state.users')

Return: void

Usage Examples:

// Bind to state
TableManager.bindToState('users', 'state.users');

// Table will auto-update when state changes
Now.setState('users', newData);
// Table renders automatically

State Properties

TableManager has a 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
    }
}

Practical Examples

Example 1: Basic Data Table

<!-- HTML -->
<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" data-filter="true">Name</th>
            <th data-field="email" data-sort="email" data-filter="true">Email</th>
            <th data-field="status" data-filter="true">Status</th>
        </tr>
    </thead>
    <tbody></tbody>
</table>
// JavaScript
await TableManager.init({
    pageSizes: [10, 25, 50, 100],
    urlParams: true
});

Example 2: Table with Checkboxes and Bulk Actions

<table data-table="products"
       data-source="api/products"
       data-show-checkbox="true"
       data-action-url="api/products/bulk"
       data-action-select-label="Select an action"
       data-actions='{"delete":"Delete","activate":"Activate"}'>
    <thead>
        <tr>
            <th data-field="id">ID</th>
            <th data-field="name" data-sort="name">Product Name</th>
            <th data-field="price" data-sort="price" data-type="number">Price</th>
            <th data-field="stock" data-sort="stock" data-type="number">Stock</th>
        </tr>
    </thead>
</table>
await TableManager.init({
    showCheckbox: true,
    confirmDelete: true,
    actionSelectLabel: 'Please select an action' // Default placeholder label (translatable)
});

// 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`);
});

Note: The action dropdown will always have an empty option as the first item with the label "Please select an action" (or custom label). The action button validates that:

  1. An action is selected (not the placeholder)
  2. At least one row is selected

If validation fails, a warning notification is shown.

Example 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 works automatically

Example 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}`);
    }
});

Example 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');

Example 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');
});

Example 7: Dynamic Lookup from API

HTML (Simple - no data-options needed):

<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>
            <!-- No data-options needed - API provides them -->
            <th data-field="status"
                data-sort="status"
                data-filter="true"
                data-format="lookup">
                Status
            </th>
            <th data-field="role"
                data-format="lookup">
                Role
            </th>
        </tr>
    </thead>
    <tbody></tbody>
</table>

API Response:

{
    "success": true,
    "message": "Users retrieved successfully",
    "code": 200,
    "data": {
        "data": [
            {
                "id": 1,
                "name": "John Doe",
                "email": "john@example.com",
                "status": "active",
                "role": "admin"
            },
            {
                "id": 2,
                "name": "Jane Smith",
                "email": "jane@example.com",
                "status": "inactive",
                "role": "user"
            }
        ],
        "filters": {
            "status": [
                {"value": "active", "label": "Active"},
                {"value": "inactive", "label": "Inactive"},
                {"value": "pending", "label": "Pending"}
            ]
        },
        "options": {
            "role": {
                "admin": "Administrator",
                "user": "Regular User",
                "guest": "Guest"
            }
        },
        "meta": {
            "page": 1,
            "pageSize": 25,
            "total": 100,
            "totalPages": 4
        }
    }
}

JavaScript:

// Just initialize - TableManager handles everything
await TableManager.init({
    pageSizes: [10, 25, 50, 100],
    urlParams: true
});

// Data will display:
// - Status column: "active" → "Active", "inactive" → "Inactive"
// - Role column: "admin" → "Administrator", "user" → "Regular User"
// - Status filter dropdown automatically populated with options

Benefits:

  • ✅ No hardcoded options in HTML
  • ✅ Easy internationalization (API returns localized labels)
  • ✅ Dynamic options based on user permissions or data state
  • ✅ Same options used for both display and filtering
  • ✅ Cleaner, more maintainable code

Events and Callbacks

TableManager emits events via EventManager:

table:loaded

Fired when data loading completes

document.addEventListener('table:loaded', (e) => {
    const {tableId, data, meta} = e.detail;
    console.log(`Table ${tableId} loaded with ${data.length} records`);
});

table:sorted

Fired when sorting occurs

document.addEventListener('table:sorted', (e) => {
    const {tableId, field, direction} = e.detail;
    console.log(`Sorted by ${field} ${direction}`);
});

table:filtered

Fired when filtering occurs

document.addEventListener('table:filtered', (e) => {
    const {tableId, filters} = e.detail;
    console.log('Active filters:', filters);
});

table:page-changed

Fired when page changes

document.addEventListener('table:page-changed', (e) => {
    const {tableId, page, pageSize} = e.detail;
    console.log(`Page changed to ${page}`);
});

table:selection-changed

Fired when row selection changes

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

Fired when inline field is edited

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

Fired when bulk action completes

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

  • ✓ Use data-* attributes for configuration
  • ✓ Set data-field to match field names in data
  • Use API response options or filters for dynamic lookup values
  • ✓ Use data-format="lookup" without data-options when API provides options
  • ✓ Use data-sort="fieldname" not data-sortable="true"
  • ✓ Use exportData() not exportTable()
  • ✓ Use server-side pagination for large datasets (> 1000 rows)
  • ✓ Use client-side for medium datasets (< 500 rows)
  • ✓ Enable urlParams to persist state
  • ✓ Use appropriate data types (data-type="number", date, etc.)
  • ✓ Set appropriate pageSize
  • ✓ Use debounce for search/filter inputs
  • ✓ Handle errors appropriately
  • ✓ Show loading states

✗ Don'ts

  • ✗ Don't hardcode data-options if API can provide them dynamically
  • ✗ Don't use data-sortable="true" (use data-sort="fieldname" instead)
  • ✗ Don't use data-editable="true" with data-type="select" (compatibility issues)
  • ✗ Don't use client-side with large datasets
  • ✗ Don't forget to validate input in inline editing
  • ✗ Don't forget to handle API errors
  • ✗ Don't use duplicate table IDs
  • ✗ Don't modify DOM directly after render
  • ✗ Don't forget to cleanup event listeners
  • ✗ Don't load all data at once if large amount

Tips and Recommendations

  1. Performance Optimization

    • Use server-side pagination for large data
    • Enable virtual scrolling for 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

API Response:

{
  "success": true,
  "data": {
    "data": [
      {"id": 1, "name": "John", "status": "active"},
      {"id": 2, "name": "Jane", "status": "inactive"}
    ],
    "filters": {
      "status": [
        {"value": "active", "label": "Active"},
        {"value": "inactive", "label": "Inactive"}
      ]
    }
  }
}

HTML (Simple - No data-options needed):

<!-- ✅ Right - API provides options automatically -->
<th data-field="status" data-format="lookup">Status</th>

Or with explicit options object:

{
  "data": [...],
  "options": {
    "status": {
      "active": "Active",
      "inactive": "Inactive"
    }
  }
}

❌ Wrong: Hardcoding options in HTML

<!-- ❌ Avoid - Hard to maintain, not i18n-friendly -->
<th data-field="status"
    data-format="lookup"
    data-options='{"active":"Active","inactive":"Inactive"}'>
  Status
</th>

✅ Right: Use data-options only as fallback

<!-- ✅ OK as fallback when API doesn't provide options -->
<th data-field="status"
    data-format="lookup"
    data-options='{"active":"Active","inactive":"Inactive"}'>
  Status
</th>

❌ Wrong: Using lookup without any options source

❌ Wrong: Using lookup without any options source

<!-- ❌ Wrong - No options from API or HTML, will display raw value -->
<th data-field="status" data-format="lookup">Status</th>

API Response (No options):

{
  "data": [
    {"id": 1, "status": "active"}
  ]
}

Result: Displays "active" (raw value)

✅ Right: Provide options from API or HTML

See examples above for correct implementation.

❌ Wrong: Client-side with large data

// ❌ Wrong - will be very slow with 10,000+ records
const allUsers = await fetch('api/users/all').then(r => r.json());
TableManager.setData('users', allUsers);

✅ Right: Use server-side pagination

// ✅ Right - load page by page
TableManager.initTable(table, {
    source: 'api/users', // Server handles pagination
    pageSize: 25
});

❌ Wrong: Using data-sortable instead of data-sort

<!-- ❌ Wrong - old attribute that doesn't work -->
<th data-field="name" data-sortable="true">Name</th>

✅ Right: Use data-sort

<!-- ✅ Right - correct attribute -->
<th data-field="name" data-sort="name">Name</th>

❌ Wrong: No validation for inline editing

// ❌ Wrong - no validation
<th data-field="email" data-editable="true">Email</th>

✅ Right: Validate input

// ✅ Right - with validation
<th data-field="email"
    data-editable="true"
    data-type="email"
    data-validate="email"
    data-required="true">
    Email
</th>

// Or use 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');
    }
});

❌ Wrong: Modify DOM after render

// ❌ Wrong - changes will be lost on re-render
TableManager.renderTable('users');
document.querySelector('#users tbody tr:first-child')
    .classList.add('highlight');

✅ Right: Use data attributes or classes

// ✅ Right - set in data
const data = TableManager.getData('users');
data[0]._class = 'highlight'; // Custom class
TableManager.setData('users', data);
TableManager.renderTable('users');

// Or use callback
table.config.rowClass = (row) => {
    return row.status === 'vip' ? 'highlight' : '';
};

❌ Wrong: Forget loading state

// ❌ Wrong - no loading indicator
await TableManager.loadTableData('users');

✅ Right: Show loading state

// ✅ Right - show 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

Performance Optimization

  1. Server-Side vs Client-Side

    // Server-side (recommended for > 1000 records)
    data-source="api/users"
    
    // Client-side (suitable for < 500 records)
    TableManager.setData('users', localData);
  2. Pagination Strategy

    // Choose appropriate page size
    pageSizes: [25, 50, 100] // Not too large
  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

    // Load data only when needed
    table.config.lazyLoad = true;
  5. Virtual Scrolling

    // For large datasets
    table.config.virtualScroll = true;
    table.config.rowHeight = 40; // px

Considerations

  • More than 1000 rows should use server-side
  • Inline editing should have debounce
  • Large data export should be done server-side
  • Watch for memory leaks from event listeners

Security Considerations

Security Considerations

  1. Input Validation

    // ✅ Validate both client and server
    document.addEventListener('table:before-field-change', (e) => {
       const {field, value} = e.detail;
    
       if (!validateInput(field, value)) {
           e.preventDefault();
           return;
       }
    });
    
    // Server-side validation is always required
  2. XSS Protection

    // ✅ TableManager escapes HTML automatically
    // But be careful with custom renderers
    
    // ✗ Dangerous
    cell.innerHTML = rowData.userInput;
    
    // ✓ Safe
    cell.textContent = rowData.userInput;
  3. CSRF Protection

    // Use CSRF tokens for actions
    table.config.csrfToken = document.querySelector('meta[name="csrf-token"]').content;
  4. Authorization

    // Check permissions server-side
    // Don't rely on client-side hiding alone
    
    // Client-side (UX only)
    if (!user.canEdit) {
       th.removeAttribute('data-editable');
    }
    
    // Server-side (security)
    // Must check permissions every time

Browser Compatibility

Supported 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 supports opening modals through row actions using the Hybrid Modal Approach that separates UI config (frontend) from data (backend).

Basics: data-row-actions

Define actions on rows via the 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 Example

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 reads data-row-actions config
  2. Extract Modal Config → Reads modal object from config
  3. Send Request → Sends action request with row data
  4. API Returns Data → Backend sends {success: true, data: {...}, options: {...}}
  5. ResponseHandler Processing:
    • Checks priority: force > suggest > frontend default
    • Auto-extracts data.data and data.options
    • Loads template from modalConfig.template
  6. TemplateManager → Binds data with template
  7. Modal Display → Shows modal with data

API Response Patterns

// API only sends data - Frontend controls UI
echo json_encode([
    'success' => true,
    'data' => $userData,
    'options' => $dropdowns
]);

Pattern 2: API Suggest (Optional)

// API suggests template (no 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 forces 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

<!-- Use {field} placeholder in 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 will automatically replace {id} with the actual value from row data.

Best Practices

1. Use Frontend-Driven as Default

<!-- ✅ Good: Define modal config in HTML -->
<th data-row-actions='{"edit": {"modal": {"template": "editprofile"}}}'>Actions</th>
// ✅ Good: API only sends data
return ['success' => true, 'data' => $user, 'options' => $opts];

2. Organize Templates by 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 actions use same 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 automatically sends context.modalConfig to ResponseHandler:

// Internal flow (auto-handled)
const modalConfig = cfg.modal;  // from 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

Supported Versions

  • Now.js v1.0+
  • ES6+ JavaScript

Breaking Changes

v1.0:

  • Changed URL parameter format to compact
  • Sort state is object instead of array
  • Event names changed from tableLoaded to table:loaded

Limitations

  1. Maximum recommended rows for client-side: 1,000
  2. Virtual scrolling requires fixed row heights
  3. Large exports should be done server-side
  4. Browser memory limits for large datasets

API Reference

Complete methods list:

Method Parameters Return Description
init(options) Object Promise Initialize TableManager
initTable(table, options) Element, Object String Initialize single table
loadTableData(tableId, params) String, Object Promise Load data
renderTable(tableId) String void Render table
setData(tableId, data, meta) String, Array, Object void Set data
getData(tableId, filtered) String, Boolean Array Get data
getTable(tableId) String Object Get table object
setFilter(tableId, field, value) String, String, Any void Set filter
clearFilters(tableId) String void Clear filters
getSelectedRows(tableId) String Array Get selected rows
clearSelection(table, tableId) Object, String void Clear selection
exportData(tableId, format, options) String, String, Object Promise Export data
bindToState(tableId, stateKey) String, String void Bind to state