LiveSelect - Dynamic selection input component for LiveView

You’re over complicating it :slight_smile: see my previous response. Treat it as any other input.

You shouldn’t need to set value at all in your scenario AFAICS

That was my first attempt, as my resource preloads the tags, however live select errors out because it requires that the selected values are:

atoms, strings or numbers, maps or keywords with keys: (:label, :value) or (:key, :value) and an optional key :tag_label
2-element tuples

And they are not because they are structs like %Tag{}.

And they are not because they are structs like %Tag{}.

then you should convert them :upside_down_face: Enum.map/2 is your friend here.

I mean, what would you do if it was an ordinary select?

The select case is a little different because there is a 1:1 mapping only but I got your point, and it helped me find the solution. I was able to simplify my form by having the current tags mapped as two-element tuple in the to_form from the changeset. Then, before saving, as the params will only contain the id and name, I can have a lookup, and set the tags for the internal API that uses Ecto put_assoc.

Thanks for your help.

1 Like

Great! Glad it worked out.

I should probably add an example in the docs that explains this kind of scenario.

1 Like

Is there a way we can handle many_to_many relations as described in Polymorphic associations with many to many — Ecto v3.10.3?

Hi @subbu ! LiveSelect is just an input component for forms, it’s unrelated to Ecto or Ecto associations. It doesn’t “handle” any associations :slight_smile: .

Like any other input component, you can use it to let the user select stuff. What you do with the selected data is up to you.

Thanks. LiveSelect isn’t sending events to the FormComponent, instead they are being sent to the parent live_view inspite of having a phx-target:

in FormComponent
<.live_select
  phx-target={@myself}
  field={@form[:tags]}
  mode={:tags}
/>

in index.html.heex
<.live_component module={FormComponent} />

Here :tags is a many_to_many with a

cast_assoc(:tags, with: &Tag.changeset/2)

in the parent schema. Any idea what I am missing?

I don’t see anything wrong here. Which event are we talking about, the live_select_change event or the form’s phx-change event? Is phx-target also set in the form?

More code would be helpful.

phx-target is set on form-element as well.

<.simple_form
  for={@form}
  id="prompt-form"
  phx-target={@myself}
  phx-change="validate"
  phx-submit="save"
>
  <.input field={@form[:text]} type="textarea" label="Prompt" />
  
  <.live_select
    phx-target={@myself}
    field={@form[:tags]}
    mode={:tags}
    style={:tailwind}
    value={Enum.map(@form[:tags].value, &{&1.name, &1.id})}
    options={[{"Legal", 1}, {"Vendors", 2}]}
  />
  <:actions>
    <.button intent="primary" phx-disable-with="Saving...">Save Prompt</.button>
  </:actions>
</.simple_form>

The above code actually errors out with:

invalid element in selection: %Tag{__meta__: ..

If I change to field={@form[:tag]} (tag is a virtual field) it works but it sends the events to the parent index:

[debug] HANDLE EVENT "change" in PromptLive.Index
  Component: LiveSelect.Component
  Parameters: %{"_target" => ["prompt", "tag_text_input"], "prompt" => %{"tag_text_input" => "s"}}
[debug] Replied in 484µs
[debug] HANDLE EVENT "blur" in PromptLive.Index
  Component: LiveSelect.Component
  Parameters: %{"value" => "s"}
[debug] Replied in 2ms

You say you expect the events to be sent to a LiveComponent and not to the parent LiveView.

Where is this LiveComponent?

in index.html.heex
<.live_component
    module={PromptLive.FormComponent}
    id={@prompt.id || :new}
    title={@page_title}
    action={@live_action}
    prompt={@prompt}
    patch={~p"/prompts"}
  />

when you change the text in the “textarea” field, is the change event being sent to FormComponent as expected?

Is the LiveSelect hook loaded? Do you say errors in the JS console?

Yes. validate event gets triggered on FormElement when I change the textarea. Here are the hooks:

in hooks.js
import live_select from "live_select"
let Hooks = {};

Hooks.Tippy = {...}
Hooks.LiveSelect = live_select;

export default Hooks;

that doesn’t tell me that the hooks are being passed to the LiveSocket constructor, but let’s assume they are.

In any case, the “change” event on the text input field should go to FormComponent and not to the parent live view even if the hooks are not there.

Are you on the latest version? Are you sure this is the only live select on the page and there isn’t one in the parent liveview triggering the event?

If the answer is yes to both question, the only thing I can suggest is to open an issue on github and share more code.

Yes, this is the only live_select. In any case I’ll try and find the bug, if not I’ll open an issue in github. Thanks for your patience :smiley:

1 Like

Is there a way to prevent the dropdown panel from closing when a selection is made? For example, the user types a text, then wants to select multiple items in quick succession from the list.

Not at the moment, but it should be pretty easy to add an option for that behavior. If you open an issue, I’ll work on it as soon as I have some time. Or you can take as stab at it yourself if you want to. Thanks!

Thanks, I opened an issue on Github. In the short term, is there a way to inspect the HTML markup of the dropdown menu? It automatically closes when using the Inspect tool in Chrome!

Speaking of testing, what is the testing strategy for a component that uses LiveSelect?

Specifically, I’d like to write a test with the following steps:

  1. Click on the input and type “Ala”
  2. Assert that the dropdown field contains states with the text, e.g. “Alabama”
  3. Click “Alabama”
  4. Assert that “Alabama” got added to the selected_states assign
  5. Click the X on the “Alabama” tag
  6. Assert that “Alabama” got removed from the assign