SmartTables.js

Smarter Tables, Less Hassle!

SmartTables.js is a powerful, feature-rich JavaScript plugin designed to enhance HTML tables with advanced functionality such as sorting, searching, pagination, responsive behavior, and data export capabilities. It is built with modern web development practices in mind, offering a flexible and extensible API for developers to customize and extend its functionality.

This plugin is ideal for developers who need to manage large datasets in a user-friendly way, providing a seamless experience for end-users while maintaining a high level of customization and control.

Pro Tip: SmartTables works with your existing HTML tables and automatically adapts to different screen sizes by hiding less important columns on smaller screens.

Installation

SmartTables.js is now available as an ES6 module. You can include it in your project in several ways:

1. Using ES6 Modules (Recommended)

// Import the SmartTables class
import { SmartTables } from './scripts/smartTables.bundle.js';

// Initialize with empty options object (required)
const myTable = new SmartTables('tableId', {});

// Or with custom options
const myTable = new SmartTables('tableId', {
    perPage: 15,
    search: true,
    sort: true,
    pagination: true,
    export: true
});
Important:
  • You must pass an empty options object ({}) as the second parameter when initializing SmartTables, even if you don't need any custom options.
  • This is required because the constructor expects an options object to properly initialize default values.
  • If you don't pass the options object, you'll get an error: Cannot read properties of undefined (reading 'responsive').

2. Using NPM (for modern build systems)

// Install via npm
npm install smarttables

// Import in your JavaScript/TypeScript file
import { SmartTables } from 'smarttables';

// Use in your component
const myTable = new SmartTables('tableId', {
    data: {
        type: 'json',
        source: myData,
        columns: [
            { data: 'id', title: 'ID' },
            { data: 'name', title: 'Name' },
            { data: 'position', title: 'Position' }
        ]
    }
});

3. Using with TypeScript

import { SmartTables, SmartTablesOptions } from 'smarttables';

interface CustomOptions extends SmartTablesOptions {
    customFeature?: boolean;
}

const options: CustomOptions = {
    perPage: 10,
    search: true,
    customFeature: true
};

const myTable = new SmartTables('tableId', options);

HTML Structure

<table id="myTable" class="table">
    <thead>
        <tr>
            <th data-priority="1">ID</th>
            <th data-priority="2">Name</th>
            <th data-priority="3">Position</th>
            <th data-priority="4">Office</th>
            <th data-priority="5">Age</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>1</td>
            <td>John Doe</td>
            <td>Developer</td>
            <td>New York</td>
            <td>32</td>
        </tr>
        <!-- More rows... -->
    </tbody>
</table>
Important:
  • Use the data-priority attribute on table headers to control which columns are hidden first when the table becomes too narrow for the viewport.
  • When using ES6 modules, make sure your server is configured to serve JavaScript files with the correct MIME type (application/javascript).
  • For older browsers, you'll need to use a bundler like Webpack, Rollup, or Parcel to transpile the ES6 code.

Configuration Options

SmartTables.js provides extensive configuration options to customize its behavior. All options are optional and will fall back to sensible defaults.

// Basic configuration
const table = new SmartTables('myTable', {
    perPage: 10,
    search: true,
    sort: true
});

// Advanced configuration with TypeScript
interface CustomOptions extends SmartTablesOptions {
    customFeature?: boolean;
}

const options: CustomOptions = {
    perPage: 25,
    search: true,
    sort: true,
    pagination: true,
    export: true,
    loading: {
        enabled: true,
        duration: 500,
        minDuration: 300
    },
    responsive: {
        enabled: true,
        breakpoint: 992,
        columnPriorities: {
            0: 1,  // ID column - highest priority
            1: 2,  // Name column - second highest priority
            2: 3,  // Position
            3: 4,  // Office
            4: 5   // Age - lowest priority
        }
    },
    debug: false,
    fuzzyMatch: {
        threshold: 0.6,
        minMatchLength: 2,
        multiWordThreshold: 0.5,
        maxDistance: 3
    },
    customFeature: true
};

const table = new SmartTables('myTable', options);
Option Default Value Description Type
perPage 10 Number of rows to display per page in pagination. number
search true Enables or disables the search functionality for the table. boolean
sort true Enables or disables sorting functionality for table columns. boolean
pagination true Enables or disables pagination controls for the table. boolean
export true Enables or disables export functionality (e.g., Excel, CSV, Copy). boolean
loading.enabled true Enables or disables the loading spinner/indicator during data loading or processing. boolean
loading.duration 0 Duration (in milliseconds) of an artificial delay for the loading indicator. number
loading.minDuration 300 Minimum duration (in milliseconds) to display the loading indicator. number
responsive.enabled true Enables or disables responsive behavior (e.g., hiding columns on small screens). boolean
responsive.breakpoint 768 Breakpoint (in pixels) at which responsive behavior starts. number
responsive.columnPriorities {} Object mapping column indices to their priority (1 being highest). Object<number, number>
responsive.details.type 'column' Type of detail display for hidden columns. string
responsive.details.target 0 Target column index for the detail control. number
debug false Enables or disables debug logging for development and troubleshooting. boolean
fuzzyMatch.threshold 0.7 Threshold for fuzzy matching (0.0–1.0). number
fuzzyMatch.minMatchLength 2 Minimum number of characters required for fuzzy matching. number
fuzzyMatch.multiWordThreshold 0.5 Threshold for multi-word search matches. number
fuzzyMatch.maxDistance 2 Maximum Levenshtein distance allowed for typo tolerance. number
classes.wrapper 'st-wrapper' Class for the wrapper div containing the table. string
classes.table 'st-table table table-striped table-hover' Class for the table element. string
classes.toolbar 'st-toolbar d-flex justify-content-between mb-3' Class for the toolbar containing controls. string
classes.search 'st-search form-control' Class for the search input field. string
classes.pagination 'st-pagination pagination justify-content-center' Class for the pagination controls. string
classes.export 'st-export btn-group' Class for the export button group. string
data.type null Type of data source ('json', 'csv', 'ajax', or null for DOM-based tables). string|null
data.source null Source of the data (URL, string, or object depending on type). string|null|Object
data.columns [] Array of column definitions. Array
data.processing false Enables or disables a processing indicator during data operations. boolean
data.serverSide false Enables or disables server-side processing for large datasets. boolean
data.method 'GET' HTTP method for AJAX requests. string
data.headers {} Custom headers for AJAX requests. Object
data.params {} Additional parameters for AJAX requests. Object
data.parser null Custom parser function for transforming raw data. Function|null
data.prefetch true Enables prefetching of the next page data for smoother pagination. boolean
data.cacheDuration 300000 Duration in milliseconds to keep cached data (default: 5 minutes). number
data.withCredentials false Whether to include credentials in cross-domain requests. boolean
data.url null The URL to fetch data from (aliased with source for compatibility). string|null
hooks {} Object containing various lifecycle and event hooks. Object
plugins [] Array of plugin objects to extend functionality. Array
Pro Tip: You can extend the default options by creating a custom interface that extends SmartTablesOptions when using TypeScript. This provides better type safety and autocompletion.

