# 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 './optional/smartTables/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 => (' +
                            '' +
                                '{column.title}' +
                            '' +
                        '))}' +
                    '' +
                '' +
                '' +
                    '{/* Data will be populated by SmartTables */}' +
                '' +
            ''
        );
    };

    // 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<IActionResult> 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 for handling large datasets efficiently. By enabling `data.serverSide: true`, data filtering, sorting, and pagination are offloaded to the server, ideal for datasets with thousands or millions of records. The server processes requests and returns only the necessary data for display, ensuring fast performance.

#### Server-side Configuration

Configure SmartTables to fetch data from a server endpoint using AJAX with server-side processing enabled:

    import { SmartTables } from 'smarttables';
    const serverTable = new SmartTables('serverTable', {
        data: {
            type: 'ajax',
            source: '/api/employees',
            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 must return data in the following JSON format, compatible with SmartTables' expectations:

    {
        "data": [
            { "id": 1, "name": "John Doe", "department": "Finance", "age": 32, "salary": 85000, "bonus": 5000 },
            { "id": 2, "name": "Jane Smith", "department": "HR", "age": 28, "salary": 65000, "bonus": 3000 }
            // ... more records
        ],
        "total": 1000 // Total records after filtering
    }

Important: Unlike some table libraries, SmartTables expects `total` instead of `recordsTotal` or `recordsFiltered`. Ensure your server returns the correct field to avoid pagination issues.

#### Request Parameters

SmartTables sends the following query parameters in GET requests to the server:

Parameter | Description | Example
--- | --- | ---
`page` | Current page number (1-based) | `2`
`perPage` | Number of records per page | `10`
`search` | Global search value | `Finance`
`sortColumn` | Name of the column being sorted | `department`
`sortDirection` | Direction of sort | `asc` or `desc`

Pro Tip: Use the `data.params` option to customize parameter names if your server expects different ones (e.g., `limit` instead of `perPage`):

    data: {
        type: 'ajax',
        source: '/api/employees',
        serverSide: true,
        params: {
            limit: function() { return this.perPage; },
            offset: function() { return (this.currentPage - 1) * this.perPage; },
            query: 'search'
        }
    }

#### Advanced Caching for Server-side Data

SmartTables enhances performance with client-side caching and prefetching:

- Cache Keys: Unique keys are generated based on `page`, `search`, `sortColumn`, and `sortDirection`.
- Prefetching: Automatically loads the next page when `prefetch: true`, reducing wait times.
- Cache Invalidation: Clears cache on parameter changes (e.g., new search term).
- Cache Duration: Configurable via `cacheDuration` (default: 5 minutes).

    const serverTable = new SmartTables('serverTable', {
        data: {
            type: 'ajax',
            source: '/api/employees',
            serverSide: true,
            prefetch: true,
            cacheDuration: 600000, // 10 minutes
            withCredentials: true,
            headers: { 'X-API-Key': 'your-api-key' }
        },
        debug: true // Log cache operations
    });

#### Implementing a Server-side Endpoint

Below are example implementations in multiple languages to demonstrate flexibility. Your server should handle pagination, filtering, and sorting based on the request parameters and return the expected response format.

###

This example uses ASP.NET Core with Entity Framework Core for database access, implementing server-side processing with caching.

    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Caching.Memory;
    using System;
    using System.Linq;
    using System.Threading.Tasks;

    public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Department { get; set; }
        public int Age { get; set; }
        public decimal Salary { get; set; }
        public decimal Bonus { get; set; }
    }

    public class PaginatedResponse<T>
    {
        public T[] Data { get; set; }
        public int Total { get; set; }
    }

    [ApiController]
    [Route("api/[controller]")]
    public class EmployeesController : ControllerBase
    {
        private readonly AppDbContext _context;
        private readonly IMemoryCache _cache;

        public EmployeesController(AppDbContext context, IMemoryCache cache)
        {
            _context = context;
            _cache = cache;
        }

        [HttpGet]
        public async Task<IActionResult> GetEmployees(
            [FromQuery] int page = 1,
            [FromQuery] int perPage = 10,
            [FromQuery] string search = "",
            [FromQuery] string sortColumn = "",
            [FromQuery] string sortDirection = "asc")
        {
            try
            {
                // Generate cache key
                string cacheKey = $"employees_{page}_{perPage}_{search}_{sortColumn}_{sortDirection}";

                // Try to get from cache
                if (!_cache.TryGetValue(cacheKey, out PaginatedResponse<Employee> response))
                {
                    // Base query
                    var query = _context.Employees.AsQueryable();

                    // Apply search
                    if (!string.IsNullOrEmpty(search))
                    {
                        query = query.Where(e =>
                            e.Name.Contains(search, StringComparison.OrdinalIgnoreCase) ||
                            e.Department.Contains(search, StringComparison.OrdinalIgnoreCase) ||
                            e.Age.ToString().Contains(search) ||
                            e.Salary.ToString().Contains(search) ||
                            e.Bonus.ToString().Contains(search));
                    }

                    // Get total count
                    int total = await query.CountAsync();

                    // Apply sorting
                    if (!string.IsNullOrEmpty(sortColumn))
                    {
                        query = sortDirection.ToLower() == "asc"
                            ? query.OrderBy(e => EF.Property<object>(e, sortColumn))
                            : query.OrderByDescending(e => EF.Property<object>(e, sortColumn));
                    }

                    // Apply pagination
                    query = query.Skip((page - 1) * perPage).Take(perPage);

                    // Execute query
                    var data = await query.ToArrayAsync();

                    response = new PaginatedResponse<Employee>
                    {
                        Data = data,
                        Total = total
                    };

                    // Cache the result
                    _cache.Set(cacheKey, response, TimeSpan.FromMinutes(5));
                }

                return Ok(response);
            }
            catch (Exception ex)
            {
                return StatusCode(500, new { data = Array.Empty<Employee>(), total = 0, error = "Internal server error" });
            }
        }
    }

ASP.NET Core Tips:

- Use `IMemoryCache` or `IDistributedCache` for caching responses.
- Validate `sortColumn` to prevent SQL injection when using dynamic sorting.
- Consider using dependency injection for database context and logging.

###

A simple Express.js implementation with in-memory data, adaptable to any database.

    const express = require('express');
    const cors = require('cors');
    const app = express();

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

    // Mock data (replace with database)
    const employees = [
        { id: 1, name: 'John Doe', department: 'Engineering', age: 32, salary: 85000, bonus: 5000 },
        { id: 2, name: 'Jane Smith', department: 'Marketing', age: 28, salary: 65000, bonus: 3000 }
        // ... more records
    ];

    app.get('/api/employees', async (req, res) => {
        try {
            const { page = 1, perPage = 10, search = '', sortColumn = '', sortDirection = 'asc' } = req.query;
            const pageNum = parseInt(page);
            const perPageNum = parseInt(perPage);

            // Filter data
            let filteredData = employees;
            if (search) {
                const searchLower = search.toLowerCase();
                filteredData = employees.filter(item =>
                    Object.values(item).some(val =>
                        String(val).toLowerCase().includes(searchLower)
                    )
                );
            }

            // Get total
            const total = filteredData.length;

            // Sort data
            if (sortColumn) {
                filteredData.sort((a, b) => {
                    const aVal = a[sortColumn];
                    const bVal = b[sortColumn];
                    if (typeof aVal === 'number' && typeof bVal === 'number') {
                        return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
                    }
                    const aStr = String(aVal).toLowerCase();
                    const bStr = String(bVal).toLowerCase();
                    return sortDirection === 'asc' ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr);
                });
            }

            // Paginate
            const offset = (pageNum - 1) * perPageNum;
            const paginatedData = filteredData.slice(offset, offset + perPageNum);

            res.json({
                data: paginatedData,
                total
            });
        } catch (error) {
            console.error(error);
            res.status(500).json({ data: [], total: 0, error: 'Internal server error' });
        }
    });

    app.listen(3000, () => console.log('Server running on port 3000'));

Node.js Tips:

- Integrate with a database like MongoDB or PostgreSQL for production.
- Use a caching layer like Redis for better performance.
- Add middleware for authentication if needed.

###

A FastAPI implementation using SQLAlchemy for database operations.

    from fastapi import FastAPI, Depends
    from sqlalchemy.orm import Session
    from sqlalchemy import or_, desc, asc
    from typing import Optional
    from pydantic import BaseModel
    import models
    from database import SessionLocal

    app = FastAPI()

    # Pydantic model for response
    class PaginatedResponse(BaseModel):
        data: list
        total: int

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

    @app.get("/api/employees", response_model=PaginatedResponse)
    async def get_employees(
        page: int = 1,
        perPage: int = 10,
        search: Optional[str] = None,
        sortColumn: Optional[str] = None,
        sortDirection: str = "asc",
        db: Session = Depends(get_db)
    ):
        try:
            # Base query
            query = db.query(models.Employee)

            # Apply search
            if search:
                query = query.filter(
                    or_(
                        models.Employee.name.ilike(f"%{search}%"),
                        models.Employee.department.ilike(f"%{search}%"),
                        models.Employee.age.cast(str).ilike(f"%{search}%"),
                        models.Employee.salary.cast(str).ilike(f"%{search}%"),
                        models.Employee.bonus.cast(str).ilike(f"%{search}%")
                    )
                )

            # Get total
            total = query.count()

            # Apply sorting
            if sortColumn:
                column = getattr(models.Employee, sortColumn, None)
                if column:
                    query = query.order_by(asc(column) if sortDirection == "asc" else desc(column))

            # Apply pagination
            offset = (page - 1) * perPage
            query = query.offset(offset).limit(perPage)

            # Execute
            data = query.all()

            return {
                "data": data,
                "total": total
            }
        except Exception as e:
            return {"data": [], "total": 0, "error": str(e)}

Python Tips:

- Use Pydantic models for response validation.
- Implement caching with libraries like `cachetools`.
- Secure endpoints with FastAPI's security utilities.

#### Handling Pagination with Search

Ensure consistent total counts across requests to prevent pagination issues:

    const table = new SmartTables('myTable', {
        data: {
            serverSide: true,
            source: '/api/employees',
            method: 'GET'
        },
        hooks: {
            afterDataLoad: function(response) {
                console.log('Total records:', response.total);
            }
        }
    });

Important Considerations:

- Calculate `total` after applying filters but before pagination.
- Reset to page 1 when search changes to avoid invalid page requests.
- Validate `sortColumn` to ensure it matches valid fields.
- Use logging to debug inconsistencies in counts or data.

#### Real-time Debugging

Enable `debug: true` to log AJAX requests, responses, and cache operations:

- Request parameters sent to the server.
- Cache hits, misses, and key generation.
- Prefetching status and errors.
- Total counts and pagination calculations.

Pro Tip: Combine client-side debug logs with server-side logging to troubleshoot issues. Check that `total` remains consistent across pages for the same search query.

## 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
        }
    }

