Millhorn
Millhorn

Reputation: 3166

How to filter elements using JavaScript filter?

I'm struggling to get this filter to work properly.

I have it set to filter and display only what's searched, based on the card titles (h5). It's filtering the unwanted titles out, but not the rest of the card.

To better explain, there's a demo here - JS Element Filter

Here's the Code:

function myFunction() {
    var input, filter, card, h5, a, i;
    input = document.getElementById("myFilter");
    filter = input.value.toUpperCase();
    card = document.getElementById("myItems");
    h5 = card.getElementsByTagName("h5");
    for (i = 0; i < h5.length; i++) {
        a = h5[i].getElementsByTagName("a")[0];
        if (a.innerHTML.toUpperCase().indexOf(filter) > -1) {
            h5[i].style.display = "";
        } else {
            h5[i].style.display = "none";
        }
    }
}
.container {
  padding: 10px;
}

ul li {
  list-style: none;
  
}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<div class="container">
  <div class="row">
    <div class="col-sm-12 mb-3">
      <input type="text" id="myFilter" class="form-control" onkeyup="myFunction()" placeholder="Search for names..">
    </div>
  </div>
  <div class="row" id="myItems">
    <div class="col-sm-12 mb-3">
      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card One</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>

      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card Two</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>

      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card Three</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>      
    </div>    
  </div>
</div> 

Upvotes: 2

Views: 17763

Answers (4)

Damiaan Dufaux
Damiaan Dufaux

Reputation: 4785

Cause

You are only hiding the title (h5.card-title) and not the whole card (div.card)

Solution

First get a reference to the whole card. Then hide that element instead of just the title.

Implementation A

A quick and dirty solution to get a reference to the whole card would be to access it via the parentElement property. Since the parent of your <h5> is the card-body and it's parent is the entire card you acces it via h5[i].parentElement.parentElement.

So change h5[i].style.display to h5[i].parentElement.parentElement.style.display like this:

function myFunction() {
    var input, filter, card, h5, a, i;
    input = document.getElementById("myFilter");
    filter = input.value.toUpperCase();
    card = document.getElementById("myItems");
    h5 = card.getElementsByTagName("h5");
    for (i = 0; i < h5.length; i++) {
        a = h5[i].getElementsByTagName("a")[0];
        if (a.innerHTML.toUpperCase().indexOf(filter) > -1) {
            h5[i].parentElement.parentElement.style.display = "";
        } else {
            h5[i].parentElement.parentElement.style.display = "none";
        }
    }
}
.container {
  padding: 10px;
}

ul li {
  list-style: none;
  
}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<div class="container">
  <div class="row">
    <div class="col-sm-12 mb-3">
      <input type="text" id="myFilter" class="form-control" onkeyup="myFunction()" placeholder="Search for names..">
    </div>
  </div>
  <div class="row" id="myItems">
    <div class="col-sm-12 mb-3">
      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card One</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>

      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card Two</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>

      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card Three</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>      
    </div>    
  </div>
</div> 

Implementation B

A more robust solution would be to iterate over the cards instead of over the titles. That way you directly have a reference to the card and don't have to fiddle with parentElements. When you only want to search on text it might also be useful to use the innerText property to acces the string of text inside your card titles.

function myFunction() {
    var input, filter, cards, cardContainer, h5, title, i;
    input = document.getElementById("myFilter");
    filter = input.value.toUpperCase();
    cardContainer = document.getElementById("myItems");
    cards = cardContainer.getElementsByClassName("card");
    for (i = 0; i < cards.length; i++) {
        title = cards[i].querySelector(".card-body h5.card-title a");
        if (title.innerText.toUpperCase().indexOf(filter) > -1) {
            cards[i].style.display = "";
        } else {
            cards[i].style.display = "none";
        }
    }
}
.container {
  padding: 10px;
}

ul li {
  list-style: none;
}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<div class="container">
  <div class="row">
    <div class="col-sm-12 mb-3">
      <input type="text" id="myFilter" class="form-control" onkeyup="myFunction()" placeholder="Search for names..">
    </div>
  </div>
  <div class="row" id="myItems">
    <div class="col-sm-12 mb-3">
      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card One</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>

      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card Two</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>

      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card Three</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>      
    </div>    
  </div>
</div>

Upvotes: 2

