1

I am using a data-table from Framework7 that is dynamically built with JSON data and I am trying to make the column filter input functional. Using other examples shown on Stack Overflow, I have been able to get the first column filtering successfully:

HTML

<input type="text" id="brandFilter" onkeyup="filterTable()" placeholder="Filter">

JS

function filterTable() {  
  var input, filter, table, tr, td, i, txtValue;
  input = document.getElementById("brandFilter");
  filter = input.value.toUpperCase();
  table = document.getElementById("productList");
  tr = table.getElementsByTagName("tr");  
  for (i = 0; i < tr.length; i++) {
    td = tr[i].getElementsByTagName("td")[0];
    if (td) {
      txtValue = td.textContent || td.innerText;
      if (txtValue.toUpperCase().indexOf(filter) > -1) {
        tr[i].style.display = "";
      } else {
        tr[i].style.display = "none";
      }
    }
  }
}

enter image description here

I can't figure out how to improve this script so that the table can be further filtered using the inputs on column 2 and 3?

<input type="text" id="modelFilter" onkeyup="filterTable()" placeholder="Filter">
<input type="text" id="energyFilter" onkeyup="filterTable()" placeholder="Filter">

Any simple solutions as I am still a newbie?

JohnGIS
  • 423
  • 6
  • 18

3 Answers3

1

You could build a list of terms based on their column position and store the type of input and the value.

You could then loop over every row and determine if you want to display it, based on the text content of each cell ant its corresponding term.

function onFilterChange(e) {
  let table = findParentBySelector(e.target, 'table');
  var terms = [].slice.call(table.querySelectorAll('.input')).map(el => {
    let item = el.querySelector('input, select');
    return { type : item.tagName, value : item.value };
  });
  table.querySelectorAll('tbody tr').forEach(tr => {
    let visible = [].slice.call(tr.querySelectorAll('td')).every((td, i) => {
      if (terms[i].type === 'SELECT' && terms[i].value === 'All') return true;
      let text = td.innerHTML.trim();
      return text != '' ? text.indexOf(terms[i].value) > -1 : true;
    });
    tr.classList[visible ? 'remove' : 'add']('hidden-row');
  });
}

Demo

var data = `2,Jane Doe,jane.doe@com,Female
3,Vladimir Kharlampidi,vladimir@google.com,Male
4,Jennifer Doe,jennifer@doe.com,Female`.split('\n');

let tbody = document.querySelector('.data-table table tbody')
data.forEach(r => {
  let tr = document.createElement('TR');
  r.split(/,\s*?/g).forEach(c => {
    let td = document.createElement('TD');
    td.innerHTML = c;
    tr.appendChild(td);
  });
  tbody.appendChild(tr);
});

document.querySelectorAll('.input').forEach(el => {
  el.querySelectorAll('input').forEach(x => x.addEventListener('keyup', onFilterChange));
  el.querySelectorAll('*').forEach(x => x.addEventListener('change', onFilterChange));
});

function onFilterChange(e) {
  let table = findParentBySelector(e.target, 'table'),
    caseInsensitive = table.getAttribute('data-ignore-case') === 'true',
    terms = [].slice.call(table.querySelectorAll('.input')).map(el => {
      let item = el.querySelector('input, select');
      return {
        type : item.tagName,
        value : (v => caseInsensitive ? v.toLowerCase() : v)(item.value)
      };
    });
  table.querySelectorAll('tbody tr').forEach(tr => {
    let visible = [].slice.call(tr.querySelectorAll('td')).every((td, i) => {
      let text = (t => caseInsensitive ? t.toLowerCase() : t)(td.textContent.trim());
      if (terms[i].type === 'SELECT') {
       if (terms[i].value.toUpperCase() === 'ALL') return true;
       return terms[i].value === text;
      }
      return text != '' ? text.indexOf(terms[i].value) > -1 : true;
    });
    tr.classList.toggle('hidden-row', !visible);
  });
}

// https://stackoverflow.com/a/14234618/1762224
// Note: Modified as a recursive routine
function findParentBySelector(el, target) {
  return el != null ? el && ![].slice.call(typeof target === 'string' ? (target = document.querySelectorAll(target)) : target).some(c => c == el) ? findParentBySelector(el.parentNode, target) : el : null;
}
body {
  background: #444 !important;
}

.data-table {
  width: 96% !important;
  margin: 1em auto !important;
  padding: 0.5em;
}

