Need some assistance 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 in my code like the one below):

<div class="searchbar">
        class="fa-solid fa-magnifying-glass">
        <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
      }
    });
  }
}

If you fix the two things below, your code should work.

  1. Your CSS selector - .menu-card .hidden - selects all elements with class hidden that are the descendants of an element with class menu-card. In your Stimulus controller, however, you are adding / removing the hidden class to and from the menuCard target which also has the class menu-card. Change your selector to .menu-card.hidden (i.e. no space between the class names) and then it will match on elements that have both menu-card and hidden in their class attribute.

  2. Stimulus controllers can only access elements in their scope. You defined the controller on the search input and added the target attribute to a menu card div which is outside of the scope of the controller (i.e. the menu card div is not a descendant of the search input). In the code you posted you can fix this by wrapping both the search bar and the menu card in another div and define the controller on that outer div:

    <div data-controller="search-bar">
      <div class="searchbar">
        <input type="search" ... data-action="input->search-bar#search">
      </div>
    
      <div class="menu-card" data-search-bar-target="menuCard">
        ...
      </div>
    </div>
    

    I assume your actual code has a more complex layout so you can’t just wrap these elements in a div, but you can certainly find their lowest common ancestor and add the data-controller="search-bar" attribute there.

    Alternatively, you can keep your HTML structure as is but instead of referencing the menu cards via Stimulus targets (i.e. instead of adding the data-search-bar-target="menuCard" attribute in the HTML and using this.menuCardTargets in the controller), you can just use vanilla JavaScript to find the menu cards in the Stimulus controller:

    ...
    search(event) {
       ...
       document.querySelectorAll('.menu-card').forEach((menuCard) => {
          ...
       });
       ...
    }
    

Thanks very much for your reply! Your response is very helpful