How can I filter my bootstrap 4 navbar links with a search bar?

How can I filter my bootstrap 4 navbar links with a search bar?

I’m transitioning my old website using Bootstrap 4, so it’s definitely been a process of learning.
I’ve got the site layout working fine, but I realize that my left-sided vertical nav bar has a lot of links. Many of these grouped in collapsed nests.
I think it would be nice to add a search bar at the top of my nav so that I can filter the links based on partial strings entered in the search bar. This works for links that are not hiding inside a hidden div (or class=collapsed boostrap 4 ul).
I’d appreciate assistance in modifying my code to show filtered results that include any links hiding inside the collapsed ul?
Here’s a fiddle

$(‘.search-filter’).on(‘keyup’, function() {
var input = $(‘.search-filter’).val();
var filter = input.toLowerCase();

if (filter.length == 0) { // show all if filter is empty
$(‘a’).each(function() {
$(this).show(); // show links
});
return;
} else {
$(‘a’).removeClass(‘collapsed’);
$(‘a’).each(function() {
$(this).hide(); // hide all links once search is begun
});

$(‘a:contains(“‘ + filter + ‘”)’).each(function() {
$(this).removeClass(‘collapsed’); // remove bootstrap 4 collapsed class designation
$(this).show(); // show only matched links to search string?

});
}
});
@import url(‘https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css’);

.navbar-nav.sidebar-nav {
position: absolute;
left: 0;
top: 0;
margin-top: 56px;
padding-bottom: 56px;
height: 100vh;
background: #292b2c;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
overflow: auto;
}

.navbar-brand {
display: inline-block;
padding-top: .25rem;
padding-bottom: .25rem;
margin-right: 1rem;
font-size: 1.25rem;
line-height: inherit;
white-space: nowrap;
color: #fff;
}

.navbar-nav .nav-link {
color: rgba(255, 255, 255, .5);
}



Solutions/Answers:

Solution 1:

Okay, I think i’ve figured it out on my own (with a little help from this post for a case insensitive filter).

1) I updated the links by wrapping them in a div with id #link-content, to separate my filter from the home and search input.

2) I added the case insensitive method referenced above

My HTML:

   <ul class="sidebar-nav navbar-nav">
      <li class="nav-item active">
         <a class="nav-link" href="#"><i class="fa fa-fw fa-home"></i> Home</a>
      </li>
      <li class="nav-item">
         <label for="nav-search" class="col-2 col-form-label sr-only">Search links</label>
         <div class="col p-2">
            <input class="form-control form-control-sm search-filter" type="search" id="nav-search" placeholder="Search for tools">
         </div>
      </li>
 <div id="link-content"><!-- added this to wrap the links-->     
      <li class="nav-item">
         <span class="navbar-brand">Popular tools</span>
      </li>
      <li class="nav-item filter">
         <a class="nav-link" href="#"><i class="fa fa-fw fa-calculator"></i> Calculator</a>
      </li>
      <li class="nav-item  filter">
         <a class="nav-link" href="#"><i class="fa fa-fw fa-battery-3"></i> Battery </a>
      </li>
      <li class="nav-item  filter">
         <a class="nav-link" href="#"><i class="fa fa-fw fa-database"></i> Pancake Batter</a>
      </li>

      <li class="nav-item  filter">
         <a class="nav-link" href="#"><i class="fa fa-fw fa-clock-o"></i> Marzipan</a>
      </li>
      <li class="nav-item  filter">
         <a class="nav-link" href="#"><i class="fa fa-fw fa-tags"></i> Cakes and Muffins</a>
      </li>
      <li class="nav-item  filter">
         <span class="navbar-brand">Categories</span>
      </li>
      <li class="nav-item ">
         <a class="nav-link nav-link-collapse collapsed" data-toggle="collapse" href="#collapseComponents"><i class="fa fa-fw fa-flask"></i> Cars</a>
         <ul class="sidebar-second-level collapse" id="collapseComponents">
            <li>
               <a class="nav-link-collapse collapsed" data-toggle="collapse" href="#collapseMulti2">American</a>
               <ul class="sidebar-third-level collapse" id="collapseMulti2">
                  <li>
                     <a href="#">Ford</a>
                  </li>
                  <li>
                     <a href="#">GMC</a>
                  </li>
               </ul>
            </li>
            <li>
               <a class="nav-link-collapse collapsed" data-toggle="collapse" href="#collapseMulti3">European</a>
               <ul class="sidebar-third-level collapse" id="collapseMulti3">
                  <li>
                     <a href="#">BMW</a>
                  </li>
                  <li>
                     <a href="#">Audi</a>
                  </li>
               </ul>
            </li>
         </ul>
      </li>
      </div>
   </ul>