Advanced Configuration Example

Here's an example of how to configure SmartTables with advanced options:

var myTable = new SmartTables('tableId', {
    perPage: 25,
    search: true,
    sort: true,
    pagination: true,
    export: true,
    loading: {
        enabled: true,
        duration: 500, // Show loading for at least 500ms
        minDuration: 300
    },
    responsive: {
        enabled: true,
        breakpoint: 992,
        columnPriorities: {
            0: 1,  // ID column - highest priority
            1: 2,  // Name column - second highest priority
            2: 3,  // Position
            3: 4,  // Office
            4: 5   // Age - lowest priority
        }
    },
    debug: false,
    fuzzyMatch: {
        threshold: 0.6,      // Lower threshold = more matches
        minMatchLength: 2,   // Minimum characters to match
        multiWordThreshold: 0.5,
        maxDistance: 3       // Higher distance = more tolerance for typos
    }
});

Data Loading Methods

SmartTables supports various data loading methods:

// Load from JSON data
var myTable = new SmartTables('tableId', {
    data: {
        type: 'json',
        source: [
            { id: 1, name: 'John Doe', position: 'Developer', office: 'New York', age: 32 },
            { id: 2, name: 'Jane Smith', position: 'Designer', office: 'London', age: 28 }
        ],
        columns: [
            { data: 'id', title: 'ID' },
            { data: 'name', title: 'Name' },
            { data: 'position', title: 'Position' },
            { data: 'office', title: 'Office' },
            { data: 'age', title: 'Age' }
        ]
    }
});

// Load from AJAX
var myTable = new SmartTables('tableId', {
    data: {
        type: 'ajax',
        source: 'api/users',
        method: 'GET',
        headers: { 'Authorization': 'Bearer token123' },
        params: { limit: 100 }
    }
});

// Load from CSV
var myTable = new SmartTables('tableId', {
    data: {
        type: 'csv',
        source: 'data/employees.csv'
    }
});

Data Import Functionality

SmartTables provides built-in data import capabilities that allow users to import data from various file formats directly into the table. This feature can be enabled by setting the import: true option.

// Initialize table with import enabled
var myTable = new SmartTables('tableId', {
    import: true,  // Enable import functionality
    data: {
        type: 'json',
        source: initialData,
        columns: [
            { data: 'id', title: 'ID' },
            { data: 'name', title: 'Name' },
            { data: 'position', title: 'Position' }
        ]
    }
});

Supported Import Formats

  • CSV Files: Import data from comma-separated value files
  • JSON Files: Import data from JSON format files
  • Excel Files: Import data from Excel spreadsheets (.xlsx)
  • Text Files: Import data from plain text files with custom delimiters

Import Options

var myTable = new SmartTables('tableId', {
    import: {
        enabled: true,
        formats: ['csv', 'json', 'xlsx'],  // Specify allowed formats
        maxFileSize: 5 * 1024 * 1024,      // 5MB max file size
        onImportStart: function() {
            console.log('Import started');
        },
        onImportComplete: function(data) {
            console.log('Import completed', data);
        },
        onImportError: function(error) {
            console.error('Import failed', error);
        }
    }
});

Example Usage

<!-- HTML Structure -->
<table id="myTable" class="table">
    <thead>
        <tr>
            <th data-priority="1">ID</th>
            <th data-priority="2">Name</th>
            <th data-priority="3">Position</th>
        </tr>
    </thead>
    <tbody>
        <!-- Data will be populated by SmartTables -->
    </tbody>
</table>

<!-- JavaScript -->
import { SmartTables } from 'smarttables';

const table = new SmartTables('myTable', {
    import: true,
    data: {
        type: 'json',
        source: [],  // Start with empty data
        columns: [
            { data: 'id', title: 'ID' },
            { data: 'name', title: 'Name' },
            { data: 'position', title: 'Position' }
        ]
    }
});
Pro Tip: When importing data, SmartTables automatically detects the column structure and maps the imported data to the appropriate columns. You can also provide custom column mappings for more control over the import process.
Important:
  • Make sure your server is configured to handle file uploads if you're using the file import feature
  • Consider implementing file size limits and format validation on both client and server side
  • For large files, consider implementing chunked uploads or progress indicators
  • Always validate imported data before processing to prevent security issues

Event Hooks

SmartTables provides hooks for various events in the table lifecycle:

var myTable = new SmartTables('tableId', {
    hooks: {
        // Table lifecycle hooks
        beforeInit: function(instance) {
            console.log('Before table initialization');
        },
        afterInit: function(instance) {
            console.log('Table initialized!');
        },
        
        // Data hooks
        beforeDataLoad: function(data, instance) {
            console.log('About to load data');
        },
        afterDataLoad: function(data, instance) {
            console.log('Data loaded successfully');
        },
        
        // Action hooks
        onSort: function(column, direction, instance) {
            console.log('Table sorted by column', column, 'in', direction, 'direction');
        },
        onFilter: function(searchTerm, filteredRows, instance) {
            console.log('Table filtered with term:', searchTerm);
        },
        onPaginate: function(pageNumber, instance) {
            console.log('Page changed to', pageNumber);
        }
    }
});

Search Capabilities

SmartTables includes powerful search capabilities with support for:

  • Fuzzy matching: Finds results even with typos or partial matches
  • Special data types: Intelligently searches dates, numbers, emails, phone numbers
  • Comparison operators: Support for >, <, = with numeric values
  • Multi-word search: Matches records containing any or all search terms
Pro Tip: Users can search with operators like ">30" to find all numeric values greater than 30, or use "@gmail.com" to find all Gmail addresses.

