Simplifying input filtering in tables with multiple selectors using jQuery

Code explained

I wrote a few identical functions for each table to filter them based on the user input typed in the input fields using jQuery. After filtering it counts the rows that are left visible.

My code is a bit flawed because I have one function for each selector or class, which is messy and can definitely be improved but after countless hours of investigating I'm exhausted.

I currently got a frontpage with overview of all the tables including the input fields; here's an image for of the relevant DOM structure for the frontpage.

What I'd like to achieve

A more efficient use of DOM classes/selectors and simplify the use of my input functions, taking full advantage of switching and identifying which input selectors that is in use and yet filter out the right individual table.

Code I've tried to improve

jQuery:

Each function has it's comment explaining where and what it does.

//jQuery search filtering
$(document).ready(function(){ //count each table BEFORE user input var rowCount_master =$('.table_data tr:visible:not(:has(th))').length;
$('#row_count_master').html(rowCount_master); var rowCount_utlan =$('#table_data_utlan tr:visible:not(:has(th))').length;
$('#row_count_utlan').html(rowCount_utlan); var rowCount_totaloversikt =$('#table_data_totaloversikt tr:visible:not(:has(th))').length;
$('#row_count_totaloversikt').html(rowCount_totaloversikt); var rowCount_midlansatt =$('#table_data_midlansatt tr:visible:not(:has(th))').length;
$('#row_count_midlansatt').html(rowCount_midlansatt); var rowCount_table =$('.table_data tr:visible:not(:has(th))').length;
$('#row_count').html(rowCount_table); //frontpage only - master table search, search filter in ALL tables$("#table_search_master").on("keyup", function() {
var value_master = $(this).val().toLowerCase();$(".table_data tr:not(:has(th))").filter(function() {
$(this).toggle($(this).text().toLowerCase().indexOf(value_master) > -1)
});

//counts number of table rows
var rowCount_master = $('.table_data tr:visible:not(:has(th))').length;$('#row_count_master').html(rowCount_master);
if (rowCount_master == 0) {
$('#no_result_master').css('display', 'block'); } else {$('#no_result_master').css('display', 'none');
}
});

//frontpage only - only utlan table search filter
$("#table_search_utlan").on("keyup", function() { var value_utlan =$(this).val().toLowerCase();
$("#table_data_utlan tr:not(:has(th))").filter(function() {$(this).toggle($(this).text().toLowerCase().indexOf(value_utlan) > -1) }); var rowCount_utlan =$('#table_data_utlan tr:visible:not(:has(th))').length;
$('#row_count_utlan').html(rowCount_utlan); if (rowCount_utlan == 0) {$('#no_result_utlan').css('display', 'block');
} else {
$('#no_result_utlan').css('display', 'none'); } }); //frontpage only - only totaloversikt table search filter$("#table_search_totaloversikt").on("keyup", function() {
var value_totaloversikt = $(this).val().toLowerCase();$("#table_data_totaloversikt tr:not(:has(th))").filter(function() {
$(this).toggle($(this).text().toLowerCase().indexOf(value_totaloversikt) > -1)
});

var rowCount_totaloversikt = $('#table_data_totaloversikt tr:visible:not(:has(th))').length;$('#row_count_totaloversikt').html(rowCount_totaloversikt);
if (rowCount_totaloversikt == 0) {
$('#no_result_totaloversikt').css('display', 'block'); } else {$('#no_result_totaloversikt').css('display', 'none');
}
});

//frontpage only - only midlansatt table search filter
$("#table_search_midlansatt").on("keyup", function() { var value_midlansatt =$(this).val().toLowerCase();
$("#table_data_midlansatt tr:not(:has(th))").filter(function() {$(this).toggle($(this).text().toLowerCase().indexOf(value_midlansatt) > -1) }); var rowCount_midlansatt =$('#table_data_midlansatt tr:visible:not(:has(th))').length;
$('#row_count_midlansatt').html(rowCount_midlansatt); if (rowCount_midlansatt == 0) {$('#no_result_midlansatt').css('display', 'block');
} else {
$('#no_result_midlansatt').css('display', 'none'); } }); //not frontpage - common search filter for each individual site with a table$("#table_search").on("keyup", function() {
var value_table = $(this).val().toLowerCase();$(".table_data tr:not(:has(th))").filter(function() {
$(this).toggle($(this).text().toLowerCase().indexOf(value_table) > -1)
});

