LiveSelect - Dynamic selection input component for LiveView

Hi @bennydreamtech23, please create an issue on GitHub, describing what you’re doing and what issues you’re encountering. The more details you add, the more likely someone will be able to help you. Thanks

1 Like

It been resolved now, the issue was with the hook. I had a file for external hooks so I didn’t add the live_select hooks but everything works fine now. Thank you

3 Likes

Hi all.

I am new to LiveSelect.
I am building an Admin App for Music.
It has Artists and Albums.
Every Album has an Artist.

The Album-Form is a LiveComponent.

I have zwo cases:

  1. I create an Album → I don´t have an Artist
  2. I edit an Album → I have an Artist
@impl true
  def update(assigns, socket) do
    {:ok,
     socket
     |> assign(assigns)
     |> assign_form()}
  end

defp assign_form(%{assigns: %{album: album}} = socket) do
    if album do
      # case 2
      artist = Tuneshg.Music.get_artist_by_id!(album.artist_id)
      form = Tuneshg.Music.form_to_update_album(album)
      # populate the LiveSelect with the name of the Artist
      send_update(LiveSelect.Component, id: "live_select_id", value: {artist.name, artist.id}) 
      socket
      |> assign(artist: artist)
      |> assign(form: to_form(form))
    else
      # case 1
      form = Tuneshg.Music.form_to_create_album()
      assign(socket, form: to_form(form))
    end
  end

Here the Form:

