4

I have a table I've built in JavaScript that's basically a big list of publications, with a "year" header, followed by a row for each publication for that year:

  <table class="mytable" id="myTable">
  <tr class="header">
    <th style="width:60%;">2020</th>
  </tr>
  <tr name="latin_america_zero-deforestation">
    <td>"Cattle Ranchers and Deforestation.”
    </td>
  </tr>
  <tr name="latin_america_policy-outcomes">
    <td>“Impacts on Land Use Conversion.”
    </td>
  </tr>
  <tr name="latin_america_supply-chain_policy-outcomes">
    <td>“Costs of the Amazon Soy Moratorium.”
    </td>
  </tr>
  <tr class="header">
    <th style="width:60%;">2019</th>
  </tr>
  <tr name="africa_policy-outcomes">
    <td>“Environmental Change”
    </td>
  </tr>
  <tr name="latin_america_policy-outcomes">
    <td>“Land Use Change”
    </td>
  </tr>
  <tr class="header">
    <th style="width:60%;">2018</th>
  </tr>
  <tr name="north_america_zero-deforestation">
    <td>“Deforestation Remedies”
    </td>
  </tr>
  <tr name="latin_america_zero-deforestation">
    <td>“Land Use Change in Latin American Cerrados”
    </td>
  </tr>
</table>  

Based on the tr name, I then am able to filter by keywords (either region or topic) via some buttons:

  <button class="btn" onclick="filterSelection('policy-outcomes')">Policy Outcomes</button>

  <button class="btn" onclick="filterSelection('latin_america')"> Latin America</button>

The buttons call this function, which filters based on keywords found in the tr name tag:

//Sort by class..
function filterSelectionByTopic(thing) {

  var rows = $('#myTable').find('tr:not(:has(th))').get();
  console.log(rows)

$("#myTable tr").show()


$('#myTable').find('tr:not(:has(th))').not('thead td').not("[name*=" + thing + "]").fadeOut(300)

var tabhead = "header"
//$("#myTable tbody tr:not(."+thing + ')').hide();

//$("#myTable tr.header").fadeIn(1)

}

While this works fine, I want the filters to hide table headers (years) where no rows are returned.

For example, if I filtered using filterSelection('policy-outcomes'): 2020 and 2019 have publications that meet this criteria, 2018 does not. Currently 2018 will show up as a table header with no rows underneath, but I would like to hide any table header that does not have rows that meet the criteria.

So, in essence, filterSelection('policy-outcomes') should look like:

2020
Impacts on Land Use Conversion.
Costs of the Amazon Soy Moratorium.
2019
Environmental Change.
Land Use Change.

How can I accomplish this?

halfer
  • 19,824
  • 17
  • 99
  • 186
