Managing has_many relationship within a liveview form

Hi,

I’m having some difficulty figuring out how to implement the following: I have the following resources:

defmodule WedEvents.Events.Invitation do
  actions do
    defaults [:read, :destroy, :update]
    default_accept [:invitation_code, :event_id]

    create :create do
      primary? true
      argument :invitation_users, {:array, :map}, allow_nil?: false

      change relate_actor(:created_by)
      change manage_relationship(:invitation_users, type: :create)
    end
  end

  attributes do
    uuid_primary_key :id
    attribute :invitation_code, :string
    timestamps()
  end

  relationships do
    belongs_to :event, WedEvents.Events.Event
    belongs_to :created_by, WedEvents.Accounts.User, allow_nil?: false

    has_many :invitation_users, WedEvents.UserEvents.InvitationUser,
      destination_attribute: :invitation_id
  end

  {...other fields}
end
defmodule WedEvents.UserEvents.InvitationUser do
  actions do
    defaults [:read, :destroy, :update]
    default_accept [:role, :invitation_id, :user_id]

    create :create do
      primary? true
      change relate_actor(:created_by)
      change relate_actor(:updated_by)
    end
  end

  attributes do
    uuid_primary_key :id
    attribute :role, :string, allow_nil?: false, default: "guest"
    timestamps()
  end

  relationships do
    belongs_to :created_by, WedEvents.Accounts.User, allow_nil?: false
    belongs_to :updated_by, WedEvents.Accounts.User, allow_nil?: false
    belongs_to :invitation, WedEvents.Events.Invitation, allow_nil?: false
    belongs_to :user, WedEvents.Accounts.User, allow_nil?: false
    has_one :rsvp, WedEvents.UserEvents.RSVP
  end
end

I have the following live component:

defmodule WedEventsWeb.InvitationLive.FormComponent do
  use WedEventsWeb, :live_component
  alias WedEvents.Events
  alias WedEvents.Accounts
  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.header>
        <%= @title %>
        <:subtitle>Use this form to manage invitation records in your database.</:subtitle>
      </.header>

      <.simple_form
        for={@form}
        id="invitation-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
        <.input field={@form[:invitation_code]} label="Invitation Code" />
        <.input field={@form[:event_id]} type="select" label="Event" options={@events} />
        <.input field={@form[:user_ids]} type="select" label="Guests" options={@users} multiple />
        <:actions>
          <.button phx-disable-with="Saving...">Save Invitation</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

  @impl true
  def update(assigns, socket) do
    {:ok, events} = Events.Event |> Ash.read()
    {:ok, users} = Accounts.User |> Ash.read()
    event_options = Enum.map(events, &{&1.title, &1.id})
    user_options = Enum.map(users, &{&1.email, &1.id})

    form =
      case assigns.action do
        :new ->
          AshPhoenix.Form.for_create(Events.Invitation, :create,
            actor: assigns.current_user,
            forms: [auto?: true]
          )

        :edit ->
          AshPhoenix.Form.for_update(assigns.invitation, :update,
            actor: assigns.current_user,
            forms: [auto?: true]
          )
      end

    IO.inspect(form, label: "form")

    {:ok,
     socket
     |> assign(assigns)
     |> assign(:form, to_form(form))
     |> assign(:events, event_options)
     |> assign(:users, user_options)}
  end

  @impl true
  def handle_event("validate", %{"form" => invitation_params}, socket) do
    form =
      AshPhoenix.Form.validate(socket.assigns.form, invitation_params)

    {:noreply, assign(socket, form: form)}
  end

  def handle_event("save", %{"form" => invitation_params}, socket) do
    save_invitation(socket, socket.assigns.action, invitation_params)
  end

  defp save_invitation(socket, :edit, invitation_params) do
    invitation_users =
      Enum.map(invitation_params["user_ids"], fn user_id ->
        %{user_id: user_id, invitation_id: socket.assigns.form.data.id}
      end)

    # How to add invitation_users to invitation_params?
    invitation_params = Map.put(invitation_params, "invitation_users", invitation_users)

    IO.inspect(invitation_params, label: "invitation_params")

    case AshPhoenix.Form.submit(socket.assigns.form,
           params: invitation_params,
           action_opts: [load: [:event, :invitation_users]]
         ) do
      {:ok, invitation} ->
        notify_parent({:saved, invitation})

        {:noreply,
         socket
         |> put_flash(:info, "Invitation updated successfully")
         |> push_patch(to: socket.assigns.patch)}

      {:error, form} ->
        {:noreply, assign(socket, form: form)}
    end
  end

  defp save_invitation(socket, :new, invitation_params) do
    case AshPhoenix.Form.submit(socket.assigns.form,
           params: invitation_params,
           action_opts: [load: [:event]]
         ) do
      {:ok, invitation} ->
        notify_parent({:saved, invitation})

        {:noreply,
         socket
         |> put_flash(:info, "Invitation created successfully")
         |> push_patch(to: socket.assigns.patch)}

      {:error, form} ->
        {:noreply, assign(socket, form: form)}
    end
  end

  defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