Responsive Behavior

SmartTables automatically adapts to different screen sizes by:

  • Hiding less important columns on smaller screens based on priority
  • Providing an expand/collapse interface to view hidden column data
  • Automatically measuring and optimizing column widths
<!-- Set column priorities with data attributes -->
<th data-priority="1">ID</th> <!-- Highest priority (last to hide) -->
<th data-priority="2">Name</th>
<th data-priority="3">Position</th>
<th data-priority="4">Office</th>
<th data-priority="5">Age</th> <!-- Lowest priority (first to hide) -->

<!-- Force columns to always remain visible -->
<th class="always-visible">Actions</th>

Export Options

SmartTables provides built-in export functionality:

  • Excel: Export to XLSX format
  • CSV: Export to CSV format
  • Copy: Copy table data to clipboard
// Get table instance
var table = document.getElementById('myTable').__smartTable;

// Export to different formats
table.exportData('excel');
table.exportData('csv');
table.exportData('copy');

API Methods

SmartTables exposes several methods for programmatic control:

// Get table instance
var table = document.getElementById('myTable').__smartTable;

// Redraw the table
table.draw();

// Sort by column
table.sortBy(2, 'asc'); // Sort by 3rd column ascending

// Filter the table
table.handleSearch('developer');

// Export data
table.exportData('excel');

// Hide/show columns
table.hideColumn(3);
table.showColumn(3);

// Clear AJAX cache for server-side tables
table.clearAjaxCache();

// Manually trigger prefetch of next page
table.prefetchNextPage();

// Generate a unique cache key for current parameters
const cacheKey = table.generateCacheKey();

// Destroy the instance
table.destroy();
Method Description Parameters Return Value
draw() Redraws the table with current settings None void
sortBy(columnIndex, direction) Sorts the table by specified column columnIndex: number, direction: 'asc'|'desc' void
handleSearch(query) Filters the table using the search query query: string void
exportData(format) Exports table data in the specified format format: 'excel'|'csv'|'copy' void
hideColumn(index) Hides the specified column index: number void
showColumn(index) Shows the specified column index: number void
clearAjaxCache() Clears all cached AJAX responses None void
prefetchNextPage() Manually fetches and caches the next page data None void
generateCacheKey() Generates a unique cache key for current parameters None string
destroy() Destroys the table instance and removes event listeners None void

Plugin System

SmartTables supports plugins to extend its functionality:

// Define a plugin
var myPlugin = {
    name: 'myPlugin',
    init: function() {
        console.log('Plugin initialized for table:', this.instance.table.id);
    },
    afterDraw: function() {
        console.log('Table was redrawn');
    }
};

// Initialize table with the plugin
var myTable = new SmartTables('tableId', {
    plugins: [myPlugin]
});

Performance Tips

  • For large datasets (1000+ rows), consider using server-side processing
  • Set appropriate perPage values to limit the number of rows rendered at once
  • Use the loading.duration option to show a loading indicator for long operations
  • Disable features you don't need (search, sort, pagination) for simpler tables

Framework Integrations

SmartTables.js can be seamlessly integrated with various modern frameworks and backend technologies. Here's how to implement it in different environments:

React Integration

import { useEffect, useRef } from 'react';
import { SmartTables } from 'smarttables';

