Switch between dropdown/select and input in phoenix form

Hi,

I am trying to implement a form that provides the dropdown option of selecting an existing reference or creating a new one:

def render(assigns) do #[...] <.form for={@form} id="person-form" phx-change="validate" phx-submit="save"> <.input field={@form[:name]} type="text" label="Name" /> <div class="space-y-1"> <.input field={@form[:role_id]} type="select" label="Role" options={@role_options} phx-change="role-changed" /> <%= if @enable_new_role do %> <.input type="hidden" field={@form[:role_id]} value=""/> <.inputs_for :let={f_nested} field={@form[:role]} > <.input type="text" field={f_nested[:name]} placeholder={if @enable_new_role, do: "Enter new role name", else: "Select 'Add new role' above"} /> </.inputs_for> <% end %> #[...] def mount(params, _session, socket) do #[...] role_options = [ [key: "-- Select role --", value: "", disabled: true], [key: "+ Add new role...", value: ""] ] ++ Enum.map(Persons.list_roles(), &[key: &1.name, value: &1.id]) #[...] 

Based on the documentation I thought that a nil id for the reference should cause the creation of a new reference but I always run into:

cannot change belongs_to association `role` because there is already a change setting its foreign key `role_id` to `nil` 

An no matter what I tried (as manually removing or overwriting parameters, or different cast options) on save phoenix is always confused and either fails or overwrites the old reference.

Can someone propose a good way how to implement this? (Ideally at the form level since I would like to turn this into a re-usable live component.)

Thanks! :smiley:

1 Like

Skipping the hidden input might already be enough. You want to either manually provide changes to :role_id or use cast_assoc(:role), never both at the same time.

1 Like

Thanks for the hints, that definetly helped me stay on track: even with the id field(s) set to “” or nil cast assoc didn’t work as intended…

My current solution that seems to be working is removing the id field in the save handler:

 person_params = if person_params["role_id"] == "" do Map.delete(person_params, "role_id") |> pop_in(["role", "id"]) |> elem(1) else person_params end 

Then cast_assoc creates an insert and the logic stays in the form.