Elm Inside a Web Component
A Whiny Introduction
This is part 1 of a story I never wanted to write. I have heard over and over again something along the lines of, “don’t make a UI widget, leave that to the platform or to someone else.” Those people are right.
What kind of UI widget, you ask? It’s that white whale, a multi-select widget. There are a lot of them out there. If I had spent more time studying the ones that folks have already made, I would probably have been better off, but when you’re doing a project in your spare time (at least it started out that way), just building it is a lot more fun.
<much-select>
still has a lot of rough edges. APIs need to be cleaned up and documents need to be written, but in quite a few use cases, it works pretty well.
The Requirements
Why not use an existing multi-select widget?
I’m glad you asked. I had the following requirements and I could not find anything out there that met them. If you know of something, please let me know:
- It needs to be a Web Component (not a React library, not a jQuery plugin)
- It needs to be compatible with Elm, which means it needs to do all its DOM fiddling in the shadow DOM. The Elm runtime needs full control of the light DOM (on the part of the page it’s responsible for).
- It needs to have a set of features pretty close to Selectize
- Sorting options
- Filtering options as you type (fuzzy matching)
- No NPM dependencies (dev dependencies are cool)
- Style-able
A Note on Elm The way I’m going to discuss Elm might get confusing. This web component is being written in such a way that Elm can use it. It’s (largely) written in Elm; Elm on the inside, Elm on the outside, JavaScript in the middle.
However, you don’t need to use <much-select>
with Elm. In its shipped form, it’s just a web component. It’s a bunch of JavaScript that uses web platform APIs to do its thing; so you should be able to use <much-select>
with Vanilla JavaScript, React, Angular, Vue, whatever.
Failed Attempts
I made at least 2 different attempts to port the Selectize code over to a web component that fit my requirements. In both cases, I made some progress. My first attempt failed because… well… that was long enough ago I don’t remember. I probably kept starting new rounds of refactoring without finishing up my previous ones and dug myself into a hole I couldn’t climb out of.
My second attempt (which I remember a little better) started out strong. I started with the latest version of Selectize (at the time) and started refactoring. I got my web component working with single-item selection. I had dropped the jQuery dependency and I was using ES6 features. It got far enough along that it was even used in production. Then I tried to take the next step and support multi-select, and the wheels came off. My assessment is that I am just not smart enough or disciplined enough of a developer to port over the Selectize code base. Hats off to the authors and maintainers of Selectize, y’all are a lot smarter than I am.
My failures helped me understand the complexity of the problem though, so when I was ready to try again, I decided to go with “the power tools"; types and functional programming. I started out with TypeScript, but I got frustrated with my functional library options. I was deep into Elm at this point. I had initially dismissed Elm, since I wasn’t sure if I could (or should) put Elm in the shadow DOM of a web component, and I wasn’t sure if the overhead of the Elm runtime would be worth it (I’m still a little on the fence about that, but time will tell).
Why Elm
I love Elm. It’s great. I am not the smartest or the fastest developer, but after several months of hard work and the help of kind mentors, I hiked up the functional programming mountain and descended into the fields of developer bliss.
As of this writing, I have only really done functional programming in 2 languages: JavasScript and Elm. I know there are a lot of functional and typed languages I have not tried yet, but for what I’ve wanted to do (at least so far), these have been adequate.
Elm has many wonderful qualities, but there are 2 things that really help me.
First, the type system. It lets you “discover” the shape your data should be. I write some new features, then refactor, then write some more new features. And the type system assists a lot with knowing when I’m done refactoring. It’s not quite as simple as “once it compiles, it works”, but it’s usually not too far off.
The second helpful characteristic is its fast feedback loop. The compiler is exceptionally quick. I make my changes, and boom; I’ve got a compiler error or a “working” website that’s ready for me to test.
Still Too Much JavaScript
Saying there’s too much JavaScript might be a little nitpicky. But right now, at least according to Github, there’s more JavaScript in the much-select project than Elm. That is a bit misleading because I have some generated JavaScript in my code base. Still, there is a lot of JavaScript. I’m confident could move more of the logic out of the JavaScript layer and into Elm.
Some JavaScript is necessary for performance reasons, though. More on that later but an example is the ascii-fold.js
library. I found ascii-fold.js
in Selectize, and I think it originated in Sifter. It removes the diacritics from characters so the filtering options work smoothly. I found a library that did that in Elm, but it was super slow. The author of the package even provided a nice warning.
There is also not an “out of the box” way for Elm to live inside a web component. It turns out it is not that hard to wire everything up. Still, though, I can’t help but wonder if we couldn’t have something in the Browser package, called web component, that would handle a lot of the web platform web component API stuff and just provide us developers with nice types and data inside the Elm runtime where everything is pure sunshine and rainbows ☀️🌈.
Managing State
One of the big stumbling blocks for me when I was trying to port over the Selectize code to a web component was managing the state. I was really bad at it. Some state you want to keep in the DOM. Some, you don’t. It’s been long enough that I don’t remember the details, but I kept creating bugs. Eventually, I didn’t have much confidence, I knew what state the data should be in.
In Elm, there’s just 1 model. That’s where everything is. That model is complicated, and there is a lot of code written to manage that model, but I can focus on small pieces at a time. The tricky parts, in this application, have usually been about getting 2 lists of options into 1 list of options while following the right rules. In these more complicated cases, I can write unit tests until I’m confident things are working correctly.
Managing State and Performance
There will be more about performance, but Elm does not mutate data, which is great for my sanity but when dealing with loads of data, it’s easy for things to get slow. Let’s say there are a series of functions that are doing a series of operations on a list of options:
- Grouping them by their group
- Sorting them within their groups
I can write that in a way that’s slow and entails a lot of extra work for the computer (but made sense to me at the time). As long as I also wrote some unit tests that correctly captured the behavior, I can come back after getting a good night’s sleep, spend some time reading through the List.Extra
library, find a new way to solve the problem, iterate over the list fewer times, change data less frequently, and it’s way faster.
Evolving the Model
Elm’s type system has really helped me out, and it can probably help me out even more.
For instance, there are many places in the code where I have a list of options. As of the writing of this article, I’m just cramming all the things into this Option
type. This is fine, but when I have a list of options, all the compiler knows about it is that it is a list of options. Based on the bugs I’ve encountered while working on <much-select>
, I might think I know what options are in that list and how they got there. However, it is very easy to make mistakes about that. The type system could be telling me things like:
This list contains only options that should currently be visible in the dropdown, based on what the user has typed into the input.
This list of options has not been sorted yet; you’ll need to do that before displaying them.
This list of options contains new options we just got from “the outside”, so perhaps they were supplied by a REST API. We’ll want to ensure there are no duplicates with the existing options.
Helpful Elm Packages
tripokey/elm-fuzzy is a fuzzy search library that mostly just worked. I had to add a little bit of extra logic, but it’s great.
Gizra/elm-debouncer There is a lot of debouncing in much-select. Most of it is in JavaScript, but when I needed to do it in Elm, this package worked well.
ohanhi/keyboard and keyboard-events If you want to build a good UI widget, you have to support the keyboard.
mhoare/elm-stack I wanted to do some highlighting based on filtering. Once you start talking about tokens, groups, and stuff like that, well, I reach for a stack. There might be other, better ways, but that’s what I know.