1

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:

  1. Apply the sorted row order from one DataTable to others, to keep all tables visually synchronized?

  2. 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.

3
  • Why do you think you need to sort a table at all? Commented Oct 24 at 22:43
  • @SergeyAKryukov Why do you think they would never need to sort a table? What an absurd question. Commented Oct 24 at 23:02
  • @Alkenrinnstet — I did not say that. I just know cases when sorting is the result of XY-problem and is not really needed. I just happens sometimes. Commented Oct 24 at 23:05

2 Answers 2

1

Without looking too deeply into the DataTables library you are using, you should be able to prevent an infinite loop simply by adding a check for whether the table actually needs to be reordered.

For example, in applyTickerOrder, you can check whether the table in question is already in the order given by tickerOrder, and only call order() if it is not.

This still results in an additional round of fan-out of calls to syncTable, but each of the subsequent calls will no longer produce any further calls.

Sign up to request clarification or add additional context in comments.

Comments

1

You can synchronize the sorting of two or more tables by using the DataTables order() API call in a different (and simpler) way from how you are using it in your question.

I also use a global variable to track the difference between a re-ordering triggered by the user and a re-ordering triggered by the order() API call. This lets you avoid the recursion problem you have been getting.

Here is a very basic demo, using three hard-coded tables - just to show you the approach. It has none of the complexity of the code in your question - so I honestly don't know if you can adapt it to your needs or not.

But it answers the basic question in your title:

How can I sync row order across multiple Data-tables without triggering infinite re-sorting?

Here it is:

<!doctype html> <html> <head> <meta charset="UTF-8"> <title>Demo</title> <link href="https://cdn.datatables.net/2.3.4/css/dataTables.dataTables.css" rel="stylesheet" integrity="sha384-E5dk44xc8dR0JUWMOjxBpX00L8J6+oXgqAu/F+MDY5AjEdZfnLzhRRbqB49UDuQ6" crossorigin="anonymous"> <script src="https://code.jquery.com/jquery-3.7.0.js" integrity="sha384-ogycHROOTGA//2Q8YUfjz1Sr7xMOJTUmY2ucsPVuXAg4CtpgQJQzGZsX768KqetU" crossorigin="anonymous"></script> <script src="https://cdn.datatables.net/2.3.4/js/dataTables.js" integrity="sha384-WF5+lGOoGjkbZWWZ4BM1wA/VJC6EbJLPKnFSIvO9Vxm7HxkN0qD5IvYmr6FVM4V/" crossorigin="anonymous"></script> <link rel="stylesheet" type="text/css" href="https://datatables.net/media/css/site-examples.css"> </head> <body> <div style="margin: 20px;"> <table id="example_one" class="display dataTable cell-border" style="width:100%"> <thead> <tr> <th>Name</th> <th>Position</th> <th>Office in Country</th> </tr> </thead> <tbody> <tr> <td>Tiger Nixon</td> <td>System Architect</td> <td>Edinburgh</td> </tr> <tr> <td>Garrett Winters</td> <td>Accountant</td> <td>Tokyo</td> </tr> <tr> <td>Ashton Cox</td> <td>Junior "Technical" Author</td> <td>San Francisco</td> </tr> <tr> <td>Cedric Kelly</td> <td>Senior Javascript Developer</td> <td>Edinburgh</td> </tr> <tr> <td>Airi Satou</td> <td>Accountant</td> <td>Tokyo</td> </tr> <tr> <td>Brielle Williamson</td> <td>Integration Specialist</td> <td>New York</td> </tr> </tbody> </table> <br> <table id="example_two" class="display dataTable cell-border" style="width:100%"> <thead> <tr> <th>Name</th> <th>Age</th> <th>Start date</th> </tr> </thead> <tbody> <tr> <td>Tiger Nixon</td> <td>61</td> <td>2011/04/25</td> </tr> <tr> <td>Garrett Winters</td> <td>63</td> <td>2011/07/25</td> </tr> <tr> <td>Ashton Cox</td> <td>66</td> <td>2009/01/12</td> </tr> <tr> <td>Cedric Kelly</td> <td>22</td> <td>2012/03/29</td> </tr> <tr> <td>Airi Satou</td> <td>33</td> <td>2008/11/28</td> </tr> <tr> <td>Brielle Williamson</td> <td>61</td> <td>2012/12/02</td> </tr> </tbody> </table> <br> <table id="example_three" class="display dataTable cell-border" style="width:100%"> <thead> <tr> <th>Name</th> <th>Age</th> <th>Favorite Color</th> </tr> </thead> <tbody> <tr> <td>Tiger Nixon</td> <td>61</td> <td>green</td> </tr> <tr> <td>Garrett Winters</td> <td>63</td> <td>blue</td> </tr> <tr> <td>Ashton Cox</td> <td>66</td> <td>2009/01/12</td> </tr> <tr> <td>Cedric Kelly</td> <td>22</td> <td>red</td> </tr> <tr> <td>Airi Satou</td> <td>33</td> <td>green</td> </tr> <tr> <td>Brielle Williamson</td> <td>61</td> <td>orange</td> </tr> </tbody> </table> </div> <script> $(document).ready(function() { let table_one = new DataTable('#example_one', { columnDefs: [{ orderable: false, targets: [1, 2] }] }); let table_two = new DataTable('#example_two', { columnDefs: [{ orderable: false, targets: [1, 2] }] }); let table_three = new DataTable('#example_three', { columnDefs: [{ orderable: false, targets: [1, 2] }] }); var apiSorting = false; $('#example_one').on('order.dt', function() { let t1_json = JSON.stringify(table_one.order()); let t2_json = JSON.stringify(table_two.order()); let t3_json = JSON.stringify(table_three.order()); if (!apiSorting) { apiSorting = true; table_two.order(table_one.order()).draw(); table_three.order(table_one.order()).draw(); apiSorting = false; } }); $('#example_two').on('order.dt', function() { let t1_json = JSON.stringify(table_one.order()); let t2_json = JSON.stringify(table_two.order()); let t3_json = JSON.stringify(table_three.order()); if (!apiSorting) { apiSorting = true; table_one.order(table_two.order()).draw(); table_three.order(table_two.order()).draw(); apiSorting = false; } }); $('#example_three').on('order.dt', function() { let t1_json = JSON.stringify(table_one.order()); let t2_json = JSON.stringify(table_two.order()); let t3_json = JSON.stringify(table_three.order()); if (!apiSorting) { apiSorting = true; table_one.order(table_three.order()).draw(); table_two.order(table_three.order()).draw(); apiSorting = false; } }); }); </script> </body> </html>


