Search code examples
javascriptselectfilterecmascript-6safari

Vanilla JS: Filtered Dropdown Select Options for Safari


I'm trying to filter an array corresponding to a select-element's option-children. Of course there are a bunch of ways to do this in Chrome, but I was trying to get it working in Safari. Other Posts here on Stack Overflow said it couldn't be done without recreating the select element's children with each and every keyup- so that's exactly what I've done.

I've got it working within safari currently filtering out indicating it can be edited, but currently it is not re-adding previously filtered items, which seems odd as the filter method is non-destructive, so I'm not exactly sure what it is that needs work but I'm sure its something dumb and simple, and then hopefully this helps others in the future.

codeSandbox link: https://codesandbox.io/s/new-snow-iwlxf1?file=/src/index.js

Note: The function keeps on referencing the elements located within the Dom every keyup event. Of course, it's these elements that are getting permanently filtered. Right now I think it's best to define it once, outside the function so that the value is not redefined like the new one...

Snippet:

const dropdownList = document.getElementById("myDropdown");
const origOptArray = Array.from(dropdownList.options).map((option) => {
  return option.value;
});

const filterOptions = (inputValue) => {
  const dropdownList = document.getElementById("myDropdown");
  const options = dropdownList.options;

  console.log(inputValue);
  const removeSelected = (options) => {
    // if the first option is a select placeholder, remove it from the options array
    if (
      options[0].value === "" ||
      options[0].textContent === "-- SELECT --" ||
      options[0].disabled === true ||
      options[0].value === null ||
      options[0].value === undefined
    ) {
      options[0].selected = false;
      const filteredOptions = Array.prototype.slice.call(options, 1);
      return filteredOptions;
    } else {
      return options;
    }
  };
  // Use the slice method to create a new array of options that excludes the first option
  const filteredOptions = removeSelected(options);

  // Use the filter function to create a new array of options that contain the input value
  const matchingOptions = Array.prototype.filter.call(
    filteredOptions,
    (option) => option.value.toUpperCase().startsWith(inputValue.toUpperCase())
  );

  // The appendChild method would probably make more sense here since we're pulling from
  // the original array. But then is the first array (matchikgOptions) even needed?
  const newMAtchingOptions = Array.prototype.filter.call(
    // I'm thinking the origOptArray goes here:
    filteredOptions,
    (option) => !option.value.toUpperCase().startsWith(inputValue.toUpperCase())
  );

  console.log(matchingOptions, newMAtchingOptions);

  // Remove all the options from the dropdown list
  while (dropdownList.firstChild) {
    dropdownList.removeChild(dropdownList.firstChild);
  }

  // // Add the first option back to the dropdown list
  const firstOption = document.createElement("option");
  firstOption.value = "";
  firstOption.textContent = "-- SELECT --";
  firstOption.disabled = true;
  dropdownList.append(firstOption);

  // Create new option elements for the matching options
  const newOptions = matchingOptions.map((option) => {
    const newOption = document.createElement("option");
    newOption.value = option.value;
    newOption.textContent = option.textContent;
    return newOption;
  });
  // Add the new options to the dropdown list
  dropdownList.append(...newOptions);
};

const input = document.getElementById("myInput");
const firstOption = document.createElement("option");
firstOption.value = "";
firstOption.textContent = "-- SELECT --";
firstOption.disabled = true;
dropdownList[0].textContent = "";
dropdownList[0].append(firstOption);

const inputField = document.getElementById("myInput");
inputField.addEventListener("input", function () {
  filterOptions(inputField.value);
});
        <!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="./src/index.js" defer></script>
    <title>SearchFilter</title>
  </head>
  <body>
    <div class="container">
      <form>
        <input type="text" id="myInput" placeholder="Search" />
        <select id="myDropdown">
          <option value="all">All</option>
          <option value="name">Name</option>
          <option value="nameste">Nameste</option>
          <option value="email">Email</option>
          <option value="emu">Emu</option>
          <option value="phone">Phone</option>
          <option value="rolex">Rolex</option>
        </select>
      </form>
    </div>
  </body>
</html>


Solution

  • The issue is that you are getting the options to filter using const options = dropdownList.options; each time you run the filterOptions function, but since the dropdown is already filtered, you get only what's left after the last filter operation. If you filter the original set of options instead, it will work (see snippet).

    Best of luck with your project:)

    const _dropdownList = document.getElementById("myDropdown");
    const origOptArray = Array.from(_dropdownList.options).map((option) => ({
      value: option.value,
      textContent: option.textContent
    }));
    
    const filterOptions = (inputValue) => {
      const dropdownList = document.getElementById("myDropdown");
    
      // Use the filter function to create a new array of options that contain the input value
      const matchingOptions = Array.prototype.filter.call(
        origOptArray,
        (option) => option.value.toUpperCase().startsWith(inputValue.toUpperCase())
      );
    
      // Remove all the options from the dropdown list
      while (dropdownList.firstChild) {
        dropdownList.removeChild(dropdownList.firstChild);
      }
    
      // // Add the first option back to the dropdown list
      const firstOption = document.createElement("option");
      firstOption.value = "";
      firstOption.textContent = "-- SELECT --";
      firstOption.disabled = true;
      dropdownList.append(firstOption);
    
      // Create new option elements for the matching options
      const newOptions = matchingOptions.map((option) => {
        const newOption = document.createElement("option");
        newOption.value = option.value;
        newOption.textContent = option.textContent;
        return newOption;
      });
      // Add the new options to the dropdown list
      dropdownList.append(...newOptions);
    };
    
    const input = document.getElementById("myInput");
    const firstOption = document.createElement("option");
    firstOption.value = "";
    firstOption.textContent = "-- SELECT --";
    firstOption.disabled = true;
    _dropdownList[0].textContent = "";
    _dropdownList[0].append(firstOption);
    
    const inputField = document.getElementById("myInput");
    inputField.addEventListener("input", function () {
      filterOptions(inputField.value);
    });
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <script src="./src/index.js" defer></script>
        <title>SearchFilter</title>
      </head>
      <body>
        <div class="container">
          <form>
            <input type="text" id="myInput" placeholder="Search" />
            <select id="myDropdown">
              <option value="all">All</option>
              <option value="name">Name</option>
              <option value="nameste">Nameste</option>
              <option value="email">Email</option>
              <option value="emu">Emu</option>
              <option value="phone">Phone</option>
              <option value="rolex">Rolex</option>
            </select>
          </form>
        </div>
      </body>
    </html>