I am using forms?: auto but it doesnt seem to generate a nested form for me to use. Ive tried manually creating the right data that gets passed to submit and it still doesnt create records for invitation_users. I know I’m probably not configuring things correctly but Ive run out of documentation or examples I can use on the internet :frowning:

How would you have it such that I can add and remove guests (users) in my model from the invitation form? Or a nudge right direction would be awesome

Any help is much appreciated! Thank you!

The nested form isn’t being generated on create because the “initial state” of the nested forms is based off of the current state of the data. So in an update form, fi there were 3 related entities, you’d get 3 forms.

If you want there to be an empty form, ready to be typed into, then you can use AshPhoenix.Form.add_form

          AshPhoenix.Form.for_create(Events.Invitation, :create,
            actor: assigns.current_user,
            forms: [auto?: true]
          )
         |> AshPhoenix.Form.add_form(:relationship_name)

Cheers @zachdaniel

Working my way through understanding this. Thanks to your guidance I can add one guest on :new. What would help in piecing things together is how do I setup the forms for update?

Do I need to assign the data when adding the additional forms - something like this?

        :edit ->
          AshPhoenix.Form.for_update(assigns.invitation, :update,
            actor: assigns.current_user,
            forms: [auto?: true]
          )
          |> AshPhoenix.Form.add_form(:invitation_users,
            data: assigns.invitation.invitation_users
          )

In my resource I have this

    update :update do
      primary? true
      argument :invitation_users, {:array, :map}, allow_nil?: false

      change manage_relationship(:invitation_users, type: :remove)
    end

I get the following error when I try the above “Attempted to add a form at path: [:invitation_users], but no create_action was configured.”

at this spot in my code

          |> AshPhoenix.Form.add_form(:invitation_users,
            data: assigns.invitation.invitation_users
          )

This is my form currently. Ideally what I’m trying to end up with is the ability to add and remove guests

      <.simple_form
        for={@form}
        id="invitation-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
        <.input field={@form[:invitation_code]} label="Invitation Code" />
        <.input field={@form[:event_id]} type="select" label="Event" options={@events} />
        <.inputs_for :let={guest_form} field={@form[:invitation_users]}>
          <.input field={guest_form[:user_id]} type="select" label="Guest" options={@users} />
          <.button type="button" phx-click="remove_form" phx-value-path={guest_form.name}>
            Remove guest
          </.button>
        </.inputs_for>
        <:actions>
          <.button type="button" phx-click="add_form" phx-value-path={@form[:invitation_users].name}>
            Add guest
          </.button>
          <.button phx-disable-with="Saving...">Save Invitation</.button>
        </:actions>
      </.simple_form>

If you want to be able to add and remove guests, try type: :append_and_remove.

I’ve changed the type to specify such for update. It looks like I have an issue loading the edit form

The `data` key was configured for [:invitation_users], but no `update_action` was configured. Please configure one.

When Ive specified the form as such

        :edit ->
          AshPhoenix.Form.for_update(assigns.invitation, :update,
            actor: assigns.current_user,
            forms: [auto?: true]
          )
          |> AshPhoenix.Form.add_form(:invitation_users,
            data: assigns.invitation.invitation_users,
            type: :update
          )

Try type: :read. :append_and_remove expects the user to be reading records that should then be related.

If you want it to be a relate and update, you can use:
type: :append_and_remove, on_lookup: :relate_and_update