DEV Community

Jeremy Rollus
Jeremy Rollus

Posted on

Custom Datalist with Stimulus

Datalist have been introduced in HTML5 as a native way to bring autocomplete functionality to web dev. Begone tedious JS code and bow down to out-of-the-box Datalists or so did I naively think when I first heard of them.

The not-so-good-points

Unfortunately they come with a few caveats.

On the one hand, the below setting where you want the user to select from several different options but actually send a different value through the form, is quite common but not straightforward with Datalists.

<select name="game-status" id="status-select">
    <option value="">--Please choose an option--</option>
    <option value="1">Win</option>
    <option value="2">Loss</option>
    <option value="3">Draw</option>
</select>
Enter fullscreen mode Exit fullscreen mode

I naturally assumed that I could do the same with Datalists as such:

<input list="status-list" name="game-status" id="status-select" />
<datalist id="status-list">
  <option value="1">Win</option>
  <option value="2">Loss</option>
  <option value="3">Draw</option>
</datalist>
Enter fullscreen mode Exit fullscreen mode

Needless to say that I was quite underwhelmed when I saw the actual output in Chrome.

Datalist in Chrome

Without looking further into Datalists and their shortcomings, I simply thought about a simple workaround for that problem: with a little bit of JS and an extra data-value attribute, I could have the datalist work with a given list and have my form process the respective values through the data-value attribute.

At that stage I was pretty happy with my simple solution and thought that Datalists were pretty useful and quick to implement. It lasted for a few minutes before I realised that their default format was not quite to my liking and that very little was doable through CSS. So much for the miraculous native and out-of-the-box solution.

The stimulus Datalist look-alike

Here is my codepen version of it :

Forgive the poor CSS styling but it wasn't the main focus of this post (that is actually my go-to excuse for my extraordinary lack of design sense). Anyway, I'll explain below the different building blocks of the codepen assuming a basic knowledge about Stimulus.

The HTML bit

If you have heard about Stimulus, you probably know that it revolves around three main elements: Controller, Target and Action. Those are defined as data-attributes in your HTML and allow Stimulus controllers to access given targets and perform given actions on specific DOM events.

<div data-controller="datalist" class="datalist-container">
  <input data-target="datalist.input" data-action="focus->datalist#showOptions 
                      input->datalist#filterOptions 
                      keydown->datalist#keyboardSelect" type="text" name="player_form[player]" id="player_form_player" value="">
  <ul data-target="datalist.list" class="custom-datalist">
    <li class="show" data-value="1" data-action="click->datalist#selectOption">Andre Rublev</li>
    <li class="show" data-value="2" data-action="click->datalist#selectOption">Andre Agassi</li>
    <li class="show" data-value="3" data-action="click->datalist#selectOption">Pete Sampras</li>
    <li class="show" data-value="4" data-action="click->datalist#selectOption">Roger Federer</li>
    <li class="show" data-value="5" data-action="click->datalist#selectOption">Rafael Nadal</li>
    <li class="show" data-value="6" data-action="click->datalist#selectOption">Novak Djokovic</li>
    <li class="show" data-value="7" data-action="click->datalist#selectOption">Stefan Edberg</li>
    <li class="show" data-value="8" data-action="click->datalist#selectOption">Stefanos Tsitsipas</li>
  </ul>
</div>
Enter fullscreen mode Exit fullscreen mode

In this particular case, we first set the data-controller attribute "datalist" on the outer div element (data-targets and data-actions of a given controller must be defined or at the level of the data-controller attribute itself or in any of its descendants).

As we will need to access both the input and ul elements, we add data-target attributes to them, respectively "input" and "list".

Finally, we add the data-action attributes, most of them being on the input element. By default, the datalist is not visible and we want to show it on the focus DOM event. We also want to filter the options of the datalist depending on what is typed (hence the input DOM event) and be able to select the relevant option using the keyboard (hence the keydown DOM event). The last data-action attribute that needs to be defined is on the various options themselves to actually be able to select them (hence the click DOM events).