const DataTable = ({ data, columns }) => {
    const tableRef = useRef(null);
    const tableInstance = useRef(null);

    useEffect(() => {
        if (tableRef.current) {
            // Initialize table
            tableInstance.current = new SmartTables(tableRef.current, {
                data: {
                    type: 'json',
                    source: data,
                    columns
                },
                hooks: {
                    afterDraw: () => {
                        console.log('Table redrawn');
                    }
                }
            });

            // Cleanup on unmount
            return () => {
                if (tableInstance.current) {
                    tableInstance.current.destroy();
        }
            };
        }
    }, [data, columns]);

    return (
        '' +
            '' +
                '' +
                    '{columns.map(column => (' +
                        '' +
                    '))}' +
                '' +
            '' +
            '' +
                '{/* Data will be populated by SmartTables */}' +
            '' +
        '
' + '{column.title}' + '
' ); }; // Usage example const App = () => { const columns = [ { data: 'id', title: 'ID', priority: 1 }, { data: 'name', title: 'Name', priority: 2 }, { data: 'position', title: 'Position', priority: 3 } ]; const data = [ { id: 1, name: 'John Doe', position: 'Developer' }, { id: 2, name: 'Jane Smith', position: 'Designer' } ]; return };
React-Specific Tips:
  • Use useRef to maintain a stable reference to both the table element and instance
  • Initialize the table in useEffect to ensure the DOM is ready
  • Clean up the table instance in the effect's cleanup function
  • Consider using React's Context API for global table state management
  • Use TypeScript for better type safety

Vue.js Integration (Composition API)

import { onMounted, onBeforeUnmount, ref } from 'vue';
import { SmartTables } from 'smarttables';

export default {
    setup(props) {
        const tableRef = ref(null);
        const tableInstance = ref(null);

        onMounted(() => {
            if (tableRef.value) {
                tableInstance.value = new SmartTables(tableRef.value, {
                    data: {
                        type: 'json',
                        source: props.data,
                        columns: props.columns
                    },
                    hooks: {
                        afterDraw: () => {
                            console.log('Table redrawn');
                        }
                    }
                });
            }
        });

        onBeforeUnmount(() => {
            if (tableInstance.value) {
                tableInstance.value.destroy();
            }
        });

        return {
            tableRef
        };
    }
}

// Usage example
const App = {
    template: '',
    setup() {
        const columns = [
            { data: 'id', title: 'ID', priority: 1 },
            { data: 'name', title: 'Name', priority: 2 },
            { data: 'position', title: 'Position', priority: 3 }
        ];

        const tableData = ref([
            { id: 1, name: 'John Doe', position: 'Developer' },
            { id: 2, name: 'Jane Smith', position: 'Designer' }
        ]);

        return {
            columns,
            tableData
        };
    }
};
Vue.js-Specific Tips:
  • Use ref for template references and instance management
  • Initialize in onMounted hook
  • Clean up in onBeforeUnmount hook
  • Consider using Pinia for state management
  • Use TypeScript for better type safety

Angular Integration

import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { SmartTables } from 'smarttables';

@Component({
    selector: 'app-data-table',
    template: '
{{column.title}}
' }) export class DataTableComponent implements OnInit, OnDestroy { @ViewChild('dataTable') tableRef: ElementRef; private tableInstance: SmartTables; columns = [ { data: 'id', title: 'ID', priority: 1 }, { data: 'name', title: 'Name', priority: 2 }, { data: 'position', title: 'Position', priority: 3 } ]; tableData = [ { id: 1, name: 'John Doe', position: 'Developer' }, { id: 2, name: 'Jane Smith', position: 'Designer' } ]; ngOnInit() { if (this.tableRef.nativeElement) { this.tableInstance = new SmartTables(this.tableRef.nativeElement, { data: { type: 'json', source: this.tableData, columns: this.columns }, hooks: { afterDraw: () => { console.log('Table redrawn'); } } }); } } ngOnDestroy() { if (this.tableInstance) { this.tableInstance.destroy(); } } }
Angular-Specific Tips:
  • Use @ViewChild for template references
  • Initialize in ngOnInit lifecycle hook
  • Clean up in ngOnDestroy lifecycle hook
  • Consider using NgRx for state management
  • Use TypeScript for better type safety

ASP.NET Core Integration

// Controller
[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{
    private readonly IDataService _service;

    public DataController(IDataService service)
    {
        _service = service;
    }

    [HttpGet]
    public async Task GetData([FromQuery] DataTableRequest request)
    {
        var data = await _service.GetDataAsync();
        
        // Apply server-side processing
        var result = data
            .Skip(request.Start)
            .Take(request.Length)
            .ToList();

        return Ok(new {
            draw = request.Draw,
            recordsTotal = data.Count,
            recordsFiltered = data.Count,
            data = result
        });
    }
}

// JavaScript/TypeScript
import { SmartTables } from 'smarttables';

const table = new SmartTables('myTable', {
    data: {
        type: 'ajax',
        source: '/api/data',
        serverSide: true,
        method: 'GET',
        headers: {
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
        }
    }
});
ASP.NET Core-Specific Tips:
  • Use dependency injection for services
  • Implement proper error handling and logging
  • Consider using Entity Framework Core for data access
  • Use middleware for authentication and authorization
  • Consider using SignalR for real-time updates

Pro Tip: When integrating with any framework, always ensure proper cleanup by calling the destroy() method when the component is unmounted or the table is no longer needed. This prevents memory leaks and ensures proper event cleanup.

Framework-Specific Considerations:

  • Modern JavaScript: Use ES6+ features like arrow functions, destructuring, and async/await for cleaner code.
  • TypeScript: Leverage TypeScript for better type safety and developer experience.
  • State Management: Consider using your framework's state management solution (Redux, Vuex, etc.) to handle table data and state.
  • CSRF Protection: Include appropriate CSRF tokens in AJAX requests when required by your backend framework.
  • Error Handling: Implement proper error handling both on the frontend and backend.
  • Performance: Use appropriate caching strategies and optimize database queries.
  • Security: Implement proper authentication and authorization mechanisms.
  • Testing: Write unit tests for both frontend and backend components.
  • Real-time Updates: Consider using WebSocket or similar technologies for real-time data updates.
  • Accessibility: Ensure your table implementation follows WCAG guidelines for accessibility.

Server-side Processing

SmartTables provides robust server-side processing capabilities for handling large datasets efficiently. Server-side processing offloads data filtering, sorting, and pagination to the server, which is essential when working with datasets containing thousands or millions of records.

Server-side Configuration

// Initialize table with server-side processing
const serverTable = new SmartTables('serverTable', {
    data: {
        type: 'ajax',
        source: 'scripts/optional/smartTables/mock-server.php',
        serverSide: true,
        method: 'GET',
        prefetch: true,
        cacheDuration: 300000, // 5 minutes cache duration
        columns: [
            { data: 'id', title: 'ID' },
            { data: 'name', title: 'Name' },
            { data: 'department', title: 'Department' },
            { data: 'age', title: 'Age' },
            { data: 'salary', title: 'Salary' },
            { data: 'bonus', title: 'Bonus' }
        ]
    }
});

Server Response Format

The server should return data in the following JSON format:

{
    "draw": 1,                    // Echo the draw parameter sent in the request
    "recordsTotal": 1000,         // Total records before filtering
    "recordsFiltered": 100,       // Total records after filtering
    "data": [                     // The actual data records
        { "id": 1, "name": "John Doe", "department": "Finance" },
        { "id": 2, "name": "Jane Smith", "department": "HR" }
        // ... more records
    ]
}

Request Parameters

SmartTables sends the following parameters to the server:

Parameter Description Example
draw Counter that ensures responses are processed in sequence 1
page Current page number (1-based) 2
perPage Number of records per page 10
search[value] Global search value Finance
search[regex] Whether the search is treated as a regular expression false
order[0][column] Index of the column being sorted 2
order[0][dir] Direction of sort (asc or desc) asc

Advanced Caching for Server-side Data

SmartTables implements an intelligent caching system to improve performance when working with server-side data:

  • Cache key generation: Creates unique keys based on page, search query, sorting criteria, and other parameters
  • Prefetching: Automatically loads the next page of data in advance
  • Cache invalidation: Automatically clears cache when search or sort parameters change
  • Cache duration: Configurable TTL for cached data
// Advanced caching configuration
const serverTable = new SmartTables('serverTable', {
    data: {
        type: 'ajax',
        source: 'api/data',
        serverSide: true,
        prefetch: true,         // Enable prefetching of next page
        cacheDuration: 600000,  // 10 minutes (in milliseconds)
        withCredentials: true   // Include credentials in requests
    },
    debug: true  // Enable debugging to see cache operations in console
});

Implementing a Server-side Endpoint

Here's an example of a PHP server-side implementation (mock-server.php):

// Example PHP implementation (simplified)
<?php
header('Content-Type: application/json');

// Get request parameters
$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
$perPage = isset($_GET['perPage']) ? intval($_GET['perPage']) : 10;
$search = isset($_GET['search']) && isset($_GET['search']['value']) ? $_GET['search']['value'] : '';
$order = isset($_GET['order']) && isset($_GET['order'][0]) ? $_GET['order'][0] : null;

// Column mapping for sorting
$columnMap = [
    0 => 'id',
    1 => 'name',
    2 => 'position',
    3 => 'office',
    4 => 'age',
    5 => 'startDate',
    6 => 'salary'
];

// Get data from database or other source
$data = getDataFromSource();
$totalRecords = count($data);
$filteredData = [];

// Apply search filter
if (!empty($search)) {
    $searchLower = strtolower($search);
    foreach ($data as $item) {
        $found = false;
        foreach ($item as $key => $value) {
            $strValue = strtolower((string)$value);
            if (strpos($strValue, $searchLower) !== false) {
                $found = true;
                break;
            }
        }
        if ($found) {
            $filteredData[] = $item;
        }
    }
} else {
    $filteredData = $data;
}

// Get total filtered count
$totalFiltered = count($filteredData);

// Apply sorting
if ($order) {
    $columnIndex = $order['column'];
    $columnKey = isset($columnMap[$columnIndex]) ? $columnMap[$columnIndex] : null;
    $direction = $order['dir'];
    
    if ($columnKey) {
        // Sort the filtered data
        usort($filteredData, function($a, $b) use ($columnKey, $direction) {
            // Handle different data types appropriately
            if (is_numeric($a[$columnKey]) && is_numeric($b[$columnKey])) {
                $aVal = (float)$a[$columnKey];
                $bVal = (float)$b[$columnKey];
                return $direction === 'asc' ? $aVal - $bVal : $bVal - $aVal;
            } else {
                $aVal = strtolower((string)$a[$columnKey]);
                $bVal = strtolower((string)$b[$columnKey]);
                return $direction === 'asc' ? strcmp($aVal, $bVal) : strcmp($bVal, $aVal);
            }
        });
    }
}

// Apply pagination
$offset = ($page - 1) * $perPage;
$paginatedData = array_slice($filteredData, $offset, $perPage);

// Return response
echo json_encode([
    'draw' => isset($_GET['draw']) ? intval($_GET['draw']) : 1,
    'recordsTotal' => $totalRecords,
    'recordsFiltered' => $totalFiltered,
    'data' => $paginatedData
]);
?>

Handling Pagination with Search

When implementing server-side processing with search functionality, it's critical to maintain consistent total counts across page navigation:

// Client-side code
const table = new SmartTables('myTable', {
    data: {
        serverSide: true,
        url: 'api/data',
        method: 'GET'
    },
    hooks: {
        afterDataLoad: function(response) {
            // Log total counts for debugging
            console.log('Total records:', response.recordsTotal);
            console.log('Filtered records:', response.recordsFiltered);
        }
    }
});
Important Server-side Implementation Considerations:
  • Always maintain consistent total counts between requests with the same search parameters
  • Calculate the maximum page based on the filtered records and ensure the requested page is valid
  • When a search query changes, reset to page 1 in both client and server implementations
  • Consistently apply sorting to maintain expected sort order across pagination
  • Include detailed logging to help diagnose issues with data processing

Real-time Debugging

SmartTables includes detailed logging when debug: true is enabled, showing:

  • AJAX request parameters and responses
  • Cache operations (hits, misses, keys generated)
  • Prefetching activity
  • Total row counts and pagination calculations
  • Search query processing
Pro Tip: When troubleshooting server-side issues, enable debug mode and examine both client-side console logs and server-side logs. Verify that total counts remain consistent when navigating between pages with an active search query.

Backend Framework Integrations

SmartTables can be integrated with various popular backend frameworks. Here are examples for different technologies:

Express.js Implementation
// Express.js server-side implementation
const express = require('express');
const app = express();
const cors = require('cors');

app.use(cors());
app.use(express.json());

// Sample data - in production, this would come from a database
const users = [
  { id: 1, name: 'John Doe', department: 'Engineering', age: 32, salary: 85000 },
  { id: 2, name: 'Jane Smith', department: 'Marketing', age: 28, salary: 65000 },
  // ... more records
];

// Column mapping for sorting
const columnMap = {
  0: 'id',
  1: 'name',
  2: 'department',
  3: 'age',
  4: 'salary'
};

// Endpoint for table data
app.get('/api/users', (req, res) => {
  try {
    // Parse parameters
    const draw = parseInt(req.query.draw) || 1;
    const page = parseInt(req.query.page) || 1;
    const perPage = parseInt(req.query.perPage) || 10;
    const search = req.query.search?.value || '';
    const order = req.query.order ? req.query.order[0] : null;
    
    // Apply search filter
    let filteredData = users;
    if (search) {
      const searchLower = search.toLowerCase();
      filteredData = users.filter(item => {
        return Object.values(item).some(val => 
          String(val).toLowerCase().includes(searchLower)
        );
      });
    }
    
    // Track total before sorting/pagination
    const recordsTotal = users.length;
    const recordsFiltered = filteredData.length;
    
    // Apply sorting
    if (order && order.column !== undefined) {
      const columnIndex = order.column;
      const columnKey = columnMap[columnIndex];
      const direction = order.dir || 'asc';
      
      if (columnKey) {
        filteredData.sort((a, b) => {
          const aVal = a[columnKey];
          const bVal = b[columnKey];
          
          // Handle numeric values
          if (typeof aVal === 'number' && typeof bVal === 'number') {
            return direction === 'asc' ? aVal - bVal : bVal - aVal;
          }
          
          // Handle strings
          const aStr = String(aVal).toLowerCase();
          const bStr = String(bVal).toLowerCase();
          return direction === 'asc' 
            ? aStr.localeCompare(bStr) 
            : bStr.localeCompare(aStr);
        });
      }
    }
    
    // Apply pagination
    const offset = (page - 1) * perPage;
    const paginatedData = filteredData.slice(offset, offset + perPage);
    
    // Return response in the format expected by SmartTables
    res.json({
      draw,
      recordsTotal,
      recordsFiltered,
      data: paginatedData
    });
  } catch (error) {
    console.error('Error processing request:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
NestJS Implementation
// users.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('api/users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll(@Query() query) {
    const draw = parseInt(query.draw) || 1;
    const page = parseInt(query.page) || 1;
    const perPage = parseInt(query.perPage) || 10;
    const search = query.search?.value || '';
    const order = query.order ? query.order[0] : null;
    
    return this.usersService.findWithPagination(draw, page, perPage, search, order);
  }
}

// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  private columnMap = {
    0: 'id',
    1: 'name',
    2: 'department',
    3: 'age',
    4: 'salary'
  };

  constructor(
    @InjectRepository(User)
    private usersRepository: Repository,
  ) {}

  async findWithPagination(draw, page, perPage, search, order) {
    // Build query with QueryBuilder
    const queryBuilder = this.usersRepository.createQueryBuilder('user');
    
    // Apply search
    if (search) {
      queryBuilder.where(
        'user.name LIKE :search OR user.department LIKE :search',
        { search: '%' + search + '%' }
      );
    }
    
    // Get total count before pagination
    const recordsTotal = await this.usersRepository.count();
    
    // Get filtered count
    const recordsFiltered = await queryBuilder.getCount();
    
    // Apply sorting
    if (order && order.column !== undefined) {
      const columnKey = this.columnMap[order.column];
      if (columnKey) {
        // Use string concatenation instead of template literals
        queryBuilder.orderBy('user.' + columnKey, order.dir.toUpperCase());
      }
    }
    
    // Apply pagination
    queryBuilder
      .skip((page - 1) * perPage)
      .take(perPage);
    
    // Execute query
    const data = await queryBuilder.getMany();
    
    // Return formatted response
    return {
      draw,
      recordsTotal,
      recordsFiltered,
      data
    };
  }
}

Django Implementation
# views.py
from django.http import JsonResponse
from django.db.models import Q
from .models import Employee

def employee_data(request):
    # Parse request parameters
    draw = int(request.GET.get('draw', 1))
    page = int(request.GET.get('page', 1))
    per_page = int(request.GET.get('perPage', 10))
    search_value = request.GET.get('search[value]', '')
    order_column = request.GET.get('order[0][column]', '')
    order_dir = request.GET.get('order[0][dir]', 'asc')
    
    # Column mapping
    column_map = {
        '0': 'id',
        '1': 'name',
        '2': 'department',
        '3': 'age',
        '4': 'salary',
    }
    
    # Get base queryset
    queryset = Employee.objects.all()
    
    # Get total count
    records_total = queryset.count()
    
    # Apply search filter
    if search_value:
        queryset = queryset.filter(
            Q(name__icontains=search_value) |
            Q(department__icontains=search_value) |
            Q(age__icontains=search_value) |
            Q(salary__icontains=search_value)
        )
    
    # Get filtered count
    records_filtered = queryset.count()
    
    # Apply sorting
    if order_column and order_column in column_map:
        sort_column = column_map[order_column]
        if order_dir == 'desc':
            sort_column = f'-{sort_column}'
        queryset = queryset.order_by(sort_column)
    
    # Apply pagination
    offset = (page - 1) * per_page
    queryset = queryset[offset:offset + per_page]
    
    # Prepare the data
    data = list(queryset.values('id', 'name', 'department', 'age', 'salary'))
    
    # Return JSON response
    return JsonResponse({
        'draw': draw,
        'recordsTotal': records_total,
        'recordsFiltered': records_filtered,
        'data': data
    })
FastAPI Implementation
# main.py
from fastapi import FastAPI, Query, Depends
from typing import Optional, List, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import or_, desc
from . import models, schemas, database

app = FastAPI()

# Column mapping
column_map = {
    0: "id",
    1: "name",
    2: "department",
    3: "age",
    4: "salary"
}

def get_db():
    db = database.SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/api/employees", response_model=schemas.SmartTablesResponse)
async def get_employees(
    draw: int = Query(1),
    page: int = Query(1),
    perPage: int = Query(10),
    search: Optional<str> = Query(None, alias="search[value]"),
    order_column: Optional<int> = Query(None, alias="order[0][column]"),
    order_dir: Optional<str> = Query("asc", alias="order[0][dir]"),
    db: Session = Depends(get_db)
):
    # Base query
    query = db.query(models.Employee)
    
    # Get total count
    records_total = query.count()
    
    # Apply search filter
    if search:
        query = query.filter(
            or_(
                models.Employee.name.ilike(f"%{search}%"),
                models.Employee.department.ilike(f"%{search}%"),
                models.Employee.age.cast(String).ilike(f"%{search}%"),
                models.Employee.salary.cast(String).ilike(f"%{search}%")
            )
        )
    
    # Get filtered count
    records_filtered = query.count()
    
    # Apply sorting
    if order_column is not None and order_column in column_map:
        column_name = column_map[order_column]
        column = getattr(models.Employee, column_name)
        
        if order_dir == "desc":
            query = query.order_by(desc(column))
        else:
            query = query.order_by(column)
    
    # Apply pagination
    offset = (page - 1) * perPage
    query = query.offset(offset).limit(perPage)
    
    # Execute query
    data = query.all()
    
    # Return formatted response
    return {
        "draw": draw,
        "recordsTotal": records_total,
        "recordsFiltered": records_filtered,
        "data": data
    }

// SmartTablesRequest.cs
public class SmartTablesRequest
{
    public int Draw { get; set; }
    public int Page { get; set; } = 1;
    public int PerPage { get; set; } = 10;
    public Search Search { get; set; } = new Search();
    public List<Order> Order { get; set; } = new List<Order>();
}

public class Search
{
    public string Value { get; set; } = "";
    public bool Regex { get; set; } = false;
}

public class Order
{
    public int Column { get; set; }
    public string Dir { get; set; } = "asc";
}

// SmartTablesResponse.cs
public class SmartTablesResponse<T>
{
    public int Draw { get; set; }
    public int RecordsTotal { get; set; }
    public int RecordsFiltered { get; set; }
    public List<T> Data { get; set; } = new List<T>();
}

// EmployeesController.cs
[ApiController]
[Route("api/[controller]")]
public class EmployeesController : ControllerBase
{
    private readonly ApplicationDbContext _context;
    private readonly ILogger<EmployeesController> _logger;
    
    // Column mapping
    private readonly Dictionary<int, string> _columnMap = new Dictionary<int, string>
    {
        { 0, "Id" },
        { 1, "Name" },
        { 2, "Department" },
        { 3, "Age" },
        { 4, "Salary" }
    };
    
    public EmployeesController(ApplicationDbContext context, ILogger<EmployeesController> logger)
    {
        _context = context;
        _logger = logger;
    }
    
    [HttpGet]
    public async Task<ActionResult<SmartTablesResponse<Employee>>> GetEmployees([FromQuery] SmartTablesRequest request)
    {
        try
        {
            // Start with base query
            IQueryable<Employee> query = _context.Employees;
            
            // Get total count
            int recordsTotal = await query.CountAsync();
            
            // Apply search filter
            if (!string.IsNullOrEmpty(request.Search.Value))
            {
                string searchValue = request.Search.Value.ToLower();
                query = query.Where(e => 
                    e.Name.ToLower().Contains(searchValue) ||
                    e.Department.ToLower().Contains(searchValue) ||
                    e.Age.ToString().Contains(searchValue) ||
                    e.Salary.ToString().Contains(searchValue)
                );
            }
            
            // Get filtered count
            int recordsFiltered = await query.CountAsync();
            
            // Apply sorting
            if (request.Order.Any())
            {
                var order = request.Order.First();
                if (_columnMap.TryGetValue(order.Column, out string column))
                {
                    if (order.Dir.ToLower() == "asc")
                    {
                        query = query.OrderBy(e => EF.Property<object>(e, column));
                    }
                    else
                    {
                        query = query.OrderByDescending(e => EF.Property<object>(e, column));
                    }
                }
            }
            
            // Apply pagination
            int skip = (request.Page - 1) * request.PerPage;
            query = query.Skip(skip).Take(request.PerPage);
            
            // Execute query
            var data = await query.ToListAsync();
            
            // Return formatted response
            return new SmartTablesResponse<Employee>
            {
                Draw = request.Draw,
                RecordsTotal = recordsTotal,
                RecordsFiltered = recordsFiltered,
                Data = data
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing SmartTables request");
            return StatusCode(500, "Internal server error");
        }
    }
}

AWS Lambda + API Gateway Implementation
// AWS Lambda handler (Node.js)
const AWS = require('aws-sdk');
const dynamoDB = new AWS.DynamoDB.DocumentClient();

// Column mapping
const columnMap = {
  0: 'id',
  1: 'name',
  2: 'department',
  3: 'age',
  4: 'salary'
};

exports.handler = async (event) => {
  try {
    // Parse query parameters
    const queryParams = event.queryStringParameters || {};
    
    const draw = parseInt(queryParams.draw) || 1;
    const page = parseInt(queryParams.page) || 1;
    const perPage = parseInt(queryParams.perPage) || 10;
    const search = queryParams['search[value]'] || '';
    
    let orderColumn = null;
    let orderDir = 'asc';
    
    if (queryParams['order[0][column]']) {
      orderColumn = parseInt(queryParams['order[0][column]']);
      orderDir = queryParams['order[0][dir]'] || 'asc';
    }
    
    // Fetch all data from DynamoDB
    // Note: In a production environment, you might want to use query/scan with filters
    const scanParams = {
      TableName: 'employees'
    };
    
    const scanResult = await dynamoDB.scan(scanParams).promise();
    let data = scanResult.Items || [];
    
    // Get total count
    const recordsTotal = data.length;
    
    // Apply search filter
    if (search) {
      const searchLower = search.toLowerCase();
      data = data.filter(item => {
        return Object.values(item).some(val => 
          String(val).toLowerCase().includes(searchLower)
        );
      });
    }
    
    // Get filtered count
    const recordsFiltered = data.length;
    
    // Apply sorting
    if (orderColumn !== null && columnMap[orderColumn]) {
      const sortKey = columnMap[orderColumn];
      
      data.sort((a, b) => {
        const aVal = a[sortKey];
        const bVal = b[sortKey];
        
        // Handle numeric sorting
        if (typeof aVal === 'number' && typeof bVal === 'number') {
          return orderDir === 'asc' ? aVal - bVal : bVal - aVal;
        }
        
        // Handle string sorting
        const aStr = String(aVal).toLowerCase();
        const bStr = String(bVal).toLowerCase();
        return orderDir === 'asc' ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr);
      });
    }
    
    // Apply pagination
    const offset = (page - 1) * perPage;
    const paginatedData = data.slice(offset, offset + perPage);
    
    // Return response
    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify({
        draw,
        recordsTotal,
        recordsFiltered,
        data: paginatedData
      })
    };
  } catch (error) {
    console.error('Error processing request:', error);
    
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify({ error: 'Internal server error' })
    };
  }
};
Cloudflare Worker Implementation
// Cloudflare Worker
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  // Parse URL and query parameters
  const url = new URL(request.url)
  
  // Only handle GET requests to /api/employees
  if (request.method !== 'GET' || !url.pathname.endsWith('/api/employees')) {
    return new Response('Not found', { status: 404 })
  }
  
  // Parse parameters
  const draw = parseInt(url.searchParams.get('draw')) || 1
  const page = parseInt(url.searchParams.get('page')) || 1
  const perPage = parseInt(url.searchParams.get('perPage')) || 10
  const search = url.searchParams.get('search[value]') || ''
  const orderColumn = parseInt(url.searchParams.get('order[0][column]'))
  const orderDir = url.searchParams.get('order[0][dir]') || 'asc'
  
  // Column mapping
  const columnMap = {
    0: 'id',
    1: 'name',
    2: 'department',
    3: 'age',
    4: 'salary'
  }
  
  try {
    // In a real implementation, fetch data from KV, D1 or external API
    // For demonstration, we use mock data
    let data = mockEmployees
    
    // Get total count
    const recordsTotal = data.length
    
    // Apply search filter
    if (search) {
      const searchLower = search.toLowerCase()
      data = data.filter(item => {
        return Object.values(item).some(val => 
          String(val).toLowerCase().includes(searchLower)
        )
      })
    }
    
    // Get filtered count
    const recordsFiltered = data.length
    
    // Apply sorting
    if (!isNaN(orderColumn) && columnMap[orderColumn]) {
      const sortKey = columnMap[orderColumn]
      
      data.sort((a, b) => {
        const aVal = a[sortKey]
        const bVal = b[sortKey]
        
        // Handle numeric sorting
        if (typeof aVal === 'number' && typeof bVal === 'number') {
          return orderDir === 'asc' ? aVal - bVal : bVal - aVal
        }
        
        // Handle string sorting
        const aStr = String(aVal).toLowerCase()
        const bStr = String(bVal).toLowerCase()
        return orderDir === 'asc' ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr)
      })
    }
    
    // Apply pagination
    const offset = (page - 1) * perPage
    const paginatedData = data.slice(offset, offset + perPage)
    
    // Return response
    const response = {
      draw,
      recordsTotal,
      recordsFiltered,
      data: paginatedData
    }
    
    return new Response(JSON.stringify(response), {
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      }
    })
  } catch (error) {
    return new Response(JSON.stringify({ error: 'Internal server error' }), {
      status: 500,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      }
    })
  }
}

