Now.js Framework Documentation
TableManager
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 formatdatetime- Display date and timenumber- Display numbers with comma separatorcurrency- Display as currency (requires currency configuration)
Options for data-format="lookup" (Priority Order):
TableManager will look for options in the following order:
-
API Response
optionsobject (highest priority){ "data": [...], "options": { "status": { "active": "Active", "inactive": "Inactive" } } } -
API Response
filtersarray{ "data": [...], "filters": { "status": [ {"value": "active", "label": "Active"}, {"value": "inactive", "label": "Inactive"} ] } } -
HTML
data-optionsattribute (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.
Footer Aggregates
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 elementoptions(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 IDparams(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 IDdata(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:
filtersare used for both dropdown options AND data-format="lookup"optionsprovide alternative format for lookup values (object notation)- Priority:
options[field]>filters[field]>data-optionsattribute - Both filters and options are optional; you can use either or both
getData(tableId, filtered)
Get data from table
Parameters:
tableId(string) - Table IDfiltered(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 objecttableId(string) - Table IDth(HTMLElement) - Header cell elementevent(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 IDfield(string) - Field namevalue(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 objecttableId(string) - Table IDoptions(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 IDformat(string) - Export format ('csv', 'json', 'excel')csv- Client-side exportjson- Client-side exportexcel- Requires server-side support
options(Object, optional) - Export optionsfilename(string) - Desired filenamefiltered(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 objecttableId(string) - Table IDfield(string) - Field namevalue(any) - New valuerowData(Object) - Row dataelement(HTMLElement) - Input elementoptions(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 IDstateKey(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 automaticallyState 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:
- An action is selected (not the placeholder)
- 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 automaticallyExample 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 optionsBenefits:
- ✅ 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-fieldto match field names in data - ✓ Use API response
optionsorfiltersfor dynamic lookup values - ✓ Use
data-format="lookup"withoutdata-optionswhen API provides options - ✓ Use
data-sort="fieldname"notdata-sortable="true" - ✓ Use
exportData()notexportTable() - ✓ Use server-side pagination for large datasets (> 1000 rows)
- ✓ Use client-side for medium datasets (< 500 rows)
- ✓ Enable
urlParamsto 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-optionsif API can provide them dynamically - ✗ Don't use
data-sortable="true"(usedata-sort="fieldname"instead) - ✗ Don't use
data-editable="true"withdata-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
-
Performance Optimization
- Use server-side pagination for large data
- Enable virtual scrolling for 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
✅ Right: Use API-provided options (Recommended)
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
-
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); -
Pagination Strategy
// Choose appropriate page size pageSizes: [25, 50, 100] // Not too large -
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
// Load data only when needed table.config.lazyLoad = true; -
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
-
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 -
XSS Protection
// ✅ TableManager escapes HTML automatically // But be careful with custom renderers // ✗ Dangerous cell.innerHTML = rowData.userInput; // ✓ Safe cell.textContent = rowData.userInput; -
CSRF Protection
// Use CSRF tokens for actions table.config.csrfToken = document.querySelector('meta[name="csrf-token"]').content; -
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;
}
}Related Classes/Methods
StateManager
- Used with
bindToState()for reactive updates - StateManager Documentation
HTTPManager
- Used for API calls in server-side mode
- HTTPManager Documentation
EventManager
- TableManager emits events via EventManager
- EventManager Documentation
FormManager
- Used together for CRUD operations
- FormManager Documentation
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>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 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
- Click Action Button → TableManager reads
data-row-actionsconfig - Extract Modal Config → Reads
modalobject from config - Send Request → Sends action request with row data
- API Returns Data → Backend sends
{success: true, data: {...}, options: {...}} - ResponseHandler Processing:
- Checks priority:
force>suggest>frontend default - Auto-extracts
data.dataanddata.options - Loads template from
modalConfig.template
- Checks priority:
- TemplateManager → Binds data with template
- Modal Display → Shows modal with data
API Response Patterns
Pattern 1: Simple Data (Recommended 90%)
// 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.html3. 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
tableLoadedtotable:loaded
Limitations
- Maximum recommended rows for client-side: 1,000
- Virtual scrolling requires fixed row heights
- Large exports should be done server-side
- 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 |