Server-side with columnControl: Active state CSS and button Clear Search not working

Server-side with columnControl: Active state CSS and button Clear Search not working

Just1nJust1n Posts: 5Questions: 2Answers: 0

Hi,

I’m using DataTables with serverSide: true and the columnControl feature.

I am facing two related issues when switching from client-side to server-side processing:

  • Visual State: I want to apply a specific CSS class (.dtcc-button_active) to the column button when a search is active. It works perfectly in client-side, but in server-side the class is not applied (it seems column().search() returns empty).

  • Clear Search: The Clear Search button in the dropdown remains disabled even after I apply a filter, so I cannot clear the search.

Here is a version of my configuration ( I don't know how to do it for server-side ) : https://live.datatables.net/zokukoxe/1/

To clarify my setup: I initialize the DataTable after fetching the filter lists via $.when.

Crucially, instead of relying solely on the standard DataTables parameters sent to the server, I inject a custom parameter to send the column configuration (including current filters) to my backend controller.

I suspect this custom data handling is why the standard DataTables API (column().search()) remains empty on the client side, causing the columnControl plugin to lose its active state.

Here is the logic (simplified and generic):

// 1. I load filter options (JSON) from the server first
$.when(
    loadFilterData("FilterTypeA"),
    loadFilterData("FilterTypeB")
).done(function (dataA, dataB) {

    // Process data into {label, value} objects for the searchLists
    let optionsA = parseAndMapData(dataA);
    let optionsB = parseAndMapData(dataB);
    
    // Global variable to store custom config
    let myCustomColumnConfig = [];

    // 2. Initialize DataTable
    oDataTable = $('#myTable').DataTable({
        processing: true,
        serverSide: true,
        
        // 3. The Ajax configuration
        ajax: {
            url: "my_api_endpoint_url", 
            type: 'POST',
            data: {
                action: "ListItems",
                // THIS IS KEY: I send column configs/filters via this custom param
                // instead of relying on standard DT parameters
                customParam_ColumnConfig: function () {
                    return JSON.stringify(myCustomColumnConfig);
                },
                customParam_GlobalSearch: function () {
                    return $("#MyGlobalSearchInput").val();
                }
            },
            dataFilter: function (data) {
                // Custom response handling
                let json = JSON.parse(data);
                return JSON.stringify(json.tableData); 
            }
        },
        
        // 4. Column Control Plugin Configuration
        columnControl: ['order', 'spacer', ['orderAsc', 'orderDesc', 'orderClear', 'spacer', 'search']],
        
        // 5. Column Definitions
        columnDefs: [
            {
                targets: [1], 
                name: "COLUMN_A", 
                // Specific searchList configuration using loaded options
                columnControl: ['order', 'spacer', ['orderAsc', 'orderDesc', 'orderClear', 'spacer',
                    {
                        extend: 'searchList',
                        options: optionsA 
                    }
                ]]
            },
            {
                targets: [2], 
                name: "COLUMN_B", 
                columnControl: ['order', 'spacer', ['orderAsc', 'orderDesc', 'orderClear', 'spacer',
                    {
                        extend: 'searchList',
                        options: optionsB 
                    }
                ]]
            },
            {
                targets: [3], 
                name: "COLUMN_C"
            },
            // ... other columns ...
        ],
        
        initComplete: function () {
            // I populate the custom config array here for the server
            this.api().columns().every(function () {
                var colConfig = this.init();
                myCustomColumnConfig.push({
                    key: colConfig.name, 
                    type: colConfig.data_type
                });
            });
        }
    });
});

How can I tell DataTables that a search is active so that the "Clear" button becomes enabled and CSS apply ?

Thanks for your help !

Answers

  • Just1nJust1n Posts: 5Questions: 2Answers: 0

    With this specific setup, since I am bypassing the standard DataTables filtering parameters to use my customParam_ColumnConfig, the columnControl plugin does not detect that a search is active.

    Consequently, the button does not get the .dtcc-button_active class automatically. That is why I am trying to manually force this class within the drawCallback by inspecting the server response.

    // ... (inside DataTable config) ...
    
            drawCallback: function (settings) {
                var api = this.api();
    
                // Retrieve the raw data sent to the server in the last Ajax request
                var ajaxData = settings.oAjaxData;
    
                api.columns().every(function (index) {
                    var header = $(this.header());
                    var btn = header.find('button.dtcc-button_dropdown');
                    var isActive = false;
    
                    // Check if the column is filtered based on the Ajax data
                    if (ajaxData && ajaxData.columns && ajaxData.columns[index]) {
                        var colData = ajaxData.columns[index];
    
                        // THE FIX: The plugin stores the search value in a specific 'columnControl' object
                        // instead of the standard location in this specific server-side setup.
                        if (colData.columnControl &&
                            colData.columnControl.search &&
                            colData.columnControl.search.value &&
                            colData.columnControl.search.value !== "") {
                            
                            isActive = true;
                        }
    
                        // Fallback: Check standard DataTables search parameter
                        if (!isActive && colData.search && colData.search.value !== "") {
                            isActive = true;
                        }
                    }
    
                    // Apply the active CSS class manually
                    if (isActive) {
                        btn.addClass('dtcc-button_active');
                    } else {
                        btn.removeClass('dtcc-button_active');
                    }
                });
            }
    
  • kthorngrenkthorngren Posts: 22,348Questions: 26Answers: 5,137

    I dont believe there is a server side processing environment that supports the extra ColumnControl parameters in the Datatables JS BIN environment. However you can find base server side processing templates in this technote.

    I haven't looked through your code to get a full uunderstanding of what you are doing but it sounds like you are trying to save and restore the ColumnControl state. Have you looked at using stateSave like this example?

    Kevin

  • Just1nJust1n Posts: 5Questions: 2Answers: 0

    Hi Kevin, thank you for the suggestion regarding stateSave.

    To clarify, my goal isn't persistence across page reloads, but simply ensuring the UI reflects the "active" state immediately after a server-side draw.

    I have made progress and identified the root causes for both the visual state and the disabled button.

    1. Visual "Active" State (SOLVED)

    I discovered that when using Server-Side Processing with custom parameters, the search value bypasses the standard DataTables API location. Instead, it is nested in settings.oAjaxData at columns[index].columnControl.search.value. Because the standard API sees an empty search, the plugin removes the active CSS class. Solution: I implemented a custom drawCallback to inspect this specific path and force the .dtcc-button_active class. This part now works.

    1. "Clear Search" Button (STILL BROKEN)

    Even though the column is filtered server-side, the "Clear Search" button in the dropdown remains disabled (dtcc-button_disabled).

    The Paradox: Inside the same dropdown, the small "X" icon (<span class="dtcc-search-clear">) works perfectly. Clicking it clears the input and reloads the table.

    The Root Cause (Missing Link found!): I compared the internal state between Client-side (working) and Server-side (broken) and found that the plugin relies on the searchFixed property using a dtcc key.

    • Client-Side (Working): When I filter a column, the internal model is updated correctly. oDataTable.settings()[0].aoColumns[index].searchFixed contains:
    Object { dtcc: "test" }
    

    Because this object is populated, the "Clear" button is enabled.

    • Server-Side (Broken): When I filter the same column, the request is sent successfully, BUT the internal model is NOT updated. oDataTable.settings()[0].aoColumns[index].searchFixed returns:
    Object { } // Empty
    

    Conclusion: In Server-side mode, the plugin fails to write the filter value into aoColumns[index].searchFixed. Since the "Clear" button logic checks this property, it remains disabled.

    My Question: Is there a way to force the plugin to populate searchFixed in Server-side mode? Or should I manually inject this object during the drawCallback to "trick" the plugin into enabling the button?

    Thanks!

  • allanallan Posts: 65,377Questions: 1Answers: 10,850 Site admin

    Hi,

    It is expected and designed so that ColumnControl will send search information to the server-side using the parameters you highlight. That is documented here, and searchFixed is not used for server-side processing with ColumnControl.

    I believe the issue you are seeing is fixed but this commit, which hasn't yet been released. If you could try the nightly build and let me know how you get on with it, that would be useful.

    Thanks,
    Allan

  • Just1nJust1n Posts: 5Questions: 2Answers: 0

    Hi Allan,

    I have tested the nightly build. It fixes the visual active state, but the "Clear Search" button remained disabled in my Server-Side setup.

    I used an AI to help me understand the execution flow, and it pointed out that the server-side check was returning too early.

    • The Cause: In the nightly build, the if ( dt.page.info().serverSide ) { ... return; } block is placed before the logic that updates column.search.fixed. Consequently, in Server-Side mode, the function returns early, and the internal search state is never updated. The "Clear" button sees an empty state and disables itself.

    • The Fix: Move the Server-Side check block to the very end of the .search() function, just before return searchInput.element();.

    Here is the fixed searchText implementation :

    var searchText = {
            defaults: {
                clear: true,
                placeholder: '',
                title: '',
                titleAttr: ''
            },
            init: function (config) {
                var _this = this;
                var dt = this.dt();
                var i18nBase = 'columnControl.search.text.';
                var searchInput = new SearchInput(dt, this.idx())
                    .addClass('dtcc-searchText')
                    .clearable(config.clear)
                    .placeholder(config.placeholder)
                    .title(config.title)
                    .titleAttr(config.titleAttr)
                    .options([
                        {label: dt.i18n(i18nBase + 'contains', 'Contains'), value: 'contains'},
                        {
                            label: dt.i18n(i18nBase + 'notContains', 'Does not contain'),
                            value: 'notContains'
                        },
                        {label: dt.i18n(i18nBase + 'equal', 'Equals'), value: 'equal'},
                        {label: dt.i18n(i18nBase + 'notEqual', 'Does not equal'), value: 'notEqual'},
                        {label: dt.i18n(i18nBase + 'starts', 'Starts'), value: 'starts'},
                        {label: dt.i18n(i18nBase + 'ends', 'Ends'), value: 'ends'},
                        {label: dt.i18n(i18nBase + 'empty', 'Empty'), value: 'empty'},
                        {label: dt.i18n(i18nBase + 'notEmpty', 'Not empty'), value: 'notEmpty'}
                    ])
                    .search(function (searchType, searchTerm, loadingState) {
                        // If in a dropdown, set the parent levels as active
                        if (config._parents) {
                            config._parents.forEach(function (btn) {
                                return btn.activeList(_this.unique(), searchType === 'empty' || searchType === 'notEmpty' || !!searchTerm);
                            });
                        }
    
                        // MOVED FROM HERE:
                    // The SSP check was here, preventing the logic below from running.
    
                        var column = dt.column(_this.idx());
                        searchTerm = searchTerm.toLowerCase();
                        if (searchType === 'empty') {
                            column.search.fixed('dtcc', function (haystack) {
                                return !haystack;
                            });
                        } else if (searchType === 'notEmpty') {
                            column.search.fixed('dtcc', function (haystack) {
                                return !!haystack;
                            });
                        } else if (column.search.fixed('dtcc') === '' && searchTerm === '') {
                            // No change - don't do anything
                            return;
                        } else if (searchTerm === '') {
                            // Clear search
                            column.search.fixed('dtcc', '');
                        } else if (searchType === 'equal') {
                            // Use a function for exact matching
                            column.search.fixed('dtcc', function (haystack) {
                                return haystack.toLowerCase() == searchTerm;
                            });
                        } else if (searchType === 'notEqual') {
                            column.search.fixed('dtcc', function (haystack) {
                                return haystack.toLowerCase() != searchTerm;
                            });
                        } else if (searchType === 'contains') {
                            // Use the built in smart search
                            column.search.fixed('dtcc', searchTerm);
                        } else if (searchType === 'notContains') {
                            // Use the built in smart search
                            column.search.fixed('dtcc', function (haystack) {
                                return !haystack.toLowerCase().includes(searchTerm);
                            });
                        } else if (searchType === 'starts') {
                            // Use a function for startsWith case insensitive search
                            column.search.fixed('dtcc', function (haystack) {
                                return haystack.toLowerCase().startsWith(searchTerm);
                            });
                        } else if (searchType === 'ends') {
                            column.search.fixed('dtcc', function (haystack) {
                                return haystack.toLowerCase().endsWith(searchTerm);
                            });
                        }
                        // If in a dropdown, set the parent levels as active
                        if (config._parents) {
                            config._parents.forEach(function (btn) {
                                return btn.activeList(_this.unique(), !!column.search.fixed('dtcc'));
                            });
                        }
    
                        // TO HERE:
                        // When SSP, don't apply a filter here, SearchInput will add to the submit data
                        if (dt.page.info().serverSide) {
                            if (!loadingState) {
                                dt.draw();
                            }
                            return;
                        }
    
                        if (!loadingState) {
                            column.draw();
                        }
                    });
    
                return searchInput.element();
            }
        };
    

    I do the same with searchDateTime and searchNumber.

    With this change, both the Active State and the Clear Button work perfectly in Server-Side Processing !

    Thanks Allan !

Sign In or Register to comment.