Populate a Select field based on previous Select field

I have database tables: Companies, Devices, and Signs. The device belongs_to a Company and the Company has_many Signs.

I want to show on the new device form:

  • a select box of the existing Companies, and
  • a select box of the Signs that is populated based on the company selection.

The select box of the signs is dynamically changed if the selected company changed.

How to achieve this?

So far this is my device_controller.ex

defmodule PortalWeb.DeviceController do
  use PortalWeb, :controller

  alias Portal.Fleet
  alias Portal.Fleet.Device


  def create(conn, %{"device" => device_params}) do
    with {:ok, %Device{} = device} <- Fleet.create_device_customer(device_params) do
      conn
      |> put_flash(:info, "The device is created successfuly")
      |> redirect(to: Routes.page_path(conn, :index))      
    end
  end

  def new(conn, _params) do
    changeset = Fleet.change_device(%Device{})
    customers = Fleet.list_customers()
    |> Enum.map(&{"#{&1.name}", &1.id})
    render(conn, "new.html", changeset: changeset, customers: customers)
  end
end

Thank you.

You will have to use either JavaScript or LiveView for this. Any preference between these two options?

@lucaong do you have any advise how to achieve it by using LiveView?

Honestly I am not a LiveView expert (been meaning to give a detailed look into it soon), so hopefully someone else can pitch in :slight_smile:

Basically you would have a phx-change event on the form which will fire on every input change. Then in the handle_event callback you will pattern match on your Company select value and see if it changed from the one you have in your socket assigns. If it did, fetch the new Signs and re-assign. This will re-render your liveview automatically.

While there isn’t an example exactly like your use-case in it, phoenix_live_view_example is a good way to peek at how things work.

Later edit: found this (the “Bike comparison” example)

2 Likes

Thank you for the suggestion and example. I managed to populate the select field by using LiveView.

As suggested, I use phx-change that will pattern match the Company select value, then fetch the new Sign based on the value. Also assign the selected value to the Company select field.

This is part of my LiveView file.

  def render(assigns), do: LiveDeviceView.render("form.html", assigns)

  def mount(session, socket) do
    companies = Portal.Fleet.list_companies()
    selected_company = nil
    
    {:ok,
     assign(socket,
       changeset: Fleet.change_device(%Device{}),
       companies: companies,
       selected_company: selected_company,
       signs: [],
     )}
  end

  def handle_event("populate_sign", %{"device" => params}, socket) do
    signs = Fleet.get_company(String.to_integer(params["company_id"]))
    companies = Portal.Fleet.list_companies()
    changeset = Fleet.change_device(%Device{})
    selected_company = String.to_integer(params["company_id"])

    {:noreply, assign(socket, signs: signs, companies: companies, selected_company: selected_company, changeset: changeset)}
  end

form.html.leex

<%= f = form_for @changeset, "#", [phx_change: :populate_sign, phx_submit: :save] %>

  <%= label f, :company_id %>
  <%= select f, :company_id, Enum.map(@companies, &{&1.name, &1.id}), prompt: "Select a company", selected: @selected_company %>
  <%= error_tag f, :companies %>

  <%= if @signs == [] do %>
    <%= if @selected_company != nil do %> 
      <p>This company has no Sign</p> 
      <%= link "Add a new sign for this company", to: Routes.company_path(@socket, :new) %>
    <% end %>
  <% else %>
    <%= label f, :sign_id %>
    <%= select f, :sign_id, Enum.map(@signs, &{&1.name, &1.id}), prompt: "Select a sign" %>
    <%= error_tag f, :signs %>
  <% end %>

    <%= submit "Save", phx_disable_with: "Saving..." %>
  </div>

</form>

Thank you again for all responses.

2 Likes

Just chiming in several years later to say this helped me solve a similar frustrating issue I had. Thanks!