Search code examples
javascripthtmlcssstimulusjs

Creating a dynamic search bar using Stimulus.js


I have created a webpage for a restaurant menu. At the top of the page there is a simple search input bar that users can use to type in a specific item name. The goal is that as soon as the user starts typing, the menu will filter results of the matching items, whilst hiding the rest. I do not want to use a button or anything for this to work; I would just like the filtering to occur as soon as the user starts typing. Here is my css to hide the menu cards (which include the name of the item, details and price) that are filtered out.

CSS:

.menu-card .hidden {
  display: none;
}

And here is a snippet of the HTML (there are multiple menu-cards like the one below):

<div class="searchbar">
        <i class="fa-solid fa-magnifying-glass"></i>
        <input type="search" id="search" placeholder="Search menu" data-controller="search-bar" data-action="input->search-bar#search">
      </div>

<div class="menu-card" data-search-bar-target="menuCard">
            <h4 class="item-name">Greek Village Salad with Feta</h4>
            <p class="item-details">Tomato, cucumber, green pepper, red onion, feta in a rich salad</p>
            <p class="item-price">£10.00</p>
          </div>

I have also set up a stimulus controller and I can confirm that it is all connected so that is not the issue here. I think something might be missing from my code but I can't seem to figure out why it isn't working. If anyone has an idea, any help would be great!

search_bar_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["menuCard"];

  connect() {
    // console.log("Hello, you are inside the search_bar controller!");
  }

  search(event) {
    console.log(event);
    const searchInput = event.target.value.toLowerCase(); // assigns the value of the text in the input field

    this.menuCardTargets.forEach((menuCard) => {
      const itemName = menuCard.querySelector(".item-name").textContent.toLowerCase();

      if (itemName.includes(searchInput)) { // if the name of the menu item is the same as whatever is inputted in the search field
        menuCard.classList.remove("hidden"); // display the menu card of the menu item
      } else {
        menuCard.classList.add("hidden"); // hide the menu card of all the items that do not match the input
      }
    });
  }
}

Solution

  • The issue is your HTML structure, your controlled element (the one with the data-controller) must be a parent of any targets you want to access.

    This HTML should be functional, wrap the .searchbar and the .menu-card in a div or put the controller on whatever element contains both those things.

    <div data-controller="search-bar">
      <div class="searchbar">
        <i class="fa-solid fa-magnifying-glass"></i>
        <input type="search" id="search" placeholder="Search menu" data-action="input->search-bar#search">
      </div>
      <div class="menu-card" data-search-bar-target="menuCard">
        <h4 class="item-name">Greek Village Salad with Feta</h4>
        <p class="item-details">Tomato, cucumber, green pepper, red onion, feta in a rich salad</p>
        <p class="item-price">£10.00</p>
      </div>
    </div>
    

    A few recommendations

    • Do not hide anything until the user has typed maybe two or three characters, if they type a common letter like a, maybe all menu items will hide until they type ad (search for Salad).
    • Hiding the menu items with display none will make your UI very janky, may I suggest visibility hidden as it will ensure that the menu 'container' stays in place.
    • When you
    • Hiding things dynamically this way generally is a bit of an accessibility problem, it may even be better to just grey them out / fade them slightly (or better, visibly highlight the ones that match the search). Imagine you are using a keyboard only and cannot see the screen, you type something and you have no idea what's changed, you tab to the menu items and some are no longer there.

    Enhancement - clearing on blur

    You should have a way for the hidden fields to be reset/cleared once you move away from the field or menu.

    
      clear() {
        this.menuCardTargets.forEach((menuCard) => {
          menuCard.classList.remove('hidden');
        });
      }
    
    

    Using data-action="blur->search-bar#clear" you can ensure that any hidden classes will be reset once you move away from the menu (e.g. close it).

    You will also want to re-add the classes once you re-focus on the field.

    <div data-controller="search-bar" data-action="blur->search-bar#clear">
      <div class="searchbar">
        <i class="fa-solid fa-magnifying-glass"></i>
        <input type="search" id="search" placeholder="Search menu" data-action="input->search-bar#search focus->search-bar#search">
      </div>
      <div class="menu-card" data-search-bar-target="menuCard">
        <h4 class="item-name">Greek Village Salad with Feta</h4>
        <p class="item-details">Tomato, cucumber, green pepper, red onion, feta in a rich salad</p>
        <p class="item-price">£10.00</p>
      </div>
    </div>
    

    Enhancement - using Stimulus classes to avoid hardcoding your .hidden class

    Stimulus has a nice feature called CSS classes that allows you to reference a set of classes from within your controller.

    This means your HTML can contain what classes are used for your logic, not your JavasCript code.

    Set static classes = ... on your Controller and reference them with something like this.fooClass.

    export default class extends Controller {
      static classes = ['hidden'];
    
      //... rest of Controller
          if (itemName.includes(searchInput)) {
            // if the name of the menu item is the same as whatever is inputted in the search field
            menuCard.classList.remove(this.hiddenClass); // display the menu card of the menu item
          } else {
            menuCard.classList.add(this.hiddenClass); // hide the menu card of all the items that do not match the input
          }
        });
      }
    }
    

    Then, in your HTML, you set the class you want there data-search-bar-hidden-class="hidden".

    This way, if you want to change your code to use a different class, you do not need to change your JavaScript.

    <div data-controller="search-bar" data-search-bar-hidden-class="hidden">
      <div class="searchbar">
        <i class="fa-solid fa-magnifying-glass"></i>
        <input type="search" id="search" placeholder="Search menu" data-action="input->search-bar#search">
      </div>
      <div class="menu-card" data-search-bar-target="menuCard">
        <h4 class="item-name">Greek Village Salad with Feta</h4>
        <p class="item-details">Tomato, cucumber, green pepper, red onion, feta in a rich salad</p>
        <p class="item-price">£10.00</p>
      </div>
    </div>