var rowCount_table = $('.table_data tr:visible:not(:has(th))').length;$('#row_count').html(rowCount_table);
if (rowCount_table == 0) {
$('#no_result').css('display', 'block'); } else {$('#no_result').css('display', 'none');
}
});
});


HTML DOM:

<div class="table_hits" style="display: none">Antall treff: <b id="row_count_master">...</b></div>
<input type="input" id="table_search_master" class="form-control" placeholder="Søk i hele databasen..." style="">

<div class="table_hits" style="display: none">Antall treff: <b id="row_count_utlan">...</b></div>
<input type="input" id="table_search_utlan" class="form-control" placeholder="Søk i utlånslisten...">

<div class="table_scroll_frontpage">
<table class="table_data" id="table_data_utlan">
<!-- table data -->
</table>
</div>

<!-- same DOM for the rest of the tables -->


What I've tried

I've tried to merge every single selector into one filter function but naturally, that gave me issues where the input of the field would search in all tables at once and not get separated from each other.

Every comment/answer that gives me a bit more knowledge of how to make this mess, neat and pretty is overly-appreciated! I'm highly motivated in learning more about on how to take full advantage of jQuery's nice features and improve my skills. Thanks in advance.

I think we need to start with a list of suffixes that can be put somewhere accessible(preferably as a property to a class, but global space works for now)

const suffixes = [
'master',
'utlan',
'totaloversikt'
];


From there we can change our event listener to make a note of the suffix and register a generic function for the event.

suffixes.foreach(suffix => {
const isAllTableSearch = suffix === 'master';
// Master has different behaviour
const filterExpression = isAllTableSearch
? '.table_data  tr:visible:not(:has(th))'
: '#table_data_' + suffix + ' tr:visible:not(:has(th))';

$("#table_search_" + suffix) .data('suffix', suffix) .data('filter', filterExpression) .data('is-all-table-search', isAllTableSearch) .on("input", doFiltering); // Input triggers whenever it changes including things like cut/paste rather than keyup which only triggers when a key is released });  Your doFiltering method can then use the search box to derive the suffix and filter to make the method more generic function doFiltering(event) { const searchBox =$(event.target);
const searchQuery = searchBox.val().toLowerCase();
$(searchBox.data('filter')).each((index, element) => {$(element).toggle($(element).text().toLowerCase().indexOf(searchQuery) > -1) }); // Update totals if (searchBox.data('is-all-table-search')) { updateAllTotals(); } else { updateTotalForSuffix(searchBox.data('suffix')); } }  While we're talking adding methods for stuff I'd also suggest adding a method to recalculate the totals, this is especially important for the master which can hide rows in other tables but doesn't update their totals. function updateTotalForSuffix(suffix) { const tableSearch =$("#table_search_" + suffix);
rowCount = $(tableSearch.data('filter')).length;$('#row_count_' + suffix).text(rowCount);
$('#no_result_' + suffix).toggle(rowCount === 0); } function updateAllTotals() { suffixes.foreach(suffix => updateTotalForSuffix(suffix)); }  This gives you a handy function to call on init and whenever the master search is called to avoid the info falling out of sync. This gives us a few advantages. We're not maintaining logic for multiple copies of the search functionality, we've got one set of methods that take an object and use it's properties to determine the behaviour rather than having changes to each bit of logic for changes in behaviour. Where possible I've tried to avoid $(this) because it can cause problems once you start abstracting stuff away into further functions(so say you wanted updating the totals to be a different method to your search but wanted a separate function for handling calling them both, using \$(this) means to do so you now need to either change the methods or rebind the function, rather than just passing in a relevant argument like event.target).

• Amazing approach, I absolutely love it. I had a real fun time tweaking and learning from your code. I've made some changes worth noting: In const filterExpression i changed tr:visible:not(:has(th)) to tr:not(:has(th)) otherwise it would only filter out visible rows and not invisible ones (not reappearing). I also added an ìf query to the function updateTotalForSuffix so I can check whether or not its counting the individual table or all of the tables. As cleverly mentioned by you, I added a call to updateAllTotals` at page load. – Sanguinary Mar 4 at 13:17