DiamondJoe12
  • 1,879
  • 7
  • 33
  • 81
  • 1
    your HTML is wrong : `TR` elements doesn't accept `name` attribute, you should use dataset attributes instead – Mister Jojo Dec 06 '21 at 01:07
  • Does this answer your question? [How to sort an HTML table?](https://stackoverflow.com/questions/67913593/how-to-sort-an-html-table) – Mister Jojo Dec 06 '21 at 01:20

6 Answers6

2

You could combine the :has selector with the adjacent sibling selector (s1 + s2). So do something like:

$("#myTable").find("tr.header:has(+ tr.header)").hide()
Shreyansh Dash
  • 147
  • 1
  • 1
  • 9
1

You could combine the :has selector with the adjacent sibling selector (s1 + s2).
So do something like:

$("#myTable").find("tr.header:has(+ tr.header)").hide()

Which would select every header row that is immediately followed by another header row - that is, every header that doesn't have at least one row associated with it

and i guess use something like :last-child to catch the edge case where the last header returns no rows.

  • Shoot. I've tried: $("#myTable").find("tr.header:has(+ tr.header)").hide() Still no luck.. it doesn't hide the headers followed by another header. – DiamondJoe12 Dec 26 '21 at 18:26
1

This is my Vanilla JavaScript take on it.

Before writing the script I changed all name attributes to data-name as trs cannot have name attributes "legally".

In a first filter() loop I make all trs invisible and filter out only those trs that don't match the given pattern pat and are not of class "header". In a forEach() loop applied on this filtered set I then make only those trs visible again that either themselves have a dataset.name attribute or whose direct successor has this attribute.

const trs=[...document.querySelector("#myTable>tbody").children];
function fltTbl(pat){
  trs
   .filter(tr=>(tr.style.display="none",tr.classList.contains("header")||
tr.dataset?.name.indexOf(pat)>-1))
   .forEach((tr,i,a)=>{
  if (tr.dataset.name
      || a[i+1]?.dataset.name)
    tr.style.display="";
 });
}
<button class="btn" onclick="fltTbl('policy-outcomes')">Policy Outcomes</button>
<button class="btn" onclick="fltTbl('latin_america')">Latin America</button>
<button class="btn" onclick="fltTbl('deforestation')">Deforestation</button>

<table class="mytable" id="myTable">
  <tr class="header">
<th style="width:60%;">2020</th>
  </tr>
  <tr data-name="latin_america_zero-deforestation">
<td>"Cattle Ranchers and Deforestation.”
</td>
  </tr>
  <tr data-name="latin_america_policy-outcomes">
<td>“Impacts on Land Use Conversion.”
</td>
  </tr>
  <tr data-name="latin_america_supply-chain_policy-outcomes">
<td>“Costs of the Amazon Soy Moratorium.”
</td>
  </tr>
  <tr class="header">
<th style="width:60%;">2019</th>
  </tr>
  <tr data-name="africa_policy-outcomes">
<td>“Environmental Change”
</td>
  </tr>
  <tr data-name="latin_america_policy-outcomes">
<td>“Land Use Change”
</td>
  </tr>
  <tr class="header">
<th style="width:60%;">2018</th>
  </tr>
  <tr data-name="north_america_zero-deforestation">
<td>“Deforestation Remedies”
</td>
  </tr>
  <tr data-name="latin_america_zero-deforestation">
<td>“Land Use Change in Latin American Cerrados”
</td>
  </tr>
</table>
Carsten Massmann
  • 26,510
  • 2
  • 22
  • 43
1

It is generally not a good idea to use inline event handlers. The snippet uses event delegation and plain es-20xx.

The buttons contain a data-term-attribute from which a css queryselector for the data-name-rows can be constructed. The name-attributes are replaced with data-name-attributes, with comma delimited values, corresponding to the data-term-attributes from the buttons.

To the 'year' rows I added a data-year-attribute, to be able to easy find a header from a data-name row.

The handler:

  • Filters all rows using a regular expression derived from the data-term-attribute from the clicked button
  • If an element is found, displays it, finds the previous header and displays that too

The snippet is also available in Stackblitz.

const doFilter = FilterHandlerFactory();
document.addEventListener(`click`, handle);

function handle(evt) {
  if (evt.target.dataset.term) {
    return  doFilter(evt.target);
  }
}

// Factory to avoid recreation of [showParentHeader] on every click
function FilterHandlerFactory() {
  const isHead = r => r.dataset.year && r;
  // helper (closed over):
  // move up until a header is found and display that
  const showParentHeader = row => {
    let prev = row.previousElementSibling;
    while (prev && !isHead(prev)) {  prev = prev.previousElementSibling; }
    prev.classList.remove(`hidden`);
  };

  return fromBtn => {
    const all = fromBtn.dataset.term === `All`;
    const filter = RegExp(fromBtn.dataset.term, "i");
    document.querySelectorAll(`#myTable tr`)
      .forEach( row => {
        if (all || filter.test(row.dataset.name)) {
          row.classList.remove(`hidden`);
          return all || showParentHeader(row);
        }

        row.classList.add(`hidden`);
      } );
  }
}
body {
  font: normal 12px/15px verdana, arial;
}

#myTable {
  border: 1px solid #999;
  border-collapse: separate;
  margin-top: 1rem;
  width: 80vw;
}

#myTable th {
  background-color: #ddd;
  border-bottom: 1px dotted #777;
  border-top: 1px dotted #777;
}

button {
  margin-right: 2px;
}

button[data-term]:before {
  content: attr(data-term);
}

.hidden {
  display: none;
}