## Managing Records with SmartTables

SmartTables provides powerful functionality for managing records with full CRUD (Create, Read, Update, Delete) operations. This allows you to build interactive data management interfaces with minimal effort.

#### Basic Setup for Record Management

    // Import the SmartTables module
    import { SmartTables } from './optional/smartTables/smartTables.bundle.js';

    document.addEventListener('DOMContentLoaded', () => {
        // Initialize SmartTables with data management options
        const clientTable = new SmartTables('clientTable', {
            // Data configuration
            data: {
                type: 'json',
                source: sampleData,  // Your data array
                idField: 'id',       // Field that uniquely identifies each record
                columns: [
                    { data: 'id', title: 'ID', editable: false },
                    { data: 'name', title: 'Name', required: true },
                    { data: 'email', title: 'Email', type: 'email', required: true },
                    { data: 'phone', title: 'Phone', type: 'tel' },
                    { data: 'active', title: 'Active', type: 'boolean' },
                    {
                        data: 'actions',
                        title: 'Actions',
                        sortable: false,
                        editable: false,
                        render: (data, row) =>
                            '<button type="button" class="btn btn-primary btn-xs edit-btn" data-id="' + row.id + '">Edit</button> ' +
                            '<button type="button" class="btn btn-danger btn-xs delete-btn" data-id="' + row.id + '">Del</button>'
                    }
                ]
            },
            // Enable record management features
            addRecord: true,  // Shows an Add Record button
            // Other options...
        });

        // Handle edit and delete button clicks with event delegation
        clientTable.table.addEventListener('click', e => {
            if (e.target.classList.contains('edit-btn')) {
                const rowId = e.target.getAttribute('data-id');
                clientTable.edit(rowId);
            } else if (e.target.classList.contains('delete-btn')) {
                const rowId = e.target.getAttribute('data-id');
                clientTable.delete(rowId);
            }
        });
    });

