justin․searls․co

HotwireCombobox is pretty damn slick

In a stroke of good fortune, this week's big, overriding to-do item was to figure out how to write a hotwire-friendly "combo box" (one of those drop-down / select boxes for the web that you can type into and filter the options). Then I happened to scan this week's Ruby Weekly and found somebody beat me to the punch!

It's by Jose Farias and he calls it HotwireCombobox. The documentation page contains plenty of demos, so go play with it!

The best part (and my favorite thing about moving to import maps for JavaScript in Rails 7) is that the front-end assets live with the gem, which means there's no risk of version drift causing the backend and front-end to fall out of sync with each other.

In fact, set up was so minimal, I'm going to share the entire changeset of what it took to convert my app's f.collection_select boxes over to f.combobox.

Install

Add the gem to your Gemfile and bundle it:

gem "hotwire_combobox"

Search your app for its stylesheet_link_tag (probably in a layout's <head> tag) and then just paste combobox_style_tag in above them. Mine now looks like:

<%= combobox_style_tag %>
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>

Add to_combobox_display to your models

I hate adding view-specific cruft to my Active Record models, but in the spirit of wanting to get this to work as quickly as possible, I threw this in my Equipment model, so that the title attribute is what's presented in the combobox's list:

class Equipment < ApplicationRecord
  #…
  def to_combobox_display
    title
  end
end

Swap out the select inputs

Next, I replaced each collection_select, like this:

<%= f.collection_select :equipment_id, Equipment.all.order(:title), :id, :title, include_blank: true %>

With a f.combobox, like this:

<%= f.combobox :equipment_id, Equipment.all.order(:title), include_blank: true %>

(As of March 29, 2024, that include_blank: true isn't working on its own, so I patched it in my custom TailwindFormBuilder).

Customize appearance

The hardest part was getting the combobox to look like my other custom form inputs. Since I'm using Tailwind, I targeted the classes and variables the gem exposes and overrode what I needed to inside @layer base {}.

If you don't use Tailwind, this will look like nonsense:

/* hotwirecombobox */

.hw-combobox__main__wrapper {
  @apply mt-1 rounded-md shadow-sm max-w-screen-md;
}

.hw-combobox__main__wrapper input:focus {
  @apply ring-0;
}

.hw-combobox__dialog__input {
  @apply rounded-lg focus:ring-0 focus:border-accent ;
}

:root {
  --hw-focus-color: rgb(var(--accent));
  --hw-border-width--thick: 1px;
  --hw-combobox-width: 100%;
  --hw-combobox-width--multiple: 100%;
}

That's it!

Note what I didn't include in any of the snippets above? No import map configuration. No registering Stimulus controllers in JavaScript. The gem handles it all on its own.

These sorts of UIs have always been a massive distraction in my experience, so having a component that's easy to drop in and plays nicely with your app's existing frontend is simply tremendous.

Thank you, Jose! 💜


Got a taste for hot, fresh takes?

Then you're in luck, because you can subscribe to this site via RSS or Mastodon! And if that ain't enough, then sign up for my newsletter and I'll send you a usually-pretty-good essay once a month. I also have a solo podcast, because of course I do.