Edit or Add association in the same form

Hmm, inputs_for is usually used with the child schema e.g. a form for an employer would contain inputs_for employee. My guess is that the _persistent_id error you’re seeing is related to the dynamically adding and removing child inputs that inputs_for supports.

This can be tricky since when the parent does not exist, you’d usually want a form that returns something like this where the parent is at the top level e.g. what a form with inputs_for would return:

employer_params = %{
   "name" => "Ericsson",
   "employees" => [%{"name" => "John Doe"}]
 }

%Employer{}
|> cast(employer_params, [:name])
|> cast_assoc(:employees)
|> Repo.insert()

Whereas when the parent already exists, you’d want a form that returns something like this where the child is at the top level and a select input dropdown for the parent id.

employee_params = %{"name" => "Eric", "employer_id" => 1}

%Employee{}
|> cast(employee_params, [:name, :employer_id])
|> Repo.insert()

References:
Inserting associated records | Ecto Associations guide
Example: Adding a comment to a post | Ecto.Changeset.put_assoc/4 docs

Instead of changing the form struct itself, here’s an example of how you could set an assign that acts as a flag for whether the parent will be existing or new and how to save them together. Note that the code below is for a schema where a calendar has many events instead of an employer has many employees.

# inside the event form, render either a dropdown for the `calendar_id` field or a calendar form based on the boolean assign `@add_to_existing_calendar`
# this flag defaults to `true` and can be toggled by a button also within the event form `@form` via a `phx-click` binding

        <%= if @add_to_existing_calendar do %>
          <.input field={@form[:calendar_id]}
            type="select"
            label="Add to existing calendar:"
            placeholder="calendar"
            options={@calendar_options}
            prompt="-- select a calendar for this event --"
          />
        <% else %>
          <.simple_form for={@calendar_form} id="calendar-form">
            <.input field={@calendar_form[:name]}
              type="text"
              label="Add to new calendar:"
              placeholder="name"
            />
          </.simple_form>
        <% end %>
        <p>-- or --</p>
        <.button type="button" phx-click="toggle_calendar_source" phx-target={@myself}>
          <span :if={@add_to_existing_calendar}>Add to new calendar instead</span>
          <span :if={!@add_to_existing_calendar}>Choose existing calendar instead</span>
        </.button>
  def update(%{event: event} = assigns, socket) do
    event_changeset = Scheduling.change_event(event)
    new_calendar_changeset = Scheduling.change_calendar(%Scheduling.Calendar{})
    calendar_options = for cal <- Scheduling.list_calendars, do: {cal.name, cal.id}

    {:ok,
     socket
     |> assign(assigns)
     |> assign_form(event_changeset)
     |> assign(:add_to_existing_calendar, true)
     |> assign(:calendar_options, calendar_options)
     |> assign(:calendar_form, to_form(new_calendar_changeset))}
  end

  def handle_event("toggle_calendar_source", _, socket) do
    {:noreply, assign(socket, :add_to_existing_calendar, !socket.assigns.add_to_existing_calendar)}
  end

  def handle_event("save", %{"calendar" => calendar_params, "event" => event_params}, socket) do
    # this works thanks to `cast_assoc(:events)` for calendars
    calendar_params_with_event = Map.put(calendar_params, "events", [event_params])
    case Scheduling.create_calendar(calendar_params_with_event) do
      ...  
    end
  end

  # creating an event tied to an existing calendar via the dropdown works out of the box with the default `"save"` event handler below
  # just make sure `calendar_id` is added to `cast` for events
  def handle_event("save", %{"event" => event_params}, socket) do
    save_event(socket, socket.assigns.action, event_params)
  end

  defp save_event(socket, :edit, event_params) do
    case Scheduling.update_event(socket.assigns.event, event_params) do
      ...
    end
  end
2 Likes