0

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/

2 Answers 2

2

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>

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

8 Comments

I tried running the code on my local MAMP server using the brackets editor and for some reason the javascript is not working, would you have any idea as to why?
@mias what does the server have to do with the browser (client code)?
nevermind the error was my fault, it's working fine now. Thanks for the help!!
is there a way to move the select dropdowns into the table headers and still have the javascript working? I can see from the way it is the select has to be outside the table.
@mias Add a new <tr> to the <thead> and place each select within a <th> to align with the associated <th> in the initial header row.
|
0

I do have the same issue and I adopted the solution provided by @Mr. Polywhirl. However, it does not work in my case and I cannot find the problem. My html is generated dynamically (not static) and I call the javscript as a function into the html. The table is nested into a specific div shown here:

screenshot of html code table header

screenshot of htmo code table body

As often as I try to filter the class .hidden-row is not added to the table rows. Basically nothing happens. And I simply don't understand why. Perhaps there is someone around who can help me? Thank you in advance.

function filterTable() {
    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));
}

2 Comments

SOLVED. I adopted the solution published by @Bryan Elliott, stackoverflow.com/a/60326655/4869421.
This does not provide an answer to the question. Once you have sufficient reputation you will be able to comment on any post; instead, provide answers that don't require clarification from the asker. - From Review

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.