Custom Datalist with Stimulus
Tags: codepen, javascript, stimulus
Published: 2021-12-23 20:26:06

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.

```html
<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>
```

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

```html
<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>
```
Needless to say that I was quite underwhelmed when I saw the actual output in Chrome.


![Datalist in Chrome](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/89ooltgpkgaq0y2870z6.png)

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 :

{% codepen https://codepen.io/jrollus/pen/OJxxNxY %}

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.

```html
<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>
```

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:

```javascript
static get targets() {
    return ["input", "list"];
}
```
We use Stimulus' [lifecycle callback method](https://stimulus.hotwired.dev/reference/lifecycle-callbacks) **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()

```javascript
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");
    }
  }
}
```
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()
```javascript
showOptions() {
  this.listTarget.classList.add("show");
}
```
Simply used to show the datalist.

#### clickOutside()
```javascript
clickOutside(event) {
  this.listTarget.classList.remove("show");
  this.focus = -1;
}
```
If the user clicks outside of the datalist, hide it and re-initialize the focus variable.

#### selectOption()
```javascript
selectOption(event) {
  this.inputTarget.value = event.currentTarget.innerHTML;
  this.listTarget.classList.remove("show");
}
```
If an option is selected, put its value into the **input** element and hide the datalist.

#### keyboardSelect()
```javascript
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);
  }
}
```
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()
```javascript
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);
}
```
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()
```javascript
removeFocus(options) {
  for (var i = 0; i < options.length; i++) {
    options[i].classList.remove("focus");
  }
}
```
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 !