My updated code:

// Case insensitive method for filter
jQuery.expr[':'].casecontains = (a, b, c) => jQuery(a).text().toUpperCase().indexOf(c[3].toUpperCase()) >= 0;

$('.search-filter').on('keyup', function () {
            var input = $('.search-filter').val();
                        console.log('input: '+input);
            if (input.length != 0) { 
            // first hide the div #link-content lists from view
            $('#link-content li').hide();
            // but secretly unhide the collapsed links
            // using .show, so the nested uls can be viewed
            $('#link-content li.nav-item ul').show();
            // then filter in the matching links only
            $('#link-content li:casecontains("'+input+'")').show();
            } else {
            // secretly unhide the collapsed links
            $('#link-content li.nav-item ul').hide();  
            // if search is empty, show the div and reset columns
            $('#link-content li').show();
            }
        });

Here is a fiddle with the working example that opens any hidden links (collapsed class in boostrap 4).

Related:  Re-initialize or destroy Bootstrap datepicker dynamically

Solution 2:

The approach I’ve used is to remember which items match, and then use that list to un-hide the parents.

Updated Fiddle: https://jsfiddle.net/j2gpann3/1/

The advantage of this approach is that the hidden sub-menu items (like ‘BMW’ etc.) will now show up in the search and won’t have huge gaps above them from the other hidden items. The regex search searches for occurences of each word, so even if they are out of order to the menu item text, it will still match.

$('.search-filter').on('keyup', function() {
  var matches = [];
  var input = $.trim($('.search-filter').val());
  var val = '^(?=.*\\b' + input.split(/\s+/).join('\\b)(?=.*\\b') + ').*$'; // using individual word matching filter from http://stackoverflow.com/a/9127872/1544886
  var filter = RegExp(val, 'i');

  if (input.length === 0) { // show all if filter is empty

    $('.collapse').removeClass('show').addClass('collapsed'); // hide collapsable items fast.
    $('.hide').removeClass('hide'); // remove any hidden elements from previous searches
  } else {
    $('.collapse').addClass('show'); // show all collapsed items

    $('ul.sidebar-nav a:not(".home")').filter(function() { // skip home <li> so it shows permanently
        $this = $(this);

        // test for a match to search string
        text = $this.text().replace(/\s+/g, ' ');
        var isMatch = filter.test(text);

                // store match so we can unhide parents of this item 
        if (isMatch) matches.push($this);

        return !isMatch;
    }).parent().addClass('hide'); // this hides any <li> that doesn't match search terms. Hiding <a> results in large gaps in the output

    $.each(matches, function() { // unhide parents of our matches
        this.parentsUntil(".sidebar-nav", ".hide").removeClass('hide');
    });
  }
});

The demo requires a home class added to the Home link to prevent it from being hidden by the search:

<a class="nav-link home" href="#"><i class="fa fa-fw fa-home"></i> Home</a>

and a CSS class for hide added:

.hide {
  display: none;
}

Solution 3:

If you remove the:

toLowerCase();

Everything matches perfectly if you type it perfectly. The issue is that you make the search term lower case, but it’s not matching against lower case words. So when you type calculator, it doesn’t match because the actual nav item shows as Calculator, with a capital c.

Related:  Why does Bootstrap 3 force the container width to certain sizes?

So you can either make the nav items lower case as well, via HTML or JavaScript, or you go about it in a different way, i.e. using IDs, but that would cause issues if the site is dynamic.

Bit of a weird work around may be to capitalize the first letters of each word so it actually matches the HTML. Take a look at How to capitalize first letter of each word, like a 2-word city?

References