/** Toggled */
.hidden-row {
  display: none;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/framework7/4.5.0/css/framework7.bundle.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/framework7/4.5.0/js/framework7.bundle.min.js"></script>
<div class="card data-table">
  <table data-ignore-case="true">
    <thead>
      <tr>
        <th class="input-cell">
          <span class="table-head-label">ID</span>
          <div class="input" style="width: 50px">
            <input type="number" placeholder="Filter">
          </div>
        </th>
        <th class="input-cell">
          <span class="table-head-label">Name</span>
          <div class="input">
            <input type="text" placeholder="Filter">
          </div>
        </th>
        <th class="input-cell">
          <span class="table-head-label">Email</span>
          <div class="input">
            <input type="text" placeholder="Filter">
          </div>
        </th>
        <th class="input-cell">
          <span class="table-head-label">Gender</span>
          <div class="input input-dropdown">
            <select>
              <option value="All">All</option>
              <option value="Male">Male</option>
              <option value="Female">Female</option>
            </select>
          </div>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>1</td>
        <td>John Doe</td>
        <td>john@doe.com</td>
        <td>Male</td>
      </tr>
    </tbody>
  </table>
</div>

Original response

  1. Add a class to your search fields with a data attribute:

    $('<input>').addClass('search-field').data('field', field))
    
  2. Add a field name to your table cells.

    $('<td>').text(rec[field]).data('field-name', field)
    
  3. Implement a search map with the terms and filter each row if all the criteria meets the text content of every cell. If the term for the column is blank, assume true.

    var $table = $.tableFromJson(data).addClass('stylized').appendTo('body');
    $table.on('keyup', '.search-field', function(e) {
      // Map fields to pairs and then reduce into a map.
      let terms = $('.search-field').map(function(i, field) {
        return { field : $(field).data('field'), value : $(field).val().trim() };
      }).toArray().reduce(function(terms, pair) {
        return Object.assign(terms, { [pair.field] : pair.value });
      }, {});
    
      let len = Object.keys(terms).length; // Store the length of the keys
    
      // Find the parent (table) of the search field that you modified and locate the rows
      $(e.target).closest('table').find('tbody tr').each(function(i, tr) {
        // Toggle the row visibility
        $(tr).toggle($(tr).find('td').filter(function(j, td) {
          let term =  terms[$(td).data('field-name')];
    
          // If not empty, locate the term within the text of the cell
          return term != '' ? $(td).text().trim().indexOf(term) > -1 : true;
        }).length === len);
      });
    });
    

Demo

(function($) {
  $.reduce = function(arr, fnReduce, valueInitial) {
    if (Array.prototype.reduce) {
      return Array.prototype.reduce.call(arr, fnReduce, valueInitial);
    }
    $.each(arr, function(i, value) {
      valueInitial = fnReduce.call(null, valueInitial, value, i, arr);
    });
    return valueInitial;
  };
  $.fn.reduce = function(fnReduce, valueInitial) {
    return $.reduce(this, fnReduce, valueInitial);
  };
  $.fn.renderTable = function(data, options) {
    options = options || {};
    let ignoreCase = options.ignoreCase, fields = Object.keys(data[0]);
    return this.renderHeaders(fields).renderRows(fields, data)
      .on('keyup', '.search-field', function(e) {
        let terms = $('.search-field').reduce(function(m, field) {
            return Object.assign(m, {
              [$(field).data('field')]: (function(val) {
                return ignoreCase && val ? val.toLowerCase() : val;
              })($(field).val().trim())
            });
          }, {}), len = Object.keys(terms).length;
        $(e.target).closest('table').find('tbody tr').each(function(i, tr) {
          $(tr).toggle($(tr).find('td').filter(function(j, td) {
            let term = terms[$(td).data('field-name')];
            return term != '' ? (function(text) {
              return ignoreCase && text ? text.toLowerCase() : text;
            })($(td).text().trim()).indexOf(term) > -1 : true;
          }).length === len);
        });
      });
  };
  $.fn.renderHeaders = function(fields) {
    return this.append($.renderHeaders(fields));
  }
  $.fn.renderRows = function(fields, data) {
    return this.append($.renderRows(fields, data));
  };
  $.tableFromJson = function(data, options) {
    return $('<table>').renderTable(data, options)
      .toggleClass('stylized', (options || {}).stylized);
  };
  $.renderHeaders = function(fields) {
    return $('<thead>').append($('<tr>').append(fields
      .map(field => $('<th>')
        .append($('<div>').text(field))
        .append($('<input>').addClass('search-field').data('field', field)))));
  };
  $.renderRows = function(fields, data) {
    return $('<tbody>').append(data
      .map((rec, row) => $('<tr>').append(fields
        .map((field, col) => $('<td>').text(rec[field]).data('field-name', field)))));
  };
})(jQuery);