#### Advanced Record Management with Hooks

SmartTables provides a comprehensive hooks system to customize the behavior of record management operations:

    const table = new SmartTables('myTable', {
        // ... other options
        hooks: {
            // Edit operation hooks
            beforeEdit(rowId) {
                // Called before edit mode is entered
                console.log('Editing row: ' + rowId);
                return true; // Return false to prevent editing
            },
            afterEdit(rowId, rowData, success) {
                // Called after edit is completed
                console.log('Edit completed: ' + success);
            },
            onEditModalCreated(modalHTML, rowId, rowData) {
                // Customize the edit modal HTML
                return modalHTML;
            },
            onEditModalBeforeShow(modalElement, rowId, rowData) {
                // Access the modal DOM before it's shown
            },
            onEditDataCollected(updatedData, rowId, originalData) {
                // Process and validate form data
                return updatedData;
            },
            onEditSuccess(rowId, updatedRecord, submittedData) {
                // Handle successful update
                console.log('Record updated successfully');
            },
            onEditError(rowId, error, attemptedData) {
                // Handle update errors
                console.error('Update failed', error);
            },

            // Delete operation hooks
            beforeDelete(rowId) {
                // Called before deletion
                return confirm('Are you sure you want to delete this record?');
            },
            afterDelete(rowId, data, success) {
                // Called after deletion attempt
                if (success) console.log('Deletion successful');
            },
            onDeleteModalCreated(modalHtml, rowId) {
                // Customize deletion confirmation modal
                return modalHtml;
            },
            onDeleteSuccess(rowId, deletedRecord) {
                // Handle successful deletion
            },

            // Add record operation hooks
            beforeAddRecord(initialData, options) {
                // Called before add form is shown
                return initialData; // Return modified initial data
            },
            afterAddRecord(newRecordData, success) {
                // Called after add operation completes
                if (success) console.log('Record added');
            },
            onAddModalCreated(modalHTML, initialData, options) {
                // Customize add record modal
                return modalHTML;
            },
            onAddModalBeforeShow(modalElement, initialData, options) {
                // DOM access before modal shows
            },
            onAddDataCollected(newRecordData, options) {
                // Process and validate new record data
                return newRecordData;
            },
            onAddRecordSuccess(newRecord) {
                // Handle successful addition
            },
            onAddRecordError(error, attemptedData) {
                // Handle addition errors
            },
            onAddCancelled(options) {
                // Called when add is cancelled
            }
        }
    });