.btnBlock {
  display: flex;
  flex-direction: row; 
}
<div class="btnBlock">
  <button data-term="Policy Outcomes"></button>
  <button data-term="Latin America"></button>
  <button data-term="Deforestation"></button>
  <button data-term="Supply Chain"></button>
  <button data-term="Africa"></button>
  <button data-term="North America"></button>
  <button data-term="All"></button>
</div>
<table class="mytable" id="myTable">
  <tr class="header" data-year="2020">
    <th>2020</th>
  </tr>
  <tr data-name="latin america, deforestation">
    <td>"Cattle Ranchers and Deforestation.”</td>
  </tr>
  <tr data-name="latin america, policy outcomes">
    <td>“Impacts on Land Use Conversion.”</td>
  </tr>
  <tr data-name="latin america, supply chain, policy outcomes">
    <td>“Costs of the Amazon Soy Moratorium.”</td>
  </tr>
  <tr class="header" data-year="2019">
    <th>2019</th>
  </tr>
  <tr data-name="africa, policy outcomes">
    <td>“Environmental Change”</td>
  </tr>
  <tr data-name="latin america, policy outcomes">
    <td>“Land Use Change”</td>
  </tr>
  <tr class="header" data-year="2018">
    <th>2018</th>
  </tr>
  <tr data-name="north america, deforestation">
    <td>“Deforestation Remedies”</td>
  </tr>
  <tr data-name="latin america, deforestation">
    <td>“Land Use Change in Latin American Cerrados”</td>
  </tr>
</table>

Alternatively, consider not using a <table> (the data are not really tabular after all).

Stackblitz playground for this snippet.

document.addEventListener(`click`, handle);
const doFilter = FilterHandlerFactory(`myData`);

function handle(evt) {
  if (evt.target.dataset.term) {
    return  doFilter(evt.target);
  }
}

// filter handler factory
function FilterHandlerFactory(filterId) {
  let filterTerm, all;
  const filterYears = document.querySelectorAll(`#${filterId} div[data-year]`);
  const filterItems = document.querySelectorAll(`#${filterId} ul li`);
  const year = item => item.closest(`[data-year]`);
  const hide = el => el.classList.add(`hidden`);
  const show = el => el.classList.remove(`hidden`);
  const displayFilteredItem = item => { show(item); show(year(item)); };
  const setTerms = fromBtn => {
    all = fromBtn.dataset.term === `All`;
    filterTerm = RegExp(fromBtn.dataset.term, "i");
  };
  const filter = item => all || filterTerm.test(item.dataset.name || '-') 
      ? displayFilteredItem(item)
      : hide(item);

  return fromBtn => {
    setTerms(fromBtn);
    filterYears.forEach( hide )
    filterItems.forEach( filter );
  };
}
body {
  margin: 2rem;
  font: normal 12px/15px verdana, arial;
}

#myData {
  margin-top: 1rem;
  width: 80vw;
}

#myData div[data-year] {
  opacity: 1;
  max-height: initial;
  transition: all ease-in-out .7s;
}

#myData div[data-year].hidden {
  opacity: 0;
  max-height: 0;
  margin: 0;
}

#myData div[data-year] ul li.hidden {
  display: none;
}

button {
  margin-right: 2px;
}

button[data-term]:before {
  content: attr(data-term);
}

.btnBlock {
  display: flex;
  flex-direction: row;
}

#myData div[data-year] ul {
  margin-left: -1.5rem;
  margin-top: 0.2rem;
}

#myData div[data-year] ul li {
  list-style-type: '\2713';
  padding-left: 5px;
}

#myData div[data-year]:before {
  content: attr(data-year);
  text-align: center;
  font-weight: bold;
  display: block;
  width: 100%;
  background-color: #ddd;
  border-bottom: 1px dotted #777;
  border-top: 1px dotted #777;
}
<div class="btnBlock">
  <button data-term="Policy Outcomes"></button>
  <button data-term="Latin America"></button>
  <button data-term="Deforestation"></button>
  <button data-term="Supply Chain"></button>
  <button data-term="Africa"></button>
  <button data-term="North America"></button>
  <button data-term="All"></button>
</div>

