I have four Data-tables tables on a page, each displaying different financial metrics about the same set of stocks. Each table shares a common Ticker column, and every table has the same number of rows and the same tickers in the same order.
When a user clicks a column header, the table sorts correctly (ascending/descending).
However, when the user switches to another tab, the sort order is lost.
To maintain visual consistency, I want the same ticker order applied across all tables.
Expected behaviour
When a user sorts Table A, the new order should be captured.
That order should then be applied to Tables B, C, and D.
Consequntly if they sort something on Table C, the row order should be updated to match on Tables A, B, and D
Problem
My current implementation works on the first sort, but after that, it enters an infinite loop.
The loop occurs because when the new order is applied to other tables, it triggers their own sort events, which then recursively re-apply the order over and over on its own.
Question
What is the best way to:
Apply the sorted row order from one DataTable to others, to keep all tables visually synchronized?
Prevent the change event from recursively triggering
Here is my current implementation
(function($) { 'use strict'; // Configuration const TABLE_IDS = [ 'tablepress-daily_performance', 'tablepress-annualized_performance', 'tablepress-historical_performance', ]; const TICKER_COLUMN = 0; const MAX_INIT_ATTEMPTS = 50; const INIT_CHECK_INTERVAL = 100; // State management let isSyncing = false; let dataTables = {}; let initAttempts = 0; /** * Check if all DataTables are initialized */ function areAllTablesReady() { for (let tableId of TABLE_IDS) { const table = $('#' + tableId); if (table.length === 0 || !$.fn.DataTable.isDataTable('#' + tableId)) { return false; } } return true; } /** * Initialize DataTable references */ function initializeDataTables() { TABLE_IDS.forEach(tableId => { // Get the API instance dataTables[tableId] = $('#' + tableId).DataTable(); }); console.log('All DataTables initialized successfully'); } /** * Get the current order of Tickers from a sorted table * @param {string} sourceTableId - The ID of the table that was just sorted * @returns {Array<string>} The list of Tickers in the new display order */ function getTickerOrder(sourceTableId) { const table = dataTables[sourceTableId]; // Use DataTables API to get data in the 'current' display order const tickerOrder = table.rows({ order: 'current', search: 'applied' }).data().map(function(row) { return row[TICKER_COLUMN]; }).toArray(); console.log(`Captured ${tickerOrder.length} Tickers from ${sourceTableId}`); return tickerOrder; } /** * Apply ticker order to a target table by re-positioning rows * This is more reliable than clearing and re-adding rows. * @param {string} targetTableId - The ID of the table to be reordered * @param {Array<string>} tickerOrder - The master order of Tickers */ function applyTickerOrder(targetTableId, tickerOrder) { const table = dataTables[targetTableId]; // 1. Map Ticker to the actual DataTables row node const tickerToRowNodeMap = new Map(); table.rows().every(function() { const rowData = this.data(); const rowNode = this.node(); if (rowData && rowNode) { tickerToRowNodeMap.set(rowData[TICKER_COLUMN], rowNode); } }); // 2. Iterate through the master order and move each row to its new index tickerOrder.forEach((ticker, newIndex) => { const rowNode = tickerToRowNodeMap.get(ticker); if (rowNode) { // Use row().index(newIndex) to manually set the new position of the row // This is the core of the synchronization table.row(rowNode).index(newIndex); } }); // 3. Redraw the table to display the new order. // We set order([]) to clear any previous sorting visual and only show the manual reorder. table.order([]).draw(false); console.log(`Applied and drew table ${targetTableId} to sync order.`); } /** * Sync all tables to match the source table's order * @param {string} sourceTableId - The ID of the table that triggered the sort */ function syncTables(sourceTableId) { // Prevent recursive syncing if (isSyncing) { return; } isSyncing = true; console.log('Syncing tables based on:', sourceTableId); try { // Get the master ticker order from the source table const tickerOrder = getTickerOrder(sourceTableId); // Apply to all other tables TABLE_IDS.forEach(tableId => { if (tableId !== sourceTableId) { applyTickerOrder(tableId, tickerOrder); } }); console.log('Sync completed successfully'); } catch (error) { console.error('Error during sync:', error); } finally { // Reset the flag isSyncing = false; } } /** * Attach event listeners to all tables */ function attachEventListeners() { TABLE_IDS.forEach(tableId => { const table = dataTables[tableId]; // Listen for order event (triggered when user sorts) // The 'order.dt' event fires *after* the sort has been applied. table.on('order.dt', function() { console.log('User Sorted table:', tableId); // Use a small delay to ensure DataTables' internal state is fully stable // after the 'order.dt' event fires. setTimeout(function() { syncTables(tableId); }, 50); }); }); console.log('Event listeners attached to all tables'); } /** * Main initialization function */ function init() { const checkInterval = setInterval(function() { initAttempts++; if (areAllTablesReady()) { clearInterval(checkInterval); initializeDataTables(); attachEventListeners(); console.log('TablePress sync initialization complete'); } else if (initAttempts >= MAX_INIT_ATTEMPTS) { clearInterval(checkInterval); console.error('TablePress sync failed to initialize - tables not ready after 5 seconds'); } }, INIT_CHECK_INTERVAL); } // Start initialization when DOM is ready $(document).ready(function() { init(); }); })(jQuery); I tried temporarily disabling event listeners while applying the new order, like this:
isSyncing = true; // apply order to other tables isSyncing = false; and checking if (isSyncing) return; inside the sort handler. This helps partially but sometimes breaks sorting or misses updates.
I also tried removing and re-attaching the click event listeners dynamically after syncing, but that approach felt hacky and still caused unpredictable behavior.
Is there a better way to implement my functionality or anything I can do to prevent the infinite loop?
Update
So I rewrote my code based on the answers provided. I moved the reordering logic to occour after order.dt completes and have a flag to prevent unnecessary draws. There is now a master order which is used to update the order of the other tables
Below is the latest version of my code:
(function($) { const TABLE_IDS = ['#table_1', '#table_2', '#table_3', '#table_4']; let dataTables = {}; let isReordering = false; function addClickListeners() { TABLE_IDS.forEach(selector => { const $t = $(selector); if (!$t.length) return; function attachSortListener(api) { api.on('order.dt', function() { if (isReordering) return; const sortedTableId = '#' + api.table().node().id; const masterTickerOrder = api.column(0, { order: 'current' }).data().toArray(); console.log('New master order from ' + sortedTableId + ':', masterTickerOrder); TABLE_IDS.forEach(otherTableSelector => { if (otherTableSelector !== sortedTableId) { reorderTableByTickers(otherTableSelector, masterTickerOrder); } }); }); } if ($.fn.dataTable && $.fn.dataTable.isDataTable(selector)) { dataTables[selector] = $t.DataTable(); attachSortListener(dataTables[selector]); } else { $t.one('init.dt', function() { dataTables[selector] = $t.DataTable(); attachSortListener(dataTables[selector]); }); } }); } function reorderTableByTickers(selector, customOrder) { function withApi(cb) { if (dataTables[selector]) return cb(dataTables[selector]); if ($.fn.dataTable && $.fn.dataTable.isDataTable(selector)) { dataTables[selector] = $(selector).DataTable(); return cb(dataTables[selector]); } $(selector).one('init.dt', function() { dataTables[selector] = $(this).DataTable(); cb(dataTables[selector]); }); } withApi(function(api) { const tickerIndex = 0; const currentRows = api.rows().nodes().toArray(); const sortedRows = currentRows.sort((a, b) => { const tickerA = a.children[tickerIndex]?.textContent.trim() || ''; const tickerB = b.children[tickerIndex]?.textContent.trim() || ''; const indexA = customOrder.indexOf(tickerA); const indexB = customOrder.indexOf(tickerB); return (indexA === -1 ? Infinity : indexA) - (indexB === -1 ? Infinity : indexB); }); isReordering = true; api.order([]).clear().rows.add(sortedRows).draw(false); isReordering = false; console.log('Table ' + selector + ' has been reordered.'); }); } $(document).ready(addClickListeners); })(jQuery); The one thing I cannot figure out is how to go about doing the reordering through Datatables. DataTables has a rowReorder extension, but it's more for drag-and-drop scenarios, and didn't really work for me. I was thinking of defining a custom sort type, but wanted some advice before pouring several hours into this.