javascripthtmljqueryfilterhtml-table

filtering html table columns with select dropdowns


I'm trying to make a table that can filter each column based on the values in it. I want each dropdown to be associated only with the column so like the first dropdown is only looking at the first column. I found javascript code that works perfectly but only when each value in the is unique. My table will be mostly just yes/no in every row/column and I think the reason the code I found doesn't work the way I want it is because the values aren't unique. The first column of OK/NO works but there's issues when looking at the other columns when specifying yes/no. If I select OK, no, yes from the 3 dropdowns it's supposed to only show one row which is the first but it also shows the third row. the first row is OK, no, yes and the third row is OK, no, no. So the problem is that in the third column of the third row it's no but it still appears after specifying yes from the third dropdown. Is there any way to get the dropdown to only look at a specific column? I'm still a beginner to javascript so I was just hoping to see if anyone could help while I still look into it in other sources.

here is the html

<select class="filter">
    <option value="">None</option>
    <option value="a">OK</option>
    <option value="b">NO</option>
</select>
<select class="filter">
    <option value="">None</option>
    <option value="1">no</option>
    <option value="2">yes</option>
    
</select>
<select class="filter">
    <option value="">None</option>
    <option value="3">no</option>
    <option value="4">yes</option>
    
</select>
<table>
    <tbody>
    <tr>
      <th>Status</th>
      <th>Cough</th>
      <th>Fever</th>
    </tr>
        <tr>
            <td>OK</td>
            <td>no</td>
            <td>yes</td>
        </tr>
        <tr>
            <td>NO</td>
            <td>yes</td>
            <td>yes</td>
        </tr>
        <tr>
            <td>OK</td>
            <td>no</td>
            <td>no</td>
        </tr>
        <tr>
            <td>NO</td>
            <td>yes</td>
            <td>no</td>
        </tr>
    </tbody>
</table>

and this is the javascript code I found online

$(document).ready(function () {

    $('.filter').change(function () {
        var values = [];
        $('.filter option:selected').each(function () {
            if ($(this).val() != "") values.push($(this).text());
        });
        filter('table > tbody > tr', values);
    });

    function filter(selector, values) {
        $(selector).each(function () {
            var sel = $(this);
            var tokens = sel.text().split('\n');
            var toknesObj = [], i;
            for(i=0;i<tokens.length;i++){
                toknesObj.push( {text:tokens[i].trim(), found:false});
            }

            var show = false;
            console.log(values);
            $.each(values, function (i, val) {
                
                for(i=0;i<toknesObj.length;i++){                    
                    if (!toknesObj[i].found && toknesObj[i].text.search(new RegExp("\\b"+val+"\\b")) >= 0) {
                        toknesObj[i].found = true;
                    }
                }
            });          
            
            var count = 0;
             $.each(toknesObj, function (i, val) {
                 if (val.found){
                     count+=1;
                 }
             });
            show = (count === values.length);        
            show ? sel.show() : sel.hide();
        });
    }
});

here is the link to the original javascript code http://jsfiddle.net/lukaszewczak/2dhE5/52/


