DEV Community

vindarel
vindarel

Posted on

Server-based interactive autocomplete with Datastar (and Common Lisp)

Whenever I try a new web library, framework, component, I look how to implement this: a server-based interactive autocomplete / typeahead / select2-like search input. I want an ajax-based search input without writing any client-side JavaScript. That's my graal.

The user starts typing something, you start searching after 300 milliseconds, the widget calls your server, the search is done server-side (because you'll call a database and maybe filter millions of rows).

I have used AngularUI's typeahead (it was actually good :) ), I still use select2 (JQuery-based, you need a JS config, a jS post-process if needed, and an API endpoint that returns JSON (that's doable ;) ).

There's the native datalist, only it's very limited: on Firefox it only shows 6 results by default, you can't style each result. I want to display a little image in each result. It's also a client-side widget, but you can plug in HTMX or Datastar.

And I'm not really a front-end guy, so please share your favorite widget for this use-case! (Maybe ex-shoelace's Web Awesome combobox? It's proprietary though).

Anyway, let's have a quick recap how this works.

HTML - with the unknown Dream-UI

As I wandered into the HTMX Discord, looking for this exact widget, I found someone who made it possible: the person behind dream-html-ui. A "set of components" (it currently has 2) that work fine with HTMX, and Bulma CSS.

Here it is in action:

Grab its small JS file and use its dh-combobox component:

          <dh-combobox class=dropdown>
            <div class="control dropdown-trigger has-icons-left">
              <input class=input type="search"
                     id="q"
                     name="q"
                     autocomplete="off"
                     data-bind:search
                     data-on:input__debounce.500ms="@get('/search')"


                     placeholder="Type a number (one, two…)">
              <span class="icon is-small is-left">🔍</span>
            </div>
            <div class="dropdown-menu" role="menu" id="fruit-dropdown-menu">
                <div id="datastar-search-results" class="dropdown-content">
Enter fullscreen mode Exit fullscreen mode

We use two Datastar attributes:

                     data-bind:search
                     data-on:input__debounce.500ms="@get('/search')"
Enter fullscreen mode Exit fullscreen mode

This fires a signal named "search" on our /search endpoint.

We must return HTML that will replace our id="datastar-search-results" node.

Common Lisp and Datastar-cl

We use https://github.com/fsmunoz/datastar-cl (follow the links and it has a comprehensive guide)

That's our endpoint:

(easy-routes:defroute datastar-dream-ui ("/search") ()
  (let* ((signals (datastar-cl:read-signals hunchentoot:*request*))
         (q (gethash "search" signals)))
    (log:info signals)
    (when (and signals
               (str:non-blank-string-p q))
      (let ((html (render-datalist q)))
        (log:info html)
        (datastar-cl:with-sse (resp hunchentoot:*request*)
          (datastar-cl:patch-elements resp html))))))
Enter fullscreen mode Exit fullscreen mode

They key parts are: read-signals, patch elements with datastar-cl:with-sse and patch-elements. We return HTML with our render-datalist function.

(defun render-datalist (q &optional (n 50))
  "Return HTML (string): a button by matching result.

  Each button displays a small image.

  The HTML fits in dream-ui's Bulma dropdown style."
  (with-output-to-string (s)
    (format s "<div id=\"datastar-search-results\" class=\"dropdown-content\">")

     (loop for option in (gen-data n)
          for i = 0 then (incf i)
          when (str:containsp q option)
            collect (markup:write-html-to-stream
                <button class="dropdown-item"
                        tabindex=i
                        value=option
                        role="menuitem">
                  <table>
                    <tr>
                      <td>
                        <img src="https://lispcookbook.github.io/cl-cookbook/orly-cover.png"
                             style="max-width: 50px"/>
                      </td>
                      <td style="padding-left: 5px">
                        ,(progn option)
                      </td>
                    </tr>
                  </table>
                </button>
                s))

    (format s "~&</div>")
   ))
Enter fullscreen mode Exit fullscreen mode

Our fake search only filters a string like "one" inside a list of numbers (written in plain english) with:


(defun gen-data (&optional (n 50))
  (loop for i below n
        collect (format nil "~R" i)))
Enter fullscreen mode Exit fullscreen mode

Notice how we reference our DOM id we want to replace:

    (format s "<div id=\"datastar-search-results\" class=\"dropdown-content\">")
Enter fullscreen mode Exit fullscreen mode

And, that's it.

The snippet you can try: https://github.com/vindarel/bacalisp/blob/master/web-stuff/datastar-dream-ui.lisp

It's quickly thrown together. I'll surely update and fix obvious stuff later. Hey, it works.

They keyboard selection works. Press up and down keys, press Enter.


I have an interactive, server-based, select2-like search input that doesn't require any JavaScript.

I'll keep you updated when I use it in the outside…

Again, I'll be very grateful if you have front-end tips for this. Thanks.

Top comments (0)