Search code examples
javascripthtmlhtml-table

html filter only one table on a page with multiple tables


I'm writing a simple html page containing multiple table

<table class="tosearch">
  <thead>
    <tr>
      <th>...</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>...</td>
    </tr>
    <tr>
      <td>...</td>
    </tr>
  </tbody>
</table>

Above each table I've placed an input text box

<input type="text" id="searchInput" placeholder="Search for data...">

to filter the table's data with the following Js script (credits to chatgpt)

// script.js
document.addEventListener("DOMContentLoaded", function () {
    const searchInput = document.getElementById("searchInput");

    // Get all tables in the document
    const tablesToSearch = document.querySelectorAll(".tosearch");

    // Event listener for search input
    searchInput.addEventListener("input", function () {
        const filter = searchInput.value.toLowerCase();

        // Filter all tables
        tablesToSearch.forEach(function (table) {
            filterTable(table, filter);
        });
    });

    function filterTable(table, filter) {
        const rows = table.getElementsByTagName("tr");

        for (let i = 1; i < rows.length; i++) {
            const row = rows[i];
            const cells = row.getElementsByTagName("td");
            let shouldShow = false;

            for (let j = 0; j < cells.length; j++) {
                const cell = cells[j];
                const text = cell.textContent.toLowerCase();

                if (text.includes(filter)) {
                    shouldShow = true;
                    break;
                }
            }

            row.style.display = shouldShow ? "" : "none";
        }
    }
});

Everything works nice, but if I would like to have multiple tables on the same page, each with a text input above it.
If I just copy and paste the html code for the text input and the table, when I try to search the script filter on all tables, while I would like to filter only on the associated table, something like

textInput for table1 only
table1

textInput  for table2 only
table2
.
.
.

I hope I explained myself good enough, otherwise I'll make some edits.


Solution

  • The easiest way was to make it a reusable function:

    function filterTable(table, filter) {
    
        const rows = table.getElementsByTagName("tr");
    
        for (let i = 1; i < rows.length; i++) {
            const row = rows[i];
            const cells = row.getElementsByTagName("td");
            let shouldShow = false;
    
            for (let j = 0; j < cells.length; j++) {
                const cell = cells[j];
                const text = cell.textContent.toLowerCase();
    
                if (text.includes(filter)) {
                    shouldShow = true;
                    break;
                }
            }
    
            row.style.display = shouldShow ? "" : "none";
        }
        
    }
    
    /* This method is called onload now, but is limited to arguments you have passed in.*/
    
    function linkInputToTablesSearch( searchInput, tablesToSearch ){
    
        searchInput.addEventListener("input", function () {
        
            const filter = searchInput.value.toLowerCase();
    
            tablesToSearch.forEach(function (table) {
                filterTable(table, filter);
            });
            
        });
        
    };
    
    
    document.addEventListener("DOMContentLoaded", function () {
    
      document.querySelectorAll( 'input' ).forEach(input => {
          
        linkInputToTablesSearch( input, [ input.nextElementSibling ] );
        
      });
    
    });
    <input type="text" placeholder="Search for data...">
    <table>
      <tbody>
        <tr>
          <td>a1</td>
          <td>a2</td>
          <td>a3</td>
          <td>a4</td>
          <td>a5</td>
        </tr>
        <tr>
          <td>b1</td>
          <td>b2</td>
          <td>b3</td>
          <td>b4</td>
          <td>b5</td>
        </tr>
      </tbody>
    </table>

    You can now link the table and the input by calling linkInputToTablesSearch with the input and an array of tables as the arguments.

    But your filtering has a couple of issues, the break is stopping it and you will never be able to hide rows after a match. I have a more simplified version of the filter here:

    function filterTable( table, query ){
    
      const regex = query && new RegExp( query, 'gi' );
        
      [...table.querySelectorAll( 'tr' )].forEach(row => {
        
        // Since you are filtering the rows, maybe skip the step of checking every cell individually. Although this would allow it to bleed (so if cell 1 contains a, and cell 2 contains b, the row contains the string 'ab'). If you want to avoid that, just filter through the cells here with the same regex.
        row.hidden = !!query && !regex.test( row.textContent );
          
      });
      
    }
    function linkInputToTables( input, tables ){
      
      // Ensure you have an array of tables. With this, you could also just pass in a single table selected with getElementById or querySelector
      tables = tables instanceof HTMLTableElement ? [ tables ] : tables;
      
      input.addEventListener( 'input', event => {
        
        tables.forEach(table => filterTable( table, input.value ));
        
      });
      
    }
    
    document.querySelectorAll( 'label' ).forEach(label => {
      
      const input = label.querySelector( 'input' );
      const target = document.querySelector( label.dataset.target );
      
      if( input && target ) linkInputToTables( input, target );
      
    })
    #table1 { border: 3px solid red }
    #table2 { border: 3px solid blue }
    <table id="table1">
      <tr>
        <td>a1</td>
        <td>a2</td>
        <td>a3</td>
        <td>a4</td>
      </tr>
      <tr>
        <td>b1</td>
        <td>b2</td>
        <td>b3</td>
        <td>b4</td>
      </tr>
    </table>
    <table id="table2">
      <tr>
        <td>a1</td>
        <td>a2</td>
        <td>a3</td>
        <td>a4</td>
      </tr>
      <tr>
        <td>b1</td>
        <td>b2</td>
        <td>b3</td>
        <td>b4</td>
      </tr>
    </table>
    
    <label data-target="#table1">
      <span>Just table 1</span>
      <input type="search" value="" id= />
    </label>
    <label data-target="#table2">
      <span>Just table 2</span>
      <input type="search" value="" id= />
    </label>