// Mock data (in a real implementation, this would come from a database or KV)
const mockEmployees = [
  { id: 1, name: 'John Doe', department: 'Engineering', age: 32, salary: 85000 },
  { id: 2, name: 'Jane Smith', department: 'Marketing', age: 28, salary: 65000 },
  // Additional mock data...
]
Pro Tip: When implementing a server-side endpoint for SmartTables, focus on consistent data handling across requests to ensure smooth user experience. Always calculate filtered counts after applying search but before pagination to ensure accurate page navigation.

Troubleshooting

Common Issues:
  • Table not responsive? Make sure you've set data-priority attributes on your table headers.
  • Search not working as expected? Check your fuzzyMatch settings and try adjusting the threshold.
  • Export not working? Ensure you have the required dependencies for Excel export.
  • Performance issues? Try reducing the number of rows per page or use server-side processing.

Server-side Processing Issues

When working with server-side processing, you might encounter these common challenges:

Symptoms: The table shows different total counts when navigating between pages. For example, page 1 shows "Showing 1 to 10 of 25 entries" but page 2 shows "Showing 11 to 20 of 13 entries".

Solutions:

  • Server-side: Ensure the search filter is applied consistently before calculating the total count. Apply the search once to the entire dataset before pagination.
  • Client-side: Clear the cache when search parameters change using clearAjaxCache(), and ensure the total from the server response is stored consistently.
  • Debug: Add logging on both server and client to track the search term, filtered count, and total count across requests.
