This document describes the comprehensive refactoring of TablixJS to fix multiple issues related to server-side pagination, sorting, filtering, and search functionality.
Problem: Multi/single row selection did not work immediately after initial rendering. It started working only after the grid refreshed (e.g., after sorting or filtering).
Root Cause: The afterRender event was not being triggered during initial load, preventing the SelectionManager from properly initializing the UI.
Solution:
- Added explicit
afterRenderevent trigger in theinit()method after initial table render - Ensured selection event listeners are properly bound after first data load
Files Changed:
src/core/Table.js- Modifiedinit()method to triggerafterRenderevent
Problem: On first load, the "Filter by values" dropdown was empty. After sorting or applying a conditional filter once, it started working.
Root Cause: The getColumnUniqueValues() method always used originalData, which for server mode wasn't populated with the server response data.
Solution:
- Modified
getColumnUniqueValues()to usefilteredDatafor server mode (which contains the loaded server data) - For client mode, continues to use
originalDataas before
Files Changed:
src/core/FilterManager.js- UpdatedgetColumnUniqueValues()method
Problem: Any server filter or sort action triggered two network requests:
- First through the sorting/filtering loader
- Then unexpectedly through
serverDataLoaderagain
Root Cause: Each manager (SortingManager, FilterManager, PaginationManager) was independently calling server loaders, and then refreshTable() would trigger pagination's getPageData() which made another request.
Solution:
- Implemented Centralized State Management: Created new
StateManagerclass to maintain unified state for all operations - Unified Server Loading: Created single
_loadServerData()method inTable.jsthat:- Collects all current state (pagination, sort, filters, search) from
StateManager - Determines the appropriate loader to use based on active operations
- Makes only ONE request with all parameters
- Updates all managers with the response
- Collects all current state (pagination, sort, filters, search) from
- Deprecated Individual Loaders: Marked
_sortServer()and_filterServer()as deprecated - Modified
refreshTable(): Now delegates to_loadServerData()for server mode operations
Files Changed:
src/core/StateManager.js- NEW FILE - Centralized state managementsrc/core/Table.js- Added_loadServerData()method and state manager integrationsrc/core/SortingManager.js- Updated to use state manager, deprecated_sortServer()src/core/FilterManager.js- Updated to use state manager, deprecated_filterServer()src/core/PaginationManager.js- SimplifiedgetServerPageData()to delegate to unified loader
Problem:
- Sort by
test_id - Apply a value filter on
test_name - Change sort to
test_name→ filter resets to empty{}
Root Cause: No persistent state storage. Each operation would overwrite the previous state instead of merging it.
Solution:
StateManagernow maintains persistentfilters,sort, andsearchstate- All operations update state through
StateManagermethods - State is always preserved and merged, never overwritten
- All server requests use
getServerParams()which includes all current state
Files Changed:
src/core/StateManager.js- Maintains persistent statesrc/core/SortingManager.js- Updates state instead of replacing itsrc/core/FilterManager.js- Updates state instead of replacing it
Problem: When controls.search = true, typing into search triggered server requests, but the search term was not present in the params object sent to server loaders.
Root Cause: Search term was stored in SearchManager but never passed to server loader methods.
Solution:
StateManagernow tracks search term viaupdateSearch()methodSearchManager.performSearch()updates state manager with current search term_loadServerData()always includes search term in params viagetServerParams()- Server loaders now receive:
{ page, pageSize, sort, filters, search }
Files Changed:
src/core/StateManager.js- Addedsearchto state andupdateSearch()methodsrc/core/SearchManager.js- Updated to sync search term with state managersrc/core/Table.js-_loadServerData()includes search in params
Problem: Returning sorted results from the API did not update the table. Pagination updates worked, but sorting did not update the table UI.
Root Cause: Sorting triggered two requests causing state desync, and the render pipeline was ignoring the result of the sorting request.
Solution:
- Fixed by eliminating duplicate requests (see #3)
_loadServerData()properly updatesDataManagerwith server response- Ensures table renders with the new sorted data from the server
Files Changed:
src/core/Table.js-_loadServerData()properly handles response and renders table
Problem: Some requests sent sort: undefined or sort: null.
Root Cause: Sort state was not normalized before being included in params.
Solution:
StateManager.getServerParams()always returnssortas an object- If no sort is active, returns
{}(empty object) instead ofnullorundefined - Consistent format across all loaders
Files Changed:
src/core/StateManager.js-getServerParams()normalizes sort to{}if null
A new centralized state management class that maintains:
{
page: 1,
pageSize: 10,
totalRows: 0,
sort: null, // { column: 'name', direction: 'asc' } or null
filters: {}, // { columnName: { type: 'value', values: [...] } }
search: '', // Global search term
isLoading: false
}Key Methods:
getServerParams()- Returns normalized params for server requestsupdatePagination()- Updates pagination stateupdateSort()- Updates sort stateupdateFilters()- Updates filter stateupdateSearch()- Updates search stateresetPage()- Resets to page 1 (used when filters/sort/search change)
Old Flow (caused duplicate requests):
User clicks sort
→ SortingManager._sortServer() makes request
→ SortingManager.sort() calls refreshTable()
→ refreshTable() calls PaginationManager.getPageData()
→ getPageData() calls serverDataLoader again ❌ DUPLICATE
New Flow (single request):
User clicks sort
→ SortingManager updates StateManager.sort
→ SortingManager calls table.refreshTable()
→ refreshTable() detects server mode
→ refreshTable() calls _loadServerData()
→ _loadServerData() gets unified params from StateManager
→ _loadServerData() makes ONE request ✅
→ Updates DataManager and renders table
All server loaders now receive consistent parameters:
{
page: 1, // Current page number
pageSize: 10, // Items per page
sort: {}, // { column: 'name', direction: 'asc' } or {}
filters: {}, // { columnName: { type: 'value', values: [...] } }
search: 'keyword' // Search term or empty string
}When making unified requests, loaders are prioritized as follows:
- serverFilterLoader - Used when filters are active
- serverSortLoader - Used when sort is active (and no filters)
- serverDataLoader - Used for basic pagination
This ensures the most specific loader handles the request.
Good News: No breaking changes! Your existing code will continue to work.
The fixes are internal refactoring that maintains backward compatibility.
Your server loaders now receive an additional search parameter:
const table = new Table('#myTable', {
pagination: {
mode: 'server',
serverDataLoader: async (params) => {
// params now includes:
// - page, pageSize (as before)
// - sort (now always an object, never null/undefined)
// - filters (as before)
// - search (NEW - search term string)
const response = await fetch(`/api/data?${new URLSearchParams(params)}`);
return response.json();
}
}
});Server-side implementation example:
app.get('/api/data', (req, res) => {
const { page, pageSize, sort, filters, search } = req.query;
let query = db.select('*').from('users');
// Apply search
if (search) {
query = query.where(function() {
this.where('name', 'like', `%${search}%`)
.orWhere('email', 'like', `%${search}%`);
});
}
// Apply filters
if (filters && Object.keys(filters).length > 0) {
// ... filter logic
}
// Apply sorting
if (sort && sort.column) {
query = query.orderBy(sort.column, sort.direction);
}
// Apply pagination
const offset = (page - 1) * pageSize;
query = query.limit(pageSize).offset(offset);
const data = await query;
const totalRows = await db.count('*').from('users').first();
res.json({ data, totalRows: totalRows.count });
});To verify all fixes are working:
-
Test Selection on First Load:
- Load table with server data
- Try to select a row immediately → Should work ✅
-
Test Filter by Values:
- Open filter dropdown on any column
- Should show values from loaded data ✅
-
Test Single Request:
- Open browser DevTools Network tab
- Sort a column → Should see only ONE request ✅
- Apply a filter → Should see only ONE request ✅
-
Test Persistent Filters:
- Apply a filter on column A
- Change sort order → Filter should remain active ✅
- Change page → Filter should remain active ✅
-
Test Search Parameter:
- Type in search box
- Check network request params → Should include
searchparameter ✅
-
Test Sort Consistency:
- Check all requests →
sortshould be{}or{ column, direction }, never null ✅
- Check all requests →
- ✅
src/core/StateManager.js- NEW - Centralized state management - ✅
src/core/Table.js- Added unified server loading, state manager integration - ✅
src/core/SortingManager.js- Updated to use state manager - ✅
src/core/FilterManager.js- Updated to use state manager - ✅
src/core/PaginationManager.js- Updated to use state manager - ✅
src/core/SearchManager.js- Updated to use state manager
✅ 100% backward compatible - All existing code will continue to work without modifications.
The changes are internal refactoring that maintains the same public API.
- Reduced Network Traffic: Single request per action instead of multiple
- Better State Management: Consistent state across all operations
- Faster Response Time: Eliminated redundant requests and processing
These fixes provide a robust foundation for server-side operations in TablixJS, ensuring:
- Consistent behavior across all features
- Single, well-formed requests for all operations
- Persistent state that doesn't get lost during operations
- Proper initialization of all features on first load
- Clean, maintainable code architecture
All issues have been resolved, and the grid now works reliably with real server APIs.