Points to note:

I am using the latest version of DataTables, where there are 3 different sort states (ascending, descending, and the originally loaded data order). I think earlier v 1.10 versions of DataTables had only 2 states (asc and desc) - I may be mis-remembering that.

The main apiSorting boolean is how I avoid recursion. This is similar to what you tried (but it works in my example - and I was not able to recreate your "it sometimes breaks" problem).

I use JSON.stringify() as a simple way to convert a JavaScript array to a string, to make comparisons easier (e.g. "do table 2's current sort criteria match table 1's new sort criteria?", and so on).

The lines like table_two.order(table_one.order()).draw(); are where I set each table's sort criteria to match the sort criteria selected by the user.

I use a DataTables event to know when to update the other tables (the ones not re-ordered by the user). This is seen in $('#example_one').on('order.dt', function() {...} See draw() for more info.


(There is repetitive JS in my approach - that can be refactored and streamlined, no doubt.)

4 Comments

Thanks for the response man, helped a ton. I’ve just updated the question with my new implementation that incorporates your suggestions. Would appreciate it if you could take a quick look and let me know if I’m heading in the right direction. I mainly need help with moving away from DOM manipulations as it sometimes clashes with the internal state (say I hide a column on the table using wordpress, bugs relating to that would be void if I sorted just the internal datatables state)
Just a word of caution: Please don't change the existing question to something new/different. That just runs the risk that it will invalidate whatever answers were already written (which would be a problem for future visitors to your question - it would be very confusing!). If you are clarifying the original question, then that is OK. But it sounds like you have basically changed your question...
... So, I suggest you start with something very simple (like my example), which works. Then start adding to that, step-by-step, slowly adding the actual behavior you need. When you hit an issue, then you will have a new question you can ask - and you will have a minimal reproducible example you can also show us. And don't ask only me for feedback. Ask a new question, so that the whole SO community is more likely to see it.
"moving away from DOM manipulations" - that sounds generally like a very good idea. When you are using DataTables, you really should use the DataTables API functions, and the DataTables options as much as possible, instead of messing with the DOM.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.