A headless autocomplete for Elm
Every product needs an autocomplete, and every product needs a different autocomplete. One is a country picker that takes a single value. One lets a user pile up a dozen tags and never offer the same one twice. One renders as a list, one as a grid of avatars, one inside a table cell that must not grow. The markup is different, the styling is different, the keyboard behaviour is argued over in design review.
And underneath all of them sits exactly the same problem, solved wrong in exactly the same way.
The part that is actually hard
Type three characters quickly and a naive implementation fires three requests.
They come back out of order. The list flickers between the results for sin,
si and sing, and settles on whichever response was slowest — which is to
say, on the query the user has already stopped caring about. Add a spinner and
you now have a spinner that lies.
Debouncing, cancelling, keeping the response that matches the current query, knowing whether you are between keystrokes or genuinely waiting: that is the autocomplete problem. It has nothing to do with how the list looks, and it is identical in every one of those different-looking components.
So the package ships the logic and no view
futureworkz/elm-autocomplete
provides the state and the timing, and deliberately provides no view
functions at all. That refusal is the design.
You keep an Autocomplete a in your model, wire its Msg into your update,
and hand it one function — a fetcher:
type alias Choices a =
{ query : String -- what the user has typed
, choices : List a -- the previous list
, ignoreList : List a -- e.g. values already selected
}
fetcher : Autocomplete.Choices String -> Task String (Autocomplete.Choices String)
The package calls it when — and only when — the query has settled. Your fetcher
is a Task, so it can hit an HTTP endpoint, filter a list already in memory, or
consult two sources and merge them; the package neither knows nor cares. The
ignoreList is what makes multi-select fall out for free: pass the values the
user has already picked and they stop being offered.
What you get back is a value you render yourself, with your own markup, your
own classes, your own keyboard handlers. The package exposes Autocomplete for
the logic, Autocomplete.View for the plumbing a view needs, and
Autocomplete.Styled for elm-css
users — and stops there. Debouncing rides on
jinjor/elm-debounce,
because that problem was already solved by someone else.
Why this is the right shape in Elm specifically
A view-owning component in Elm has to guess your Html msg, your styling
approach and your event model, and every guess is a place where a real design
system will fight it. A view-less component has to guess nothing. It composes
with whatever you already have, including the parts of your app that were never
designed to accommodate it.
There is a general lesson here that we keep re-learning: the reusable part of a component is the part with no pixels in it. Timing, ordering, state transitions, "which request still matters" — that is library work. Appearance is product work, and product work does not want a library's opinion.
Elm 0.19, MIT, elm install futureworkz/elm-autocomplete. The repository's
examples/ directory has the single-value and multi-select versions in full.