javascriptjqueryhtmlfilter

Displaying a message when jQuery filter returns nothing


I found code online that filters elements by their text content.
How can I display a message when there is no match?

$("button").click(function() {
  var value = $(this).data('value').toUpperCase();
  $("div").filter(function(index) {
    $(this).toggle($(this).text().indexOf(value) > -1)
  });
});
<button>example</button>

Solution

  • You're using filter() to toggle each item based on state, like using each(). But one advantage of filter() is that you can return a reduced selection and count the items it contains. That value can determine whether a "no match" message should be displayed.

    ... the .filter() method constructs a new jQuery object from a subset of the matching elements. The supplied selector is tested against each element; all elements matching the selector will be included in the result. -- filter().

    For each element, if the function returns true (or a "truthy" value), the element will be included in the filtered set; otherwise, it will be excluded. -- Using a Filter Function

    So, instead of toggling items directly from the filter call, consider returning a Boolean measure of whether the current item is a match. Save the resulting filtered selection in a variable. After filtering, you can toggle that selection as a whole:

    var $filtered = $items.filter(function() {
      return $(this).text().indexOf(value) > -1;
    });
    
    $items.toggle(false);
    $filtered.toggle(true);
    

    This hides all items and then shows only the filtered items.
    You might even consider some fading animation:

    $items.hide(250);
    $filtered.stop(true,false).show(250);
    

    Then you can reference the filtered selection's length.
    If it's zero, show the "not found" message:

    var hasMatches = $filtered.length;
    
    if (hasMatches) {
      // there were matches.
    } else {
      // no matches.
    }
    

    You can also pass a selector to a filter. jQuery's :contains() selector selects "all elements that contain the specified text", which makes a nice choice.

    Working Example:

    var $items = $('.item');
    var $none = $('#none');
    var fade = 250;
    
    function filterContent() {
    
      // get word from value of clicked button.
      var word = this.value;
    
      // hide items; filter; show filtered; count matches
      var hasMatches = $items
        .hide(fade)
        .filter(':contains(' + word + ')')
        .stop(true, false)
        .show(fade)
        .length;
    
      // if no matches, show message.
      if (hasMatches) {
        $none.hide(fade);
      } else {
        $none.show(fade);
      }
    
    }
    
    $('button').on('click', filterContent);
    #none {
      display: none;
      color: darkred;
    }
    
    #buttons {
      margin: 1em 0 0;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    
    <div class="item">Here is some text.</div>
    <div class="item">Here is some other text.</div>
    <div class="item">Here is some other different text.</div>
    <div class="item">Here is something else.</div>
    <div class="item">Here is some additional text.</div>
    
    <div id="none">No matches found.</div>
    
    <nav id="buttons">
      <button type="button" value="">all</button>
      <button type="button" value="text">text</button>
      <button type="button" value="other">other</button>
      <button type="button" value="additional">additional</button>
      <button type="button" value="bazooka">bazooka</button>
    </nav>


    Another way:

    If you prefer, you can toggle inside the filter as long as you still return the state Boolean from the function. I suggest making a separate function to pass to the filter. In this case, toggleItem() determines the state of an item (match or non-match), toggles the item according to that state, and returns the state.

    var $items = $('.item');
    var $none = $('#none');
    
    function toggleItem(word) {
      return function(k, el) {
        var $item = $(el);
        var state = $item.text().indexOf(word) > -1;
        $item.toggle(state);
        return state;
      }
    }
    
    function filterContent() {
    
      // get word from value of clicked button.
      var word = this.value;
    
      // filter while toggling and count result.
      var hasMatches = $items
        .filter(toggleItem(word))
        .length;
    
      // if no matches, show message.
      $none.toggle(!hasMatches);
    
    }
    
    $('button').on('click', filterContent);
    #none {
      display: none;
      color: darkred;
    }
    
    #buttons {
      margin: 1em 0 0;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    
    <div class="item">Here is some text.</div>
    <div class="item">Here is some other text.</div>
    <div class="item">Here is some other different text.</div>
    <div class="item">Here is something else.</div>
    <div class="item">Here is some additional text.</div>
    
    <div id="none">No matches found.</div>
    
    <div id="buttons">
      <button type="button" value="">all</button>
      <button type="button" value="text">text</button>
      <button type="button" value="other">other</button>
      <button type="button" value="additional">additional</button>
      <button type="button" value="bazooka">bazooka</button>
    </div>

    In my opinion, this is a bit harder to read and not as clear as the chained "hide,filter,show,length" commands, but that's somewhat a matter of style. You can see that there are many ways to bake this cake!

    This one's pretty short and sweet:

    var $none = $("#none");
    var $items = $(".item");
    
    $("button").click(function() {
    
      var value = $(this).data('value');
    
      $items.each(function() {
        $(this).toggle($(this).text().indexOf(value) > -1);
      });
    
      $none.toggle(!$items.filter(':visible').length);
    
    });
    #none {
      display: none;
      color: darkred;
    }
    
    #buttons {
      margin: 1em 0 0;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    
    <div class="item">Here is some text.</div>
    <div class="item">Here is some other text.</div>
    <div class="item">Here is some other different text.</div>
    <div class="item">Here is something else.</div>
    <div class="item">Here is some additional text.</div>
    
    <div id="none">No matches found.</div>
    
    <nav id="buttons">
      <button type="button" data-value="">all</button>
      <button type="button" data-value="text">text</button>
      <button type="button" data-value="other">other</button>
      <button type="button" data-value="additional">additional</button>
      <button type="button" data-value="bazooka">bazooka</button>
    </nav>