Does Liveview support multiple levels of nested inputs_for in one form?

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?

Should be name={"department[employees][#{sf.index}][skills_sort][]"} for the interpolation to work.

4 Likes

wow!! thanks this worked..