Tschallacka
Tschallacka

Reputation: 28722

You are hiding the titles. What you want to hide is the card.

You can do this by doing h5[i].parentNode.parentNode since the title is nested 2 layers deep in the card.

If your clients browsers support it you can use Closest ancestor matching selector using native DOM?

or if you have jQuery at your disposal you can search for $(h5[i]).closest('.card');

function myFunction() {
    var input, filter, card, h5, a, i;
    input = document.getElementById("myFilter");
    filter = input.value.toUpperCase();
    card = document.getElementById("myItems");
    h5 = card.getElementsByTagName("h5");
    for (i = 0; i < h5.length; i++) {
        var current = h5[i];
        a = current.getElementsByTagName("a")[0];
        if (a.innerHTML.toUpperCase().indexOf(filter) > -1) {
            current.parentNode.parentNode.style.display = "";
        } else {
            current.parentNode.parentNode.style.display = "none";
        }
    }
}
.container {
  padding: 10px;
}

ul li {
  list-style: none;
  
}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<div class="container">
  <div class="row">
    <div class="col-sm-12 mb-3">
      <input type="text" id="myFilter" class="form-control" onkeyup="myFunction()" placeholder="Search for names..">
    </div>
  </div>
  <div class="row" id="myItems">
    <div class="col-sm-12 mb-3">
      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card One</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>

      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card Two</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>

      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card Three</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>      
    </div>    
  </div>
</div>

But I would actually recommend you by using getElementsByClassName to get all the cards, then loop through them, select the h5 element by the class name and then accessing the innerText of the h5, that way non visible text like a link title="some thing cool here" won't get in the way.

See the below snippet.

function myFunction() {
    var input, filter, myItems, cards, i, current, h5, text;
    input = document.getElementById("myFilter");
    filter = input.value.toUpperCase();
    myItems = document.getElementById("myItems");
    cards = myItems.getElementsByClassName("card");
    
    for (i = 0; i < cards.length; i++) {
        current = cards[i];
        h5 = current.getElementsByClassName('card-title')[0];
        text = h5.innerText.toUpperCase();
        if (text.indexOf(filter) > -1) {
            current.style.display = "";
        } else {
            current.style.display = "none";
        }
    }
}
.container {
  padding: 10px;
}

ul li {
  list-style: none;
  
}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<div class="container">
  <div class="row">
    <div class="col-sm-12 mb-3">
      <input type="text" id="myFilter" class="form-control" onkeyup="myFunction()" placeholder="Search for names..">
    </div>
  </div>
  <div class="row" id="myItems">
    <div class="col-sm-12 mb-3">
      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card One</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>

      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card Two</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>

      <div class="card">
        <div class="card-body">
          <h5 class="card-title"><a href="#">Card Three</a></h5>
          <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
          <p class="card-text">Some text.</p>
        </div>
      </div>      
    </div>    
  </div>
</div>

Upvotes: 0

Jack Wilkinson
Jack Wilkinson

Reputation: 487

The problem here is that you're only setting display:none and vice versa to the h5 and not the whole card itself.

See here

function myFunction() {
    var input, filter, card, h5, a, i;
    input = document.getElementById("myFilter");
    filter = input.value.toUpperCase();
    card = document.getElementById("myItems");
    h5 = card.getElementsByTagName("h5");
    for (i = 0; i < h5.length; i++) {
        a = h5[i].getElementsByTagName("a")[0];
        if (a.innerHTML.toUpperCase().indexOf(filter) > -1) {
            h5[i].closest(".card").style.display = "";
        } else {
            h5[i].closest(".card").style.display = "none";
        }
    }
}

I have changed it so that it gets its closest parent which has a class of .card here

h5[i].closest(".card")

Alternatively, if your target browser does not support .closest you can use

h5[i].parentNode.parentNode.style.display = "none"

See the full Codepen I have forked here: https://codepen.io/anon/pen/gjKgjN

Upvotes: 0

Juky
Juky

Reputation: 257

You must hide the whole card container instead of hiding the heading (h5) only. A quick correction would be using parentNode on the heading, for example

if (a.innerHTML.toUpperCase().indexOf(filter) > -1) {
    h5[i].parentNode.style.display = "";
} else {
    h5[i].parentNode.style.display = "none";
}

Upvotes: 0

Related Questions