<div id="myData">

  <div data-year="2020">
    <ul>
      <li data-name="latin america, deforestation">
        Cattle Ranchers and Deforestation.
      </li>
      <li data-name="latin america, policy outcomes">
        Impacts on Land Use Conversion.
      </li>
      <li data-name="latin america, supply chain, policy outcomes">
        Costs of the Amazon Soy Moratorium.
      </li>
    </ul>
  </div>

  <div data-year="2019">
    <ul>
      <li data-name="africa, policy outcomes">Environmental Change</li>
      <li data-name="latin america, policy outcomes">Land Use Change</li>
    </ul>
  </div>

  <div data-year="2018">
    <ul>
      <li data-name="north america, deforestation">
        Deforestation Remedies
      </li>
      <li data-name="latin america, deforestation">
        Land Use Change in Latin American Cerrados
      </li>
    </ul>
  </div>

</div>
KooiInc
  • 119,216
  • 31
  • 141
  • 177
0

This solution starts at the last row and walks up the DOM tree. If a table row doesn't have a term, it hides it. I also added a clear button.

function filterSelection(topic) {
  const index = {};
  const HIDE_CLASS = 'policy-no-term';
  let tr = document.querySelector('tr:last-child');
  let hasTerms = false;
  
  while(tr) {
     tr.classList.remove(HIDE_CLASS);
    
     if (tr.classList.contains('header')) {
       if (hasTerms === false) {
         tr.classList.add(HIDE_CLASS);
       }
       hasTerms = false;
     } else if(tr.getAttribute('name').indexOf(topic) > -1) {
       hasTerms = true;
       tr.classList.remove(HIDE_CLASS);
     } else {
       hasTerms = hasTerms || false;
       tr.classList.add(HIDE_CLASS);
     }
     tr = tr.previousElementSibling;
  }
}
first last
  • 396
  • 2
  • 5
0

Solution

This solution goes through each row and saves the headers. It then looks for data rows following it that match the regex of the search term. If there were no data rows following the header before it reaches the next header it will apply a style of none to the header node.

Example

The below example sets a search term of africa and then 4 seconds later applies a search term of latin.

//Sort by class..
function filterSelectionByTopic(thing) {
  $("#myTable tr").show()

  $('#myTable').find('tr:not(:has(th))').not('thead td').not("[name*=" + thing + "]").fadeOut(300)

const allRows = Array.from($('#myTable').find('tr'));
let currentHeaderNode = null;
let visibleRows = 0;
allRows.forEach((row, index,{ length })=>{
  // reset display none from last search
  if (row.classList.contains("header"))
    row.style.display=undefined;
  if (row.classList.contains("header") || index===length-1) {
    if (!visibleRows && currentHeaderNode !== null) {
      currentHeaderNode.style.display="none";
    }
    // header to target if no sibling data rows
    currentHeaderNode = row;
    visibleRows = 0;
    return
  }
  if (new RegExp(thing,"gi").test(row.getAttribute("name"))) {
    // There IS a row that will be shown (don't hide header)
    visibleRows++;
  }
})
}

filterSelectionByTopic("africa")

setTimeout(()=>{filterSelectionByTopic("latin")},4000)
th {
  background-color:red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<table class="mytable" id="myTable">
  <tr class="header">
    <th style="width:60%;">2020</th>
  </tr>
  <tr name="latin_america_zero-deforestation">
    <td>"Cattle Ranchers and Deforestation.”
    </td>
  </tr>
  <tr name="latin_america_policy-outcomes">
    <td>“Impacts on Land Use Conversion.”
    </td>
  </tr>
  <tr name="latin_america_supply-chain_policy-outcomes">
    <td>“Costs of the Amazon Soy Moratorium.”
    </td>
  </tr>
  <tr class="header">
    <th style="width:60%;">2019</th>
  </tr>
  <tr name="africa_policy-outcomes">
    <td>“Environmental Change”
    </td>
  </tr>
  <tr name="latin_america_policy-outcomes">
    <td>“Land Use Change”
    </td>
  </tr>
  <tr class="header">
    <th style="width:60%;">2018</th>
  </tr>
  <tr name="north_america_zero-deforestation">
    <td>“Deforestation Remedies”
    </td>
  </tr>
  <tr name="latin_america_zero-deforestation">
    <td>“Land Use Change in Latin American Cerrados”
    </td>
  </tr>
</table>
Daniel
  • 1,392
  • 1
  • 5
  • 16