Solution

  • You can filter the table data by introducing a few things:

    1. A filter state to keep track of the filter choices
    2. Data attributes for each filter to identify them
    3. Make sure the value of the filter matches the text of the data exactly

    const
      table = document.querySelector('.filter-table'),
      filterState = {};
    
    const dataFromRow = (row, headers) =>
      Object.fromEntries([...row.querySelectorAll('td')]
        .map((td, index) => [headers[index], td.textContent]));
    
    const matchesCriteria = (rowData, filters) =>
      filters.every(([key, value]) => rowData[key] === value);
    
    const refresh = () => {
      const
        headers = [...table.querySelectorAll('thead th')].map(th => th.textContent),
        filters = Object.entries(filterState),
        showAll = filters.length === 0;
      table.querySelectorAll('tbody tr').forEach(row => {
        const show = showAll || matchesCriteria(dataFromRow(row, headers), filters);
        row.classList.toggle('hidden-row', !show);
      });
    };
    
    const handleFilterChange = (e) => {
      const
        field = e.target.dataset.field,
        value = e.target.value;
      if (value) { filterState[field] = value; }
      else { delete filterState[field]; }
      refresh();
    };
    
    document.querySelectorAll('.filter').forEach(filter =>
      filter.addEventListener('change', handleFilterChange));
    .filter-table {
      border-collapse: collapse;
    }
    
    .filter-table {
      border: thin solid grey;
    }
    
    .filter-table thead {
      border-bottom: thin solid grey;
    }
    
    .filter-table th, .filter-table td {
      padding: 0.25em 0.5em;
    }
    
    .filter-table th {
      background: #CCC;
    }
    
    .filter-table tbody tr:nth-child(even) {
      background: #EEE;
    }
    
    .hidden-row {
      display: none;
    }
    <select class="filter" data-field="Status">
      <option value="">None</option>
      <option value="OK">OK</option>
      <option value="NO">NO</option>
    </select>
    <select class="filter" data-field="Cough">
      <option value="">None</option>
      <option value="No">No</option>
      <option value="Yes">Yes</option>
    </select>
    <select class="filter" data-field="Fever">
      <option value="">None</option>
      <option value="No">No</option>
      <option value="Yes">Yes</option>
    </select>
    <hr />
    <table class="filter-table">
      <thead>
        <tr><th>Status</th><th>Cough</th><th>Fever</th></tr>
      </thead>
      <tbody>
        <tr><td>OK</td><td>No</td><td>Yes</td></tr>
        <tr><td>NO</td><td>Yes</td><td>Yes</td></tr>
        <tr><td>OK</td><td>No</td><td>No</td></tr>
        <tr><td>NO</td><td>Yes</td><td>No</td></tr>
      </tbody>
    </table>


    If you want the filters to be in the headers, you can move the <select> elements into <th> inside of another <tr> inside of the <thead>.

    const
      table = document.querySelector('.filter-table'),
      filterState = {};
    
    const dataFromRow = (row, headers) =>
      Object.fromEntries([...row.querySelectorAll('td')]
        .map((td, index) => [headers[index], td.textContent]));
    
    const matchesCriteria = (rowData, filters) =>
      filters.every(([key, value]) => rowData[key] === value);
    
    const refresh = () => {
      const
        headers = [...table.querySelectorAll('thead th')].map(th => th.textContent),
        filters = Object.entries(filterState),
        showAll = filters.length === 0;
      table.querySelectorAll('tbody tr').forEach(row => {
        const show = showAll || matchesCriteria(dataFromRow(row, headers), filters);
        row.classList.toggle('hidden-row', !show);
      });
    };
    
    const handleFilterChange = (e) => {
      const
        field = e.target.dataset.field,
        value = e.target.value;
      if (value) {
        filterState[field] = value;
      } else {
        delete filterState[field];
      }
      refresh();
    };
    
    document.querySelectorAll('.filter').forEach(filter =>
      filter.addEventListener('change', handleFilterChange));
    .filter-table {
      border-collapse: collapse;
    }
    
    .filter-table {
      border: thin solid grey;
    }
    
    .filter-table thead {
      border-bottom: thin solid grey;
    }
    
    .filter-table th,
    .filter-table td {
      padding: 0.25em 0.5em;
    }
    
    .filter-table th {
      background: #CCC;
    }
    
    .filter-table tbody tr:nth-child(even) {
      background: #EEE;
    }
    
    .hidden-row {
      display: none;
    }
    <table class="filter-table">
      <thead>
        <tr>
          <th>Status</th>
          <th>Cough</th>
          <th>Fever</th>
        </tr>
        <tr>
          <th>
            <select class="filter" data-field="Status">
              <option value="">None</option>
              <option value="OK">OK</option>
              <option value="NO">NO</option>
            </select>
          </th>
          <th>
            <select class="filter" data-field="Cough">
              <option value="">None</option>
              <option value="No">No</option>
              <option value="Yes">Yes</option>
            </select>
          </th>
          <th>
            <select class="filter" data-field="Fever">
              <option value="">None</option>
              <option value="No">No</option>
              <option value="Yes">Yes</option>
            </select>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>OK</td>
          <td>No</td>
          <td>Yes</td>
        </tr>
        <tr>
          <td>NO</td>
          <td>Yes</td>
          <td>Yes</td>
        </tr>
        <tr>
          <td>OK</td>
          <td>No</td>
          <td>No</td>
        </tr>
        <tr>
          <td>NO</td>
          <td>Yes</td>
          <td>No</td>
        </tr>
      </tbody>
    </table>