@impl true
  def render(assigns) do
    ~H"""
    <div>
      <.header>
        {@title}
        <:subtitle>Use this form to manage album records in your database.</:subtitle>
      </.header>

      <.simple_form
        for={@form}
        id="album-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
        <%= if @form.source.type == :create do %>
          <.input field={@form[:name]} type="text" label="Name" />
          <.input field={@form[:year_released]} type="number" label="Year released" />
          <.input field={@form[:cover_image_url]} type="text" label="Cover image url" />
          <.form :let={f} for={@form} phx-change="change">
            <.live_select
              field={f[:artist_id]}
              id="live_select_id"
              phx-target={@myself}
              label="Artist"
            />
          </.form>
        <% end %>
        <%= if @form.source.type == :update do %>
          <.input field={@form[:name]} type="text" label="Name" />
          <.input field={@form[:year_released]} type="number" label="Year released" />
          <.input field={@form[:cover_image_url]} type="text" label="Cover image url" />
          <.form :let={f} for={@form} phx-change="change">
            <.live_select
              field={f[:artist_id]}
              id="live_select_id"
              phx-target={@myself}
              label="Artist"
            />
          </.form>
        <% end %>

        <:actions>
          <.button phx-disable-with="Saving...">Save Album</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

When the user types into the Artist-field the is handle_event(“live_select_change” called

@impl true
  def handle_event("live_select_change", %{"text" => text, "id" => live_select_id}, socket) do
    artists =
      Tuneshg.Music.search_artists!(text)
      |> Enum.map(fn artist -> {artist.name, artist.id} end)

    send_update(LiveSelect.Component, id: live_select_id, options: artists)

    {:noreply, socket}
  end

When the submit botton is clicked, handle_event(“save” is called

def handle_event("save", %{"form" => form_data}, socket) do
    IO.inspect(form_data, label: "form_data")
    IO.inspect(socket.assigns.form, label: "form")

    case AshPhoenix.Form.submit(socket.assigns.form, params: form_data) do
      {:ok, album} ->
        notify_parent({:saved, album})

        socket =
          socket
          |> put_flash(:info, "Album #{socket.assigns.form.source.type} saved successfully")
          |> push_patch(to: socket.assigns.patch)

        {:noreply, socket}

      {:error, form} ->
        {:noreply, assign(socket, form: form)}
    end
  end

That works well for case 1 (Create an Album), although the Artist-Field flashes shortly, when the Artist name is inserted.
Is there a better way to do the insert?

My real problem is the case 2 (edit an exiting Album which already has an Artist.
I changed the Artist name from Heiko to “Zombie Kittens!!”

When I change the Artist and submit, the Artist is NOT changed!

I did an IO.inspect on the form_data and socket.assigns.form.
The form_data is correct:

form_data: %{
  "artist_id" => "f1cec0aa-50bd-4913-aad7-33b56df8b8eb",
  "artist_id_text_input" => "Zombie Kittens!!",
  "cover_image_url" => "",
  "name" => "Heikos Album",
  "year_released" => "2025"
}

The form looks this way:

form: %Phoenix.HTML.Form{
  source: #AshPhoenix.Form<
    resource: Tuneshg.Music.Album,
    action: :update,
    type: :update,
    params: %{
      "_unused_cover_image_url" => "",
      "_unused_name" => "",
      "_unused_year_released" => "",
      "artist_id" => "f1cec0aa-50bd-4913-aad7-33b56df8b8eb",
      "artist_id_text_input" => "Zombie Kittens!!",
      "cover_image_url" => "",
      "name" => "Heikos Album",
      "year_released" => "2025"
    },
    source: #Ash.Changeset<
      domain: Tuneshg.Music,
      action_type: :update,
      action: :update,
      attributes: %{},
      relationships: %{},
      errors: [],
      data: #Tuneshg.Music.Album<
        artist: #Ash.NotLoaded<:relationship, field: :artist>,
        __meta__: #Ecto.Schema.Metadata<:loaded, "albums">,
        id: "321b9718-79c0-404b-b5ed-0c43f2845aa2",
        name: "Heikos Album",
        year_released: 2025,
        cover_image_url: nil,
        inserted_at: ~U[2025-02-28 05:25:17.727489Z],
        updated_at: ~U[2025-03-01 09:27:11.285160Z],
        artist_id: "e71d98b5-2eee-478c-ad2d-cfcbd4d02f1f",
        aggregates: %{},
        calculations: %{},
        ...
      >,
      valid?: true
    >,
    name: "form",
    data: #Tuneshg.Music.Album<
      artist: #Ash.NotLoaded<:relationship, field: :artist>,
      __meta__: #Ecto.Schema.Metadata<:loaded, "albums">,
      id: "321b9718-79c0-404b-b5ed-0c43f2845aa2",
      name: "Heikos Album",
      year_released: 2025,
      cover_image_url: nil,
      inserted_at: ~U[2025-02-28 05:25:17.727489Z],
      updated_at: ~U[2025-03-01 09:27:11.285160Z],
      artist_id: "e71d98b5-2eee-478c-ad2d-cfcbd4d02f1f",
      aggregates: %{},
      calculations: %{},
      ...
    >,
    form_keys: [],
    forms: %{},
    domain: Tuneshg.Music,
    method: "put",
    submit_errors: nil,
    id: "form",
    transform_errors: nil,
    original_data: #Tuneshg.Music.Album<
      artist: #Ash.NotLoaded<:relationship, field: :artist>,
      __meta__: #Ecto.Schema.Metadata<:loaded, "albums">,
      id: "321b9718-79c0-404b-b5ed-0c43f2845aa2",
      name: "Heikos Album",
      year_released: 2025,
      cover_image_url: nil,
      inserted_at: ~U[2025-02-28 05:25:17.727489Z],
      updated_at: ~U[2025-03-01 09:27:11.285160Z],
      artist_id: "e71d98b5-2eee-478c-ad2d-cfcbd4d02f1f",
      aggregates: %{},
      calculations: %{},
      ...
    >,
    transform_params: nil,
    prepare_params: nil,
    prepare_source: nil,
    raw_params: %{
      "_unused_cover_image_url" => "",
      "_unused_name" => "",
      "_unused_year_released" => "",
      "artist_id" => "f1cec0aa-50bd-4913-aad7-33b56df8b8eb",
      "artist_id_text_input" => "Zombie Kittens!!",
      "cover_image_url" => "",
      "name" => "Heikos Album",
      "year_released" => "2025"
    },
    warn_on_unhandled_errors?: true,
    any_removed?: false,
    added?: false,
    changed?: false,
    touched_forms: MapSet.new(["_unused_artist_id_text_input",
     "_unused_cover_image_url", "_unused_name", "_unused_year_released",
     "artist_id", "artist_id_text_input", "cover_image_url", "name",
     "year_released"]),
    valid?: true,
    errors: true,
    submitted_once?: false,
    just_submitted?: false,
    ...
  >,
  impl: Phoenix.HTML.FormData.AshPhoenix.Form,
  id: "form",
  name: "form",
  data: #Tuneshg.Music.Album<
    artist: #Ash.NotLoaded<:relationship, field: :artist>,
    __meta__: #Ecto.Schema.Metadata<:loaded, "albums">,
    id: "321b9718-79c0-404b-b5ed-0c43f2845aa2",
    name: "Heikos Album",
    year_released: 2025,
    cover_image_url: nil,
    inserted_at: ~U[2025-02-28 05:25:17.727489Z],
    updated_at: ~U[2025-03-01 09:27:11.285160Z],
    artist_id: "e71d98b5-2eee-478c-ad2d-cfcbd4d02f1f",
    aggregates: %{},
    calculations: %{},
    ...
  >,
  action: nil,
  hidden: [
    _touched: "_unused_artist_id_text_input,_unused_cover_image_url,_unused_name,_unused_year_released,artist_id,artist_id_text_input,cover_image_url,name,year_released",
    _form_type: "update",
    id: "321b9718-79c0-404b-b5ed-0c43f2845aa2"
  ],
  params: %{
    "_unused_cover_image_url" => "",
    "_unused_name" => "",
    "_unused_year_released" => "",
    "artist_id" => "f1cec0aa-50bd-4913-aad7-33b56df8b8eb",
    "artist_id_text_input" => "Zombie Kittens!!",
    "cover_image_url" => "",
    "name" => "Heikos Album",
    "year_released" => "2025"
  },
  errors: [],
  options: [method: "put"],
  index: nil
}

Can anybody tell me please, what I am doing wrong and to fix my problem
or
Is there an easier way accomplishing the edit?
If yes, what do I have to do?

Thank you for your reply, Heiko

Hi Heiko,

This doesn’t look related to LiveSelect at all - it’s doing its thing and giving you the data in your params.

I’d recommend you to check the update action in your resource - if it’s the same as the one we wrote in the book, it doesn’t accept the artist_id attribute so it cannot be changed via an update.

1 Like

Hi Rebecca.

Of course, you were right :+1:

I added artist_id to the update action of the Album resource:

update :update do
      accept [:name, :year_released, :cover_image_url, :artist_id]
    end

and everything works.

I would never have looked there…
But I learned a lot by adding the LiveSelect component to my code :innocent:

So, thanks a lot again for your time and your effort…
Cheers, Heiko

1 Like

OMG !!! @trisolaran
I already wanted to take a week off for implementing something like this. Because I know.. building something like this is not easy… with so many options !!

THANK YOU ! That is just great !!!

I dropped it in, configured it with the Showcase App, asked a bit chatGPT and was done within 30 minutes!!!

That is awesome. great work!!! I appreciate !!!

did I already say, that it works like a charm ???
Great ! Thanks !!!

3 Likes

Hi everyone. Really appreciate this library and it’s been really useful and simple to use. I have found myself wanting slightly more complex use case that helps show the user a “Create option”, when we find something typed that does not exist. I’m using live_select with user_defined_options={true} to allow users to create new items by typing text that doesn’t match existing options but I feel like it could be better UX to make it more visual.

This works great, but I’m finding myself repeating the same pattern in every
```elixir

handle_event(“live_select_change”, …) handler:

def handle_event(“live_select_change”, %{“id” => “project_select”, “text” => text} = params, socket) do
  base_options = get_options(socket)
  filtered = filter_by_text(base_options, text)
  # Add "Create" option if no match
  options = if text != "" && !exact_match?(base_options, text) do
    [{"Create \"#{text}\"", text} | filtered]
  else
    filtered
  end

  send_update(LiveSelect.Component, id: params["id"], options: options)
  {:noreply, socket}
end

Is there a recommended pattern for making this reusable? Ideally, I’d like to configure which LiveSelects should have this behavior and have the logic happen automatically, rather than writing the same handler for each select.

Has anyone implemented something like a behavior module or component wrapper that handles this generically? I think this pattern trips me up because it wants to hook into the handle_event so I’m thinking of using attach_hook but I can’t use that since I’d need to make sure that the option does not exist