// Server-side fix (PHP example)
// Apply search filter once to get consistent count
$filteredData = [];
$totalRecords = count($data);

if (!empty($search)) {
    $searchLower = strtolower($search);
    foreach ($data as $item) {
        $found = false;
        foreach ($item as $key => $value) {
            $strValue = strtolower((string)$value);
            if (strpos($strValue, $searchLower) !== false) {
                $found = true;
                break;
            }
        }
        if ($found) {
            $filteredData[] = $item;
        }
    }
} else {
    $filteredData = $data;
}

// Get TOTAL filtered count BEFORE pagination
$totalFiltered = count($filteredData);

// Now apply pagination
$offset = ($page - 1) * $perPage;
$paginatedData = array_slice($filteredData, $offset, $perPage);

// Return consistent counts
return [
    'recordsTotal' => $totalRecords,
    'recordsFiltered' => $totalFiltered,
    'data' => $paginatedData
];

Symptoms: Stale data appears after changing search parameters, or prefetching doesn't seem to work.

Solutions:

  • Cache keys: Ensure cache keys include all relevant parameters (search term, sort column, sort direction)
  • Cache invalidation: Clear the cache when search or sort parameters change
  • Cache debugging: Enable debug mode to see cache operations in the console
// Client-side cache key generation
generateCacheKey() {
    // Include ALL parameters that affect the results in the key
    const params = {
        url: this.options.data.url,
        page: this.currentPage,
        perPage: this.options.perPage,
        search: this.searchQuery || '',
        sort: this.currentSort
    };
    
    this.log('Generating cache key with params:', JSON.stringify(params));
    return JSON.stringify(params);
}

