LiveSelect - Dynamic search and selection component for LiveView

Hi! :wave:

I would like to present LiveSelect, a little library that I wrote to easily add a search and selection input component to your LV forms.

Github
Hexdocs

The idea is that the user can type some text, and the component presents a dropdown with content that is filled dynamically by your LV as the user types (LiveSelect sends your LV a message, and your LV replies with the new list of options). The user can then select an option or continue the search.

demo

Background

There are a few (mostly oldish) tutorials that explain how to build similar components, but to the best of my knowledge no component you can add to your LV as a library and just use.

While creating this component for one of my projects, I put considerable effort trying to get it right, especially handling all the tiny little details like navigation with the arrow keys, selection with mouse or enter key, resetting the selection and so on. Therefore, I decided to make it all available as an easy-to-use library so that hopefully the next poor developer won’t have to reeinvent this wheel :wink:

How to use

To use LiveSelect, you add it to your mix dependencies, import the JS hooks (1-2 lines) into your app.js, and add an extra line in your tailwind configurations. You’re then ready to add the LiveSelect input to your forms. For now, the out-of-the-box styles require daisyUI, but I plan to add vanilla Tailwind styles soon.

I also plan to add a way to customize the layout of the options in the dropdown using slots.

I would be very happy if at least some folks found this component useful, and I’ll be extremely grateful for any feedback :pray:

Thanks,
Max

24 Likes

This looks great! Thanks for releasing it.

Do you have any plans/thoughts about exposing it as a component when LiveView 0.18 hits? Link to new attr syntax (not sure if there’s better docs available as of now).

1 Like

This Looks great. Thanks for releasing it!

I am curious, what does this do?

1 Like

This is so great, I was hoping for such effort since a while now.
Does it handle multiselect already, or anyway you plan to implement it?

3 Likes

Thanks @zachallaun !

Interesting, I haven’t been following the development of the 0.18 version that much to be honest, so thank you for the pointer. Looks like LV will soon incorporate some features reminiscent of Surface. I was expecting that and I find it good :+1:

To answer your question: my initial idea was to provide an interface as similar as possible to Phoenix.HTML.Forms, because I beleive this is how most people write LV forms:

<%= live_select form, field, options %>

However, as I mentioned, I would like to make the rendering of the options in the dropdown customizable. The most natural way to do that seems to be using slots, and so I believe I’m gonna have to provide a second interface that uses function components. Let’s say you wanna display all the option labels in uppercase (silly example but just to make the point):

<.live_select form={form} field={field} options={options} let={{label, _value}}>
  <div><%= String.upcase(label) %></div>
</.live_select>

So the answer to your question is: “probably yes”.

I’m interested in any thoughts you might have

Hi @TwistingTwists and thanks.

The LiveSelect component renders 2 inputs: a visible one where the user enter text and that will contain the label of the option after selection, and a hidden one that will contain the actual value of the option after selection.

Let’s say you do this in your form:

<%= live_select form, :user_role %>

This will render 2 inputs: a visible text input called :user_role_text_input and a hidden one called :user_role

If now you pass the options: %{user: 1, admin: 2, guest: 3}, LiveSelect will render 3 labels in the dropdown: ["user", "admin", "guest"]. If you select one of them, the corresponding numeric value (1, 2 or 3) will be the value of the hidden input :user_role, whereas the selected option label (“user”, “admin” or “guest”) will be the value of the text input :user_role_text_input.

The label and the values of the options could also be the same of course (i.e. options = ["user", "admin", "guest"]), in which case both :user_role and :user_role_text_input will have the same value.

Hope this makes sense

1 Like

Hi @ivanminutillo and thank you!

you raise a very good point, and I confess I had not given this too much thought until I read your question :slight_smile:

I think that a good way to handle multiple selects is to do what this jQuery library is doing:

https://harvesthq.github.io/chosen/

Basically adding removable tags to the input field as the user selects options.

What do you think?

Right on this makes sense.

I’ve had very contrived examples of trying to do the same in Angular in past. This approach is relatively breeze.

Thanks for explaining this one!

1 Like

This is awesome !! Thank you.
A couple of suggestions if I may …

  1. commenting out the phx-change on the component (component.html.heex) allows for the event to fire. For some reason, I was not seeing the handle_event triggering (only the handle_info callback).
  2. a bit of a hack, but I needed to have three LiveSelect components on the same form with “intelligence” i.e. the search criteria and the options displayed were interdependent. Hence, I needed the values of all three components passed into handle_info (for determining the options to display) when any of them changed. This is similar to the “grouping” function of checkboxes and how the values are passed into liveview callbacks. I was able to accomplish this with a few tweaks … basically, am using the handle_event to collect the key value pairs as all form component values are passed to handle_event upon change. I then pass this back as an assign and onto the LiveSelect component (had to add a “values” attribute in ChangeMsg) which then shows up nicely in the handle_info callback. I can submit the PR on github if you think worthy …
  3. One side-effect, for handle_events to be generated reliably, I had to also remove the debounce (handle-info always fires).
  4. Do you think it is possible to adapt this so it can work within a live_component ? I am still learning …

Again, thank you.

1 Like

Thank you @milangupta this is excellent feedback! I’m currently traveling and unable to answer. Expect a proper response over the weekend :+1:

So as I dug deeper into the design, I realized I didn’t need to use handle_event triggered by phx-change at all, so 1 & 3 are not needed. The “grouping” equivalent can be accomplished over the top. Your widget is quite elegant and carefully designed. There is a lot of work that has gone in to make it behave precisely …

The one thing I struggled with is the interface/integration with the parent - I had to implement a new message to be able to get the “selection” event in the parent. I did not see a way for the parent to determine whether selections had been made.

For complex/tightly integrated interactions, is there an easier way to be able to get to the state of the component i.e. directly accessing variables ?

A quick answer from the road on this one. This is explained in the docs:

Whenever an option is selected, LiveSelect will trigger a standard phx-change event in the form. See the “Examples” section below for details on how to handle the event.

A live select input behaves like an ordinary select input, the change event is triggered when the user selects one of the options. No need to implement new messages. Let me know if this isn’t explained clearly enough in the docs or if you’re still struggling.

I assume you’re refering to this. This is intentional: you don’t want the change event in the form to fire whenever the user types something in the text input, but only when they select an option. Handling the ChangeMsg message is the proper way to be notified of and react to changes in the text input.

This is an interesting use case and, as you apparently already found out, is best handled in the parent LV by keeping track in the assigns of what is selected in each liveselect input and using this state to determine the options to send to a liveselect when the ChangeMsg message is received.

Interesting use case I didn’t think about. I never nest live components, but I can imagine how this could be desirable in some (perhaps rare) cases. The problem here is that messages can’t be handled in live components, only events can, and ChangeMsg is a message. Maybe one way would be to turn ChangeMsg into a custom event and route it to the parent component using phx-target. The parent component could communicate its “address” to live select using the @myself assign. Just an idea, will think about it. Let me know what you think.