Sandro Turriate

Coder, cook, explorer

Using a datalist for typeahead with htmx

Jul 27, 2023

Implement typeahead using the HTML datalist element with a little help from htmx.
AKA ActiveSearch.

The <datalist> element

The datalist element allows you to provide a list of recommended options as the value for an input field. The browser natively displays this list as a scrollable dropdown that can be selected with the mouse. This element is enough to implement a browser native typeahead without any added JS packages or styling.

picture of datalist dropdown
Datalist dropdown courtesy of MDN

First implementation

htmx is a small javascript library that champions using the backend for all UI state. At it’s core, it allows you to add http requests to any element, and swap the resulting html in-place, or anywhere else on the page. It is a little like pjax, though with more precision. We will use htmx to swap out the datalist options shown on the page.

The first implementation of this feature uses two htmx attributes: hx-get and hx-target. These attributes will request HTML from the server and put the response onto the page.

hx-get will send a GET request to the provided URL. The response should be HTML with a list of <option> elements, filtered by the provided input value. The GET request will have a querstring like ?wordInput=app when typing “app” (first three letters of “apple”) into the input field. By default, the request is sent when the input’s change event fires.

Set hx-target to a css selector for an element on the page. When the HTML response is returned, the html will be placed inside of the target element.

1
2
3
4
5
6
7
<input
  list="wordList"
  name="wordInput"
  hx-get="/typeahead"
  hx-target="#wordList"
/>
<datalist id="wordList"></datalist>
First implementation updates the datalist on “change” event.

Search while you type

The first implementation has a problem, it doesn’t “typeahead”. By default, htmx only triggers HTTP requests on the change event for input elements. We can easily fix this by using the hx-trigger attribute. This tells htmx to trigger the GET request on keyup.

1
2
3
4
5
6
7
8
<input
  list="wordList"
  name="wordInput"
  hx-get="/typeahead"
  hx-target="#wordList"
  hx-trigger="keyup"
/>
<datalist id="wordList"></datalist>
Show datalist on “keyup” events.

Fixing the double render on select

Now we have a new problem, which I believe is inherent in datalist. When an option is selected from the datalist, a keydown → input → change → keyup event cycle fires on the input element.

This retriggers the htmx event which sends a duplicate, unncessary request to the server. Thankfully, we can write a small function to prevent hx-trigger from firing in certain cases. htmx refers to this as a condition.

When the datalist sends the event to the input field, it looks different than the normal keydown event. Instead of being an instance of KeyboardEvent, it’s an instance of the generic Event type. By checking for this, we can prevent the double render. See the final code and full demo below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<script>
function checkUserKeydown(event) {
  return event instanceof KeyboardEvent
}
</script>

<input
  list="wordList"
  name="wordInput"
  hx-get="/typeahead"
  hx-target="#wordList"
  hx-trigger="keyup[checkUserKeydown.call(this, event)] changed delay:25ms"
/>
<datalist id="wordList"></datalist>
Typeahead search with datalist

Full demo

Note, search results aren’t actually filtered in this demo because the backend is simply a static page. To fully implement typeahead search, have your backend check for the wordInput query parameter in the URL, then use that to filter the results.