Symptoms: Columns sort incorrectly, especially numeric, date, or special format columns.

Solutions:

  • Column mapping: Implement direct column mapping between column indices and field names
  • Data type detection: Sort different data types appropriately (numbers, dates, strings)
  • Normalization: Normalize values before comparison for consistent sorting
// Server-side sorting solution (PHP)
// Define column map for sorting
$columnMap = [
    0 => 'id',
    1 => 'name',
    2 => 'position',
    3 => 'office',
    4 => 'age',
    5 => 'startDate',
    6 => 'salary'
];

// Identify data types for special handling
$numericFields = ['id', 'age', 'salary', 'bonus', 'progress'];
$dateFields = ['startDate', 'hireDate'];

// Apply sorting with data type awareness
if ($order) {
    $columnIndex = $order['column'];
    $columnKey = isset($columnMap[$columnIndex]) ? $columnMap[$columnIndex] : null;
    $direction = $order['dir'];
    
    if ($columnKey) {
        usort($filteredData, function($a, $b) use ($columnKey, $direction, $numericFields, $dateFields) {
            // Handle numeric fields
            if (in_array($columnKey, $numericFields)) {
                $aVal = (float)$a[$columnKey];
                $bVal = (float)$b[$columnKey];
                return $direction === 'asc' ? $aVal - $bVal : $bVal - $aVal;
            }
            // Handle date fields
            else if (in_array($columnKey, $dateFields)) {
                $aDate = strtotime($a[$columnKey]);
                $bDate = strtotime($b[$columnKey]);
                return $direction === 'asc' ? $aDate - $bDate : $bDate - $aDate;
            }
            // Default string comparison
            else {
                $aVal = strtolower($a[$columnKey]);
                $bVal = strtolower($b[$columnKey]);
                $result = strcmp($aVal, $bVal);
                return $direction === 'asc' ? $result : -$result;
            }
        });
    }
}

Symptoms: Empty results when navigating to a page, or "No matching records found" errors when searching.

Solutions:

  • Max page calculation: Ensure the max page is calculated based on filtered results
  • Page clamping: If requested page exceeds max page, clamp to max page
  • Search resets: Reset to page 1 when search query changes
// Server-side page validation (PHP)
// Calculate maximum page based on filtered count
$maxPage = ceil($totalFiltered / $perPage);

// Ensure page is valid
if ($page < 1) {
    $page = 1;
} else if ($page > $maxPage && $maxPage > 0) {
    $page = $maxPage;
}

// Calculate offset based on validated page
$offset = ($page - 1) * $perPage;

// Client-side search reset
handleSearch(value) {
    this.searchQuery = value;

    if (this.options.data.serverSide) {
        // Reset the cache when search changes
        this.clearAjaxCache();
        
        this.currentPage = 1; // Reset to first page on new search
        this.loadAjax(); // Fetch new data with search query
    } else {
        // Client-side search logic
    }
}