Combo-box
A dynamic, stylish and feature-rich combo-box as a HEEX component.
Remote options inside a form (with validate)
When using remote options inside a form with phx-change="validate", each validation re-renders the component. If the options assign is empty, the combo box can’t display the selected item’s label — it only has the raw value from form params. To fix this, maintain a separate assign that tracks the selected option across re-renders:
<.form for={@form} phx-change="validate" phx-submit="save">
<.combo_box
remote_options_event_name="search_products"
label="Product"
options={@selected_product_options}
placeholder="Search for a product..."
field={@form[:product_id]}
/>
<.button type="submit">Save</.button>
</.form>
def mount(_params, _session, socket) do
changeset = Licenses.change_license(%License{})
{:ok,
assign(socket,
form: to_form(changeset),
selected_product_options: []
)}
end
def handle_event("search_products", query, socket) do
results =
Products.search_by_name(query)
|> Enum.map(&%{text: &1.name, value: &1.id})
{:reply, %{results: results}, socket}
end
def handle_event("validate", %{"license" => params}, socket) do
changeset =
%License{}
|> Licenses.change_license(params)
|> Map.put(:action, :validate)
# Keep the selected product visible across re-renders
socket = maybe_track_selected_product(socket, params["product_id"])
{:noreply, assign(socket, form: to_form(changeset))}
end
def handle_event("save", %{"license" => params}, socket) do
socket = maybe_track_selected_product(socket, params["product_id"])
# ... your save logic here
end
# Looks up the product label once so the combo box can display it.
# Only fetches when the assign is empty (i.e. first selection).
defp maybe_track_selected_product(socket, product_id)
when product_id in [nil, ""],
do: socket
defp maybe_track_selected_product(socket, product_id) do
if socket.assigns.selected_product_options == [] do
product = Products.get_product!(product_id)
assign(socket, :selected_product_options, [{product.name, product.id}])
else
socket
end
end
The key insight: the combo box needs the options list to contain the selected item so it can render the label. Without it, the component only knows the value (e.g. a UUID) but not the display text.
Remote options search with a live component
By default the eventcombo_box_search is handled on the live view. But sometimes you may have your Combo Box in a live component and want that live component to contain the event handler. In this case you can tell the Combo Box to target the current live component using the @myself assigns:
<.combo_box
remote_options_event_name="live_component_combo_box_search"
remote_options_target={@myself}
label="Country"
placeholder="Search for a country..."
field={@form[:country]}
/>
The key addition is remote_options_target={@myself}. This value essentially gets passed along to the Javascript function pushEventTo(selectorOrTarget, event, payload, (reply, ref) => ...) as the selectorOrTarget param. See docs.
Create new option
The user can create a brand new option if they like.
<.combo_box
create
label="Pick your favourite fruit"
options={["Apple", "Banana", "Orange", "Pineapple", "Strawberry"]}
field={@form[:fruit]}
/>
Disabled
Disabled works like any other field.
<.combo_box
disabled
label="Pick your favourite fruit"
options={["Apple", "Banana", "Orange", "Pineapple", "Strawberry"]}
field={@form[:fruit]}
/>
Max items
Set the max number of items that can be selected.
<.combo_box
label="Pick your favourite fruit"
max_items={2}
options={["Apple", "Banana", "Orange", "Pineapple", "Strawberry"]}
field={@form[:fruit]}
/>
Placeholder
Placeholder text similar to a normal text input.
<.combo_box
placeholder="AI will take our jobs :("
label="Pick your favourite fruit"
options={["Apple", "Banana", "Orange", "Pineapple", "Strawberry"]}
field={@form[:fruit]}
/>
Option groups
<.combo_box
options={[Birds: ["Eagle", "Seagull"], Animals: ["Dog", "Rhino"]]}
label="Pick your favourite fruit"
field={@form[:fruit]}
/>
Plugins
You can toggle plugins and pass options to them.
See https://tom-select.js.org/plugins for available plugins. We have enabled two by default: “Remove button” and “Checkbox options” for multiple selects. You can disable them though:
Disable remove button
<.combo_box
tom_select_plugins={%{remove_button: false}}
multiple
label="Pick your favourite fruit"
options={["Apple", "Banana", "Orange", "Pineapple", "Strawberry"]}
field={@form[:fruit]}
/>
Drag and drop
Note that this requires jQuery and jQueryUI. We've made it so these libraries will automatically be fetched from a CDN if the `drag_drop` plugin is enabled.
<.combo_box
tom_select_plugins={%{drag_drop: true}}
label="Pick your favourite fruit"
options={["Apple", "Banana", "Orange", "Pineapple", "Strawberry"]}
field={@form[:fruit]}
/>
Custom Tom Select options
Simple options
We have two ways of providing Tom Select options. You can pass in a map to the attribute tom_select_options, which works for options of primitive types such as strings or numbers.
<.combo_box
tom_select_options={%{highlight: false}}
label="Pick your favourite fruit"
options={["Apple", "Banana", "Orange", "Pineapple", "Strawberry"]}
field={@form[:fruit]}
/>
Complex options
For complex options that require setting a Javascript function as an option, we have made it so you can reference a global Javascript variable that contains those options:
<script phx-update="ignore" id="customTomSelectOptions">
window.customTomSelectOptions = {
render: {
option_create: function(data, escape) {
return '<div class="create">Create new option for <strong>' + escape(data.input) + '</strong>…</div>';
},
no_results:function(data,escape){
return '<div class="no-results">No results found for "'+escape(data.input)+'"</div>';
},
}
}
</script>
<.h3 class="mt-10">Remote fetching</.h3>
<.combo_box
label="Fetch remotely"
id="fetch_remotely"
tom_select_options_global_variable="customTomSelectOptions"
multiple
remote_options_event_name="combo_box_search"
field={@form[:widget_category_names]}
help_text="The options are fetched remotely in your live view"
/>
In the above code you can see we set a global Javascript variable called customTomSelectOptions and then pass it to our combo box via the attribute tom_select_options_global_variable="customTomSelectOptions".
Note that we merge these options with any other options you pass in, so you can combine the tom_select_options and tom_select_options_global_variable methods of passing options.
Customization
You can copy the CSS into your own project:
cp deps/petal_framework/assets/css/combo-box.css ./assets/css
Then modify your `app.css` to point to it.
@import "tailwindcss/base";
@import "../../deps/petal_components/assets/default.css";
@import "combo-box.css"; /* <-- Add this line */
@import "tailwindcss/components";
@import "tailwindcss/utilities";
Now you can customize `combo-box.css` to your needs.
Note that we also use styles from Petal Components for the label, help text and errors.