With the HTML all set up, we're ready to move on to the JS part and replicate the behavior of a Datalist.

The JS bit

We first import useClickOutside from stimulus-use as we will use it to hide the Datalist options whenever the user clicks outside of it.

Then we need to define the targets that we will be using as below:

static get targets() {
    return ["input", "list"];
}
Enter fullscreen mode Exit fullscreen mode

We use Stimulus' lifecycle callback method connect() to add the clickOutside behavior to our Stimulus controller and set the currentFocus variable as -1 (default value we'll use to have no visible focus).

Then we will define the various methods that we need to fully replicate the datalist behavior:

filterOptions()

filterOptions() {
  this.listTarget.classList.add("show");
  const text = this.inputTarget.value.toUpperCase();
  let options = this.listTarget.children;
  for (var i = 0; i < options.length; i++) {
    if (options[i].innerHTML.toUpperCase().indexOf(text) != -1) {
      options[i].classList.add("show");
    } else {
      options[i].classList.remove("show");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

What this method does is to first show the datalist and then put in capital letters (so that the case is not relevant) what was typed into the input element and compare it to each option of the list. If there is a match in whichever part of the option, then show it. Otherwise hide it.

showOptions()

showOptions() {
  this.listTarget.classList.add("show");
}
Enter fullscreen mode Exit fullscreen mode

Simply used to show the datalist.

clickOutside()

clickOutside(event) {
  this.listTarget.classList.remove("show");
  this.focus = -1;
}
Enter fullscreen mode Exit fullscreen mode

If the user clicks outside of the datalist, hide it and re-initialize the focus variable.

selectOption()

selectOption(event) {
  this.inputTarget.value = event.currentTarget.innerHTML;
  this.listTarget.classList.remove("show");
}
Enter fullscreen mode Exit fullscreen mode

If an option is selected, put its value into the input element and hide the datalist.

keyboardSelect()

keyboardSelect(event) {
  const options = Array.from(this.listTarget.children).filter((option) => option.classList.contains("show"));
  if (!options.length) return;
  if (event.keyCode == 13) {
    event.preventDefault();
    if (this.focus > -1) {
      options[this.focus].click();
    }
  } else if (event.keyCode == 40) {
    this.focus++;
    this.putFocus(options);
  } else if (event.keyCode == 38) {
    this.focus--;
    this.putFocus(options);
  }
}
Enter fullscreen mode Exit fullscreen mode

First, extract the available options from the datalist (i.e. those that are shown after applying the filterOptions() method). If there is no available option, exit the method. Otherwise, depending on the key pressed, select the option which has the focus on or shift the focus down/up.

putFocus()

putFocus(options) {
  this.removeFocus(options);

  if (this.focus >= options.length) {
    this.focus = 0;
  } else if (this.focus < 0) {
    this.focus = options.length - 1;
  }

  options[this.focus].classList.add("focus");
  options[this.focus].scrollIntoViewIfNeeded(false);
}
Enter fullscreen mode Exit fullscreen mode

We first need to remove prior existing focus. However, since the available options list vary depending on what is typed by the user, we cannot use the focus index to remove it directly. Instead, we loop through the available options and remove the focus on whichever one has it.

Then we control for "out-of-bounds" scrolling to shift the focus from the first element to the last in case of pressing "Up" and the opposite when pressing "Down".

Finally, in order to have a smooth scrolling experience if the list is long, we use the scrollIntoViewIfNeeded method.

removeFocus()

removeFocus(options) {
  for (var i = 0; i < options.length; i++) {
    options[i].classList.remove("focus");
  }
}
Enter fullscreen mode Exit fullscreen mode

Remove the focus in the available options list.

The CSS bit

All of this was just so that you can customize the design of your datalist, so that part's up to you now !

Top comments (0)