I’ve been playing with using inputs_for to add associations using one form. Using this Phoenix.Component — Phoenix LiveView v1.0.10 I’ve managed to get things to work but only for one level e.g. If I have an Department has_many Employees I can do the following:
defmodule AcmeWeb.AcmeLive.New do
use AcmeWeb, :live_view
alias Acme.Catalog
alias Acme.Catalog.Department
def mount(_params, _session, socket) do
# form =
# %Department{}
# |> Catalog.change_department(%{employees: [%Employee{}]})
# |> Map.put(:action, :insert)
# {:ok, assign(socket, :changeset, form)}
{:ok, assign(socket, :changeset, Catalog.change_department(%Department{}))}
end
def handle_event("validate", %{"department" => department_params}, socket) do
changeset =
%Department{}
|> Catalog.change_department(department_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
def handle_event("save", %{"department" => department_params}, socket) do
IO.inspect(department_params, label: "Department Params")
case Catalog.create_department(department_params) |> dbg() do
{:ok, _department} ->
{:noreply,
socket
|> put_flash(:info, "Department created successfully.")
|> push_navigate(to: ~p"/acme/departments")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
def render(assigns) do
~H"""
<div class="max-w-4xl mx-auto py-8 px-4">
<h2 class="mt-6 text-left text-3xl font-extrabold text-gray-900">
Acme - New Department
</h2>
<.form :let={dept} for={@changeset} id="department-form" phx-change="validate" phx-submit="save">
<div class="mb-3">
<.input field={dept[:title]} type="text" label="Department Title" />
</div>
<%= if length(Ecto.Changeset.get_field(@changeset, :employees, [])) > 0 do %>
<h3 class="mt-6 text-left text-2xl font-extrabold text-gray-900">
Employees
</h3>
<% end %>
<.inputs_for :let={ef} field={dept[:employees]}>
<input type="hidden" name="department[employees_sort][]" value={ef.index} />
<div class="mb-3">
<.input field={ef[:firstname]} type="text" label="First Name" />
<.input field={ef[:middlename]} type="text" label="Middle Name" />
<.input field={ef[:lastname]} type="text" label="Last Name" />
<.input field={ef[:work_email]} type="email" label="Work Email" />
<.input field={ef[:profile_pic]} type="text" label="Profile Pic" />
<button
type="button"
name="department[employees_drop][]"
value={ef.index}
phx-click={JS.dispatch("change")}
class="px-4 py-2 rounded-md font-medium bg-red-700 text-white shadow-lg"
>
Remove
</button>
</div>
</.inputs_for>
<input type="hidden" name="department[employees_drop][]" />
<button
type="button"
name="department[employees_sort][]"
value="new"
phx-click={JS.dispatch("change")}
class="px-4 py-2 rounded-md font-medium bg-blue-700 text-white shadow-lg"
>
Add Employee
</button>
<div class="flex justify-end">
<.button phx-disable-with="Saving...">Save</.button>
</div>
<div class="flex justify-start">
<.link navigate={~p"/acme/departments"} class="ml-2">
<.button type="button">Close</.button>
</.link>
</div>
</.form>
</div>
"""
end
end
but if I then say an Employee has_many Skills and try to added a nested inputs_for inside the initial one for employees it doesn’t work.
defmodule AcmeWeb.AcmeLive.New do
use AcmeWeb, :live_view
alias Acme.Catalog
alias Acme.Catalog.Department
def mount(_params, _session, socket) do
# form =
# %Department{}
# |> Catalog.change_department(%{employees: [%Employee{}]})
# |> Map.put(:action, :insert)
# {:ok, assign(socket, :changeset, form)}
{:ok, assign(socket, :changeset, Catalog.change_department(%Department{}))}
end
def handle_event("validate", %{"department" => department_params}, socket) do
changeset =
%Department{}
|> Catalog.change_department(department_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
def handle_event("save", %{"department" => department_params}, socket) do
IO.inspect(department_params, label: "Department Params")
case Catalog.create_department(department_params) |> dbg() do
{:ok, _department} ->
{:noreply,
socket
|> put_flash(:info, "Department created successfully.")
|> push_navigate(to: ~p"/acme/departments")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
def render(assigns) do
~H"""
<div class="max-w-4xl mx-auto py-8 px-4">
<h2 class="mt-6 text-left text-3xl font-extrabold text-gray-900">
Acme - New Department
</h2>
<.form :let={dept} for={@changeset} id="department-form" phx-change="validate" phx-submit="save">
<div class="mb-3">
<.input field={dept[:title]} type="text" label="Department Title" />
</div>
<%= if length(Ecto.Changeset.get_field(@changeset, :employees, [])) > 0 do %>
<h3 class="mt-6 text-left text-2xl font-extrabold text-gray-900">
Employees
</h3>
<% end %>
<.inputs_for :let={ef} field={dept[:employees]}>
<input type="hidden" name="department[employees_sort][]" value={ef.index} />
<div class="mb-3">
<.input field={ef[:firstname]} type="text" label="First Name" />
<.input field={ef[:middlename]} type="text" label="Middle Name" />
<.input field={ef[:lastname]} type="text" label="Last Name" />
<.input field={ef[:work_email]} type="email" label="Work Email" />
<.input field={ef[:profile_pic]} type="text" label="Profile Pic" />
<button
type="button"
name="department[employees_drop][]"
value={ef.index}
phx-click={JS.dispatch("change")}
class="px-4 py-2 rounded-md font-medium bg-red-700 text-white shadow-lg"
>
Remove
</button>
<!-- Skills -->
<h4 class="mt-4 text-left text-xl font-bold text-gray-800">Skills</h4>
<.inputs_for :let={sf} field={ef[:skills]}>
<input
type="hidden"
name="department[employees][#{ef.index}][skills_sort][]"
value={sf.index}
/>
<div class="mb-3">
<.input field={sf[:name]} type="text" label="Skill Name" />
<.input field={sf[:level]} type="number" label="Skill Level" />
<.input field={sf[:description]} type="text" label="Skill Description" />
<button
type="button"
name="department[employees][#{sf.index}][skills_drop][]"
value={sf.index}
phx-click={JS.dispatch("change")}
class="px-4 py-2 rounded-md font-medium bg-red-700 text-white shadow-lg"
>
Remove Skill
</button>
</div>
</.inputs_for>
<!-- End Skills -->
<input
type="hidden"
name="department[employees][#{sf.index}][skills_sort][]"
value="new-skill"
/>
<button
type="button"
name="department[employees][#{sf.index}][skills_sort][]"
value="new-skill"
phx-click={JS.dispatch("change")}
class="px-4 py-2 rounded-md font-medium bg-blue-700 text-white shadow-lg"
>
Add Skill
</button>
</div>
</.inputs_for>
<input type="hidden" name="department[employees_drop][]" />
<button
type="button"
name="department[employees_sort][]"
value="new"
phx-click={JS.dispatch("change")}
class="px-4 py-2 rounded-md font-medium bg-blue-700 text-white shadow-lg"
>
Add Employee
</button>
<div class="flex justify-end">
<.button phx-disable-with="Saving...">Save</.button>
</div>
<div class="flex justify-start">
<.link navigate={~p"/acme/departments"} class="ml-2">
<.button type="button">Close</.button>
</.link>
</div>
</.form>
</div>
"""
end
end
I get multiple inner forms being created. Anybody tried this before with some success?