#### Column Type Configuration

SmartTables supports various input types for different data fields:

Type | Description | Additional Options
--- | --- | ---
`text` | Default text input | maxlength, pattern
`number` | Numeric input | min, max, step
`email` | Email input with validation | pattern
`tel` | Telephone input | format, placeholder
`date` | Date picker | min, max
`select` | Dropdown selection | options (array)
`boolean` | Checkbox | -
`password` | Password input | minlength

    // Column configuration examples for different types
    columns: [
        {
            data: 'salary',
            title: 'Salary',
            type: 'number',     // Numeric input
            min: 0,             // Minimum value
            step: 500,          // Increment by 500
            render: data => '$' + data.toLocaleString() // Format with $ and commas
        },
        {
            data: 'department',
            title: 'Department',
            type: 'select',     // Dropdown select input
            options: [          // Available options
                'Engineering',
                'Sales',
                'Marketing',
                'Human Resources'
            ]
        },
        {
            data: 'joinDate',
            title: 'Join Date',
            type: 'date',       // Date input
            render: data => new Date(data).toLocaleDateString() // Format date
        },
        {
            data: 'active',
            title: 'Active',
            type: 'boolean',    // Boolean/checkbox
            render: data => data ?
                '<span class="badge bg-success">Yes</span>' :
                '<span class="badge bg-danger">No</span>'
        }
    ]

#### Visual Feedback for Row States

Implement visual feedback for different row states during CRUD operations:

    // Track row states
    const rowStates = {
        editing: null,     // ID of the row being edited
        saved: new Set(),  // Set of recently saved row IDs
        adding: false      // Flag for adding operation
    };

    // Apply classes in hooks
    hooks: {
        beforeEdit(rowId) {
            rowStates.editing = rowId;
            const rowElement = this.table.querySelector('tbody tr[data-id="' + rowId + '"]');
            if (rowElement) {
                rowElement.classList.add('editing');
            }
            return true;
        },

        afterEdit(rowId, rowData, success) {
            rowStates.editing = null;
            if (success) {
                rowStates.saved.add(rowId);
                setTimeout(() => {
                    rowStates.saved.delete(rowId);
                    const row = this.table.querySelector('tbody tr[data-id="' + rowId + '"]');
                    if (row) row.classList.remove('saved');
                }, 3000);
            }
            this.draw();
        }
    }

Pro Tip: When implementing CRUD operations, consider using a custom modal system for consistency across the application. SmartTables hooks make it easy to integrate with custom UI components.

##### On This Page

- Introduction
- Installation
- Configuration
- Advanced Configuration
- Data Loading
- Data Import
- Event Hooks
- Search Capabilities
- Responsive Behavior
- Export Options
- API Methods
- Plugin System
- Performance Tips
- Framework Integrations
- Server-side Processing
- Troubleshooting
- Managing Records

#### Hi Sunny,

how can I help you today?
Analyze my data

Create a new report

Summarize my Calendar

App Builder
Unlock limitless design potential with 16+ layout combinations and extensive customization options—tailor your dashboard to fit your exact needs.
Header position fixed
Navigation full height
Navigation position fixed
Navigation collapsed
Navigation Minified
Dark Navigation
Colorblind Mode
High Contrast Mode