Multi Select with Hotwire Combobox
Recently, I put some work into lowering the barrier to contributing talks to RubyEvents. Not entirely out of the kindness of my heart, I might add. As the organizer of the Vienna.rb Ruby Meetup, I contribute talk recordings every couple of months.
Previously, that meant updating a bunch of YAML files, and who enjoys manually updating a bunch of YAML? I certainly don’t.
Less YAML, More Forms
RubyEvents now provides a simple form where you can fill in event and talk details and automatically generate the needed YAML. Then you just copy paste and open a PR.
Part of that form is the selection of speakers. In the initial implementation, you would simply enter a comma-separated list of names.
Now, wouldn’t it be nice if you could search and get speakers auto-suggested? Yes, it would, and yes, indeed it is! Following Adrian Poly’s suggestion, I used Hotwire Combobox. This is what the final result looks like.
I’m not going to lie, implementation wasn’t as straightforward as I would have hoped. Hotwire Combobox isn’t huge on documentation - “just read the source code” is the motto here. If you’re as stumped as I was, this post may help.
Basics First
To simplify the input of speaker names and provide a pleasant user experience, we have a couple of requirements.
- We want to search for speakers by name
- We want to be able to select multiple speakers
- We must be able to add new speakers by entering arbitrary text
- Speaker names should be used in the final YAML
Let’s tackle these one by one. We start simple. By following the instructions in the README or Justin Searl’s excellent tutorial, we can get up and running quickly. Let’s add basic auto-completion for speaker names first.
# app/controllers/templates_controller.rb
def speakers_search
@speakers = Speaker.canonical
@speakers = @speakers.ft_search(search_query) if search_query
@speakers = @speakers.limit(100)
end
# app/views/templates/speakers_search.turbo_stream.erb
<%= async_combobox_options @speakers %>
# app/views/templates/new.html.erb -->
<%= form.combobox :speakers, speakers_search_templates_path %>
# app/models/speaker.rb
def to_combobox_display
name
end
This works great. We can search for and auto-complete speaker names.
Multi Selection with Hotwire Combobox
Now, let’s tackle multi-selection. Sadly, there are no pointers on how to implement this on the Hotwire Combobox Homepage. Time to go spelunking in the source code. Thankfully, there are tons of examples in the test/dummy app. One of them shows us how to add multi-selection.
We need to pass multiselect_chip_src
and use the combobox_selection_for_chips
helper to render selected items.
# app/views/templates/new.html.erb -->
<%= form.combobox :speakers,
speakers_search_templates_path,
multiselect_chip_src: speakers_search_chips_templates_path %>
# app/controllers/templates_controller.rb
def speakers_search_chips
@speakers = Speaker.find(params[:combobox_values].split(","))
render turbo_stream: helpers.combobox_selection_chips_for(@speakers)
end
Hotwire Combobox does the heavy lifting for us. We get a nice looking result without much of a fuss.
Free Text and Combobox Value
This solution already works wonderfully for existing speakers. However, remember that we also want to be able to add new speakers. To do so, we’ll need to add free_text
and name_when_new
.
We also need to make some modifications to the controller. A free-text name isn’t an actual speaker model - Hotwire Combobox can’t work with that. So we’ll need to do some manual mapping. This is based on these helpful examples in the source code.
# app/views/templates/new.html.erb
<%= form.combobox :speakers,
speakers_search_templates_path,
multiselect_chip_src: speakers_search_chips_templates_path,
name_when_new: "template[speakers]",
free_text: true %>
# app/controllers/templates_controller.rb
def speakers_search_chips
@speakers = params[:combobox_values].split(",").map do |value|
Speaker.find_by(id: value) || OpenStruct.new(to_combobox_display: value, id: value)
end
render turbo_stream: helpers.combobox_selection_chips_for(@speakers)
end
Now, the last missing piece is the ability to control the checkbox’s value. By default, Hotwire Combobox uses the ID of records. We allow free_text
, so our combobox values may be a mix of IDs and Strings.
<input
type="hidden"
name="template[speakers]"
value="4,2887,784,Your Name?"
id=...
>
To generate a YAML with speaker names, we are only interested in their names. Now, we could do some mapping in the controller, but there is a more straightforward way. We can provide a proc to the combobox helper to map the value from IDs to whatever we need.
# app/views/templates/speakers_search.turbo_stream.erb
<%= async_combobox_options @speakers, value: proc { |element| element.name } %>
If you are curious about more details, check out the PR to RubyEvents.