var data = [
  { "brand" : "Bosch", "model" : 333, "energyUse" : "A"   },
  { "brand" : "Bosch", "model" : 456, "energyUse" : "A++" }
];

$.tableFromJson(data, { ignoreCase: true, stylized: true }).appendTo('body');
body {
  padding: 0.25em;
}

h1 {
  font-weight: bold;
  margin-top: 0.75em;
  margin-bottom: 0.33em;
}

table.stylized {
  font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
  font-size: 12px;
  text-align: left;
  border-collapse: collapse;
  margin: 4px;
  width: 600px;
}

table.stylized thead th {
  text-transform: capitalize;
  font-size: 13px;
  color: #039;
  background: #b9c9fe;
  padding: 6px;
  cursor: pointer;
}

table.stylized thead th input {
  background: #f2f5ff;
  color: #039;
  font-size: smaller;
}

table.stylized tbody tr:nth-child(odd) {
  background: #f2f5ff;
}

table.stylized tbody tr:nth-child(even) {
  background: #e8edff;
}

table.stylized tbody td {
  border-top: 1px solid #fff;
  color: #669;
  padding: 6px;
}

table.stylized tbody tr:hover td {
  background: #d0dafd;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

Note: The table generation jQuery plugin in this demo is based on a sortable table plugin I previously implemented.

Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
0

If you want to change the filter to each column just change this:

function filterTable(column) {  // add a parameter
  var input, filter, table, tr, td, i, txtValue;
  input = document.getElementById("brandFilter");
  filter = input.value.toUpperCase();
  table = document.getElementById("productList");
  tr = table.getElementsByTagName("tr");  
  for (i = 0; i < tr.length; i++) {
  td = tr[i].getElementsByTagName("td")[0];// change this
  td = tr[i].getElementsByTagName("td")[column];// to this
    if (td) {
      txtValue = td.textContent || td.innerText;
      if (txtValue.toUpperCase().indexOf(filter) > -1) {
        tr[i].style.display = "";
      } else {
        tr[i].style.display = "none";
      }
    }
  }
}

That way you pass the column index as value to filter per column

Hope it helps

stan chacon
  • 768
  • 1
  • 6
  • 19
0

A small correction to @Mr. Polywhirl answer In his answer if data from for example Name column is all empty the script will not work because all data is empty
Example:

var data = `2,,jane.doe@com,Female
3,,vladimir@google.com,Male
4,,jennifer@doe.com,Female`.split('\n');

let tbody = document.querySelector('.data-table table tbody')
data.forEach(r => {
  let tr = document.createElement('TR');
  r.split(/,\s*?/g).forEach(c => {
    let td = document.createElement('TD');
    td.innerHTML = c;
    tr.appendChild(td);
  });
  tbody.appendChild(tr);
});

document.querySelectorAll('.input').forEach(el => {
  el.querySelectorAll('input').forEach(x => x.addEventListener('keyup', onFilterChange));
  el.querySelectorAll('*').forEach(x => x.addEventListener('change', onFilterChange));
});

function onFilterChange(e) {
  let table = findParentBySelector(e.target, 'table'),
    caseInsensitive = table.getAttribute('data-ignore-case') === 'true',
    terms = [].slice.call(table.querySelectorAll('.input')).map(el => {
      let item = el.querySelector('input, select');
      return {
        type : item.tagName,
        value : (v => caseInsensitive ? v.toLowerCase() : v)(item.value)
      };
    });
  table.querySelectorAll('tbody tr').forEach(tr => {
    let visible = [].slice.call(tr.querySelectorAll('td')).every((td, i) => {
      let text = (t => caseInsensitive ? t.toLowerCase() : t)(td.textContent.trim());
      if (terms[i].type === 'SELECT') {
       if (terms[i].value.toUpperCase() === 'ALL') return true;
       return terms[i].value === text;
      }
      return text != '' ? text.indexOf(terms[i].value) > -1 : true;
    });
    tr.classList.toggle('hidden-row', !visible);
  });
}

// https://stackoverflow.com/a/14234618/1762224
// Note: Modified as a recursive routine
function findParentBySelector(el, target) {
  return el != null ? el && ![].slice.call(typeof target === 'string' ? (target = document.querySelectorAll(target)) : target).some(c => c == el) ? findParentBySelector(el.parentNode, target) : el : null;
}
body {
  background: #444 !important;
}

.data-table {
  width: 96% !important;
  margin: 1em auto !important;
  padding: 0.5em;
}

/** Toggled */
.hidden-row {
  display: none;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/framework7/4.5.0/css/framework7.bundle.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/framework7/4.5.0/js/framework7.bundle.min.js"></script>
<div class="card data-table">
  <table data-ignore-case="true">
    <thead>
      <tr>
        <th class="input-cell">
          <span class="table-head-label">ID</span>
          <div class="input" style="width: 50px">
            <input type="number" placeholder="Filter">
          </div>
        </th>
        <th class="input-cell">
          <span class="table-head-label">Name</span>
          <div class="input">
            <input type="text" placeholder="Filter">
          </div>
        </th>
        <th class="input-cell">
          <span class="table-head-label">Email</span>
          <div class="input">
            <input type="text" placeholder="Filter">
          </div>
        </th>
        <th class="input-cell">
          <span class="table-head-label">Gender</span>
          <div class="input input-dropdown">
            <select>
              <option value="All">All</option>
              <option value="Male">Male</option>
              <option value="Female">Female</option>
            </select>
          </div>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>1</td>
        <td></td>
        <td>john@doe.com</td>
        <td>Male</td>
      </tr>
    </tbody>
  </table>
</div>

Instead change this line return text != '' ? text.indexOf(terms[i].value) > -1 : true; to this return text.indexOf(terms[i].value) > -1; will do the trick.
Example:

var data = `2,,jane.doe@com,Female
3,,vladimir@google.com,Male
4,,jennifer@doe.com,Female`.split('\n');

let tbody = document.querySelector('.data-table table tbody')
data.forEach(r => {
  let tr = document.createElement('TR');
  r.split(/,\s*?/g).forEach(c => {
    let td = document.createElement('TD');
    td.innerHTML = c;
    tr.appendChild(td);
  });
  tbody.appendChild(tr);
});

document.querySelectorAll('.input').forEach(el => {
  el.querySelectorAll('input').forEach(x => x.addEventListener('keyup', onFilterChange));
  el.querySelectorAll('*').forEach(x => x.addEventListener('change', onFilterChange));
});

function onFilterChange(e) {
  let table = findParentBySelector(e.target, 'table'),
    caseInsensitive = table.getAttribute('data-ignore-case') === 'true',
    terms = [].slice.call(table.querySelectorAll('.input')).map(el => {
      let item = el.querySelector('input, select');
      return {
        type : item.tagName,
        value : (v => caseInsensitive ? v.toLowerCase() : v)(item.value)
      };
    });
  table.querySelectorAll('tbody tr').forEach(tr => {
    let visible = [].slice.call(tr.querySelectorAll('td')).every((td, i) => {
      let text = (t => caseInsensitive ? t.toLowerCase() : t)(td.textContent.trim());
      if (terms[i].type === 'SELECT') {
       if (terms[i].value.toUpperCase() === 'ALL') return true;
       return terms[i].value === text;
      }
      return text.indexOf(terms[i].value) > -1;
    });
    tr.classList.toggle('hidden-row', !visible);
  });
}

// https://stackoverflow.com/a/14234618/1762224
// Note: Modified as a recursive routine
function findParentBySelector(el, target) {
  return el != null ? el && ![].slice.call(typeof target === 'string' ? (target = document.querySelectorAll(target)) : target).some(c => c == el) ? findParentBySelector(el.parentNode, target) : el : null;
}
body {
  background: #444 !important;
}

.data-table {
  width: 96% !important;
  margin: 1em auto !important;
  padding: 0.5em;
}

/** Toggled */
.hidden-row {
  display: none;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/framework7/4.5.0/css/framework7.bundle.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/framework7/4.5.0/js/framework7.bundle.min.js"></script>
<div class="card data-table">
  <table data-ignore-case="true">
    <thead>
      <tr>
        <th class="input-cell">
          <span class="table-head-label">ID</span>
          <div class="input" style="width: 50px">
            <input type="number" placeholder="Filter">
          </div>
        </th>
        <th class="input-cell">
          <span class="table-head-label">Name</span>
          <div class="input">
            <input type="text" placeholder="Filter">
          </div>
        </th>
        <th class="input-cell">
          <span class="table-head-label">Email</span>
          <div class="input">
            <input type="text" placeholder="Filter">
          </div>
        </th>
        <th class="input-cell">
          <span class="table-head-label">Gender</span>
          <div class="input input-dropdown">
            <select>
              <option value="All">All</option>
              <option value="Male">Male</option>
              <option value="Female">Female</option>
            </select>
          </div>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>1</td>
        <td></td>
        <td>john@doe.com</td>
        <td>Male</td>
      </tr>
    </tbody>
  </table>
</div>

I want to emphasize that this is a subjective variation, I prefer to see the result even with empty fields

Simone Rossaini
  • 8,115
  • 1
  • 13
  • 34