FlyingNoodle

FlyingNoodle

Managing many_to_many with AshPhoenix - works in iex but not in liveview?

I have a resource called Meeting which has a many to many relationship. My create action looks like this:

defmodule Meeting do
  create :create do
    accept [:date]
    change manage_relationship(
        :instances,
         type: :append_and_remove
    )
end

when I create a meeting through iex this works just fine:

MyApp.Meetings.create_meeting(%{date: ~D[2020-01-01], instances: [%{id: 16}]}, actor: u)

# which produces
{:ok,
 %MyApp.Meetings.Meeting{
   id: "33be6aed-3e72-475f-bcdd-5da3749722ad",
   date: ~D[2020-01-01],
   inserted_at: ~U[2025-11-15 21:10:33.992514Z],
   updated_at: ~U[2025-11-15 21:10:33.992514Z],
   is_upcoming?: #Ash.NotLoaded<:calculation, field: :is_upcoming?>,
   actor_is_part_of_meeting?: #Ash.NotLoaded<:calculation, field: :actor_is_part_of_meeting?>,
   users: #Ash.NotLoaded<:relationship, field: :users>,
   instances: [
     %MyApp.Core.Instance{
       id: 16,
       dossier_number: #Ash.NotLoaded<:calculation, field: :dossier_number>,
       __meta__: #Ecto.Schema.Metadata<:loaded, "INSTANCE">
     }
   ],
   users_join_assoc: #Ash.NotLoaded<:relationship, field: :users_join_assoc>,
   instances_join_assoc: #Ash.NotLoaded<:relationship, field: :instances_join_assoc>,
   __meta__: #Ecto.Schema.Metadata<:loaded, "meetings">
 }}

However, when I run the same thing in a liveview:

<.form for={@form} class="uk-form-stacked" phx-submit="submit" phx-change="validate">
<.input field={@form[:date]} label="Datum" type="date" type="date" class="uk-input" />

<div class="uk-flex">
  <div class="uk-flex-1-2">
    <.inputs_for :let={instance} field={@form[:instances]}>
      <!-- <.input field={instance[:dossier_number]} /> -->
      <.input field={instance[:id]} />
    </.inputs_for>

    <label>
      <input
        type="checkbox"
        name={"#{@form.name}[_add_instances]"}
        value="end"
        class="hidden"
      />
      <span class="uk-button uk-button-secondary">
        Neues Dossier hinzufügen
      </span>
    </label>
    -->
  </div>
</div>

<button type="submit" class="uk-button uk-button-primary uk-margin">Speichern</button>
</.form>

with some very vanilla event handlers:

  @impl Phoenix.LiveView
  def mount(_params, _session, socket) do
    form = MyApp.Meetings.form_to_create_meeting(actor: socket.assigns.current_user)

    socket = assign(socket, :form, to_form(form))

    {:ok, socket}
  end

  @impl Phoenix.LiveView
  def handle_event("submit", %{"form" => form}, socket) do
    dbg(form)

    case AshPhoenix.Form.submit(socket.assigns.form,
           params: form,
           actor: socket.assigns.current_user
         ) do
      {:ok, _meeting} ->
        {:noreply, push_navigate(socket, to: ~p"/meetings")}

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

  @impl Phoenix.LiveView
  def handle_event("validate", %{"form" => form}, socket) do
    {:noreply,
     assign(
       socket,
       :form,
       socket.assigns.form
       |> AshPhoenix.Form.validate(form, actor: socket.assigns.current_user)
       |> to_form()
     )}
  end

I get this (it errors out during the creation of the join resource)

[(my_app 0.1.0) lib/my_app_web/live/meetings_new_live.ex:70: MyAppWeb.MeetingsNewLive.handle_event/3]
form #=> %{
  "date" => "1111-01-11",
  "instances" => %{
    "0" => %{
      "_form_type" => "read",
      "_persistent_id" => "0",
      "_touched" => "_form_type,_persistent_id,_touched,dossier_number",
      "dossier_number" => "2024-1"
    }
  }
}

[(my_app 0.1.0) lib/my_app_web/live/meetings_new_live.ex:80: MyAppWeb.MeetingsNewLive.handle_event/3]
form #=> %Phoenix.HTML.Form{
  source: #AshPhoenix.Form<
    resource: MyApp.Meetings.Meeting,
    action: :create,
    type: :create,
    params: %{
      "date" => "1111-01-11",
      "instances" => %{
        "0" => %{
          "_form_type" => "read",
          "_persistent_id" => "0",
          "_touched" => "_form_type,_persistent_id,_touched,dossier_number",
          "dossier_number" => "2024-1"
        }
      }
    },
    source: #Ash.Changeset<
      domain: MyApp.Meetings,
      action_type: :create,
      action: :create,
      attributes: %{date: ~D[1111-01-11]},
      relationships: %{
        instances: [
          {[
             %{
               dossier_number: %{
                 "_form_type" => "read",
                 "_persistent_id" => "0",
                 "_touched" => "_form_type,_persistent_id,_touched,dossier_number",
                 "dossier_number" => "2024-1"
               }
             }
           ],
           [
             debug?: false,
             ignore?: false,
             on_missing: :unrelate,
             on_match: :ignore,
             on_lookup: {:relate, :create, :read, [:dossier_number]},
             on_no_match: :error,
             eager_validate_with: false,
             authorize?: true,
             meta: [id: :instances],
             type: :append_and_remove,
             use_identities: [:dossier_number],
             value_is_key: :dossier_number
           ]}
        ]
      },
      arguments: %{
        instances: [
          %{
            "_form_type" => "read",
            "_persistent_id" => "0",
            "_touched" => "_form_type,_persistent_id,_touched,dossier_number",
            "dossier_number" => "2024-1"
          }
        ]
      },
      errors: [],
      data: %MyApp.Meetings.Meeting{
        id: nil,
        date: nil,
        inserted_at: nil,
        updated_at: nil,
        is_upcoming?: #Ash.NotLoaded<:calculation, field: :is_upcoming?>,
        actor_is_part_of_meeting?: #Ash.NotLoaded<:calculation, field: :actor_is_part_of_meeting?>,
        users: #Ash.NotLoaded<:relationship, field: :users>,
        instances: #Ash.NotLoaded<:relationship, field: :instances>,
        users_join_assoc: #Ash.NotLoaded<:relationship, field: :users_join_assoc>,
        instances_join_assoc: #Ash.NotLoaded<:relationship, field: :instances_join_assoc>,
        __meta__: #Ecto.Schema.Metadata<:built, "meetings">
      },
      valid?: true
    >,
    name: "form",
    data: nil,
    form_keys: [
      instances: [
        data: #Function<25.61500058/1 in AshPhoenix.Form.Auto.relationship_fetcher/3>,
        read_action: :read,
        read_resource: MyApp.Core.Instance,
        type: :list,
        forms: [
          _update: [
            resource: MyApp.Meetings.MeetingInstance,
            managed_relationship: {MyApp.Meetings.Meeting, :instances,
             [
               on_missing: {:unrelate, :destroy},
               type: :append_and_remove,
               on_lookup: {:relate, :create, :read, [:dossier_number]},
               on_no_match: :error,
               on_match: :ignore,
               use_identities: [:dossier_number],
               value_is_key: :dossier_number
             ]},
            type: :single,
            data: %MyApp.Meetings.MeetingInstance{
              id: nil,
              inserted_at: nil,
              updated_at: nil,
              instance_id: nil,
              meeting_id: nil,
              instance: #Ash.NotLoaded<:relationship, field: :instance>,
              meeting: #Ash.NotLoaded<:relationship, field: :meeting>,
              __meta__: #Ecto.Schema.Metadata<:built, "meeting_instances">
            },
            update_action: :create
          ],
          _join: [
            resource: MyApp.Meetings.MeetingInstance,
            managed_relationship: {MyApp.Meetings.Meeting, :instances,
             [
               on_missing: {:unrelate, :destroy},
               type: :append_and_remove,
               on_lookup: {:relate, :create, :read, [:dossier_number]},
               on_no_match: :error,
               on_match: :ignore,
               use_identities: [:dossier_number],
               value_is_key: :dossier_number
             ]},
            type: :single,
            data: #Function<44.61500058/2 in AshPhoenix.Form.Auto.add_join_form/4>,
            destroy_fields: [],
            destroy_action: :destroy,
            merge?: true
          ]
        ],
        sparse?: false,
        managed_relationship: {MyApp.Meetings.Meeting, :instances,
         [
           on_missing: {:unrelate, :destroy},
           type: :append_and_remove,
           on_lookup: {:relate, :create, :read, [:dossier_number]},
           on_no_match: :error,
           on_match: :ignore,
           use_identities: [:dossier_number],
           value_is_key: :dossier_number
         ]},
        must_load?: false
      ]
    ],
    forms: %{
      instances: [
        #AshPhoenix.Form<
          resource: MyApp.Core.Instance,
          action: :read,
          type: :read,
          params: %{
            "_form_type" => "read",
            "_persistent_id" => "0",
            "_touched" => "_form_type,_persistent_id,_touched,dossier_number",
            "dossier_number" => "2024-1"
          },
          source: #Ash.Query<resource: MyApp.Core.Instance, action: :read>,
          name: "form[instances][0]",
          data: nil,
          form_keys: [
            _update: [
              resource: MyApp.Meetings.MeetingInstance,
              managed_relationship: {MyApp.Meetings.Meeting, :instances,
               [
                 on_missing: {:unrelate, :destroy},
                 type: :append_and_remove,
                 on_lookup: {:relate, :create, :read, [:dossier_number]},
                 on_no_match: :error,
                 on_match: :ignore,
                 use_identities: [:dossier_number],
                 value_is_key: :dossier_number
               ]},
              type: :single,
              data: %MyApp.Meetings.MeetingInstance{
                id: nil,
                inserted_at: nil,
                updated_at: nil,
                instance_id: nil,
                meeting_id: nil,
                instance: #Ash.NotLoaded<:relationship, field: :instance>,
                meeting: #Ash.NotLoaded<:relationship, field: :meeting>,
                __meta__: #Ecto.Schema.Metadata<:built, "meeting_instances">
              },
              update_action: :create
            ],
            _join: [
              resource: MyApp.Meetings.MeetingInstance,
              managed_relationship: {MyApp.Meetings.Meeting, :instances,
               [
                 on_missing: {:unrelate, :destroy},
                 type: :append_and_remove,
                 on_lookup: {:relate, :create, :read, [:dossier_number]},
                 on_no_match: :error,
                 on_match: :ignore,
                 use_identities: [:dossier_number],
                 value_is_key: :dossier_number
               ]},
              type: :single,
              data: #Function<44.61500058/2 in AshPhoenix.Form.Auto.add_join_form/4>,
              destroy_fields: [],
              destroy_action: :destroy,
              merge?: true
            ]
          ],
          forms: %{
            _update: #AshPhoenix.Form<
              resource: MyApp.Meetings.MeetingInstance,
              action: :create,
              type: :create,
              params: %{},
              source: #Ash.Changeset<
                domain: MyApp.Meetings,
                action_type: :create,
                action: :create,
                attributes: %{},
                relationships: %{},
                errors: [
                  %Ash.Error.Changes.Required{
                    field: :meeting_id,
                    type: :attribute,
                    resource: MyApp.Meetings.MeetingInstance,
                    splode: Ash.Error,
                    bread_crumbs: [],
                    vars: [],
                    path: [],
                    stacktrace: #Splode.Stacktrace<>,
                    class: :invalid
                  },
                  %Ash.Error.Changes.Required{
                    field: :instance_id,
                    type: :attribute,
                    resource: MyApp.Meetings.MeetingInstance,
                    splode: Ash.Error,
                    bread_crumbs: [],
                    vars: [],
                    path: [],
                    stacktrace: #Splode.Stacktrace<>,
                    class: :invalid
                  }
                ],
                data: %MyApp.Meetings.MeetingInstance{
                  id: nil,
                  inserted_at: nil,
                  updated_at: nil,
                  instance_id: nil,
                  meeting_id: nil,
                  instance: #Ash.NotLoaded<:relationship, field: :instance>,
                  meeting: #Ash.NotLoaded<:relationship, field: :meeting>,
                  __meta__: #Ecto.Schema.Metadata<:built, "meeting_instances">
                },
                context: %{
                  accessing_from: %{
                    name: :instances,
                    source: MyApp.Meetings.Meeting,
                    manage_relationship_opts: [
                      on_missing: {:unrelate, :destroy},
                      type: :append_and_remove,
                      on_lookup: {:relate, :create, :read, [:dossier_number]},
                      on_no_match: :error,
                      on_match: :ignore,
                      use_identities: [:dossier_number],
                      value_is_key: :dossier_number
                    ]
                  }
                },
                valid?: false
              >,
              name: "form[instances][0][_update]",
              data: nil,
              form_keys: [],
              forms: %{},
              domain: MyApp.Meetings,
              method: "post",
              submit_errors: [
                meeting_id: {"is required", []},
                instance_id: {"is required", []}
              ],
              id: "form_instances_0__update",
              transform_errors: nil,
              post_process_errors: nil,
              original_data: nil,
              transform_params: nil,
              prepare_params: nil,
              prepare_source: nil,
              raw_params: %{},
              warn_on_unhandled_errors?: true,
              any_removed?: false,
              added?: false,
              changed?: false,
              touched_forms: MapSet.new([]),
              valid?: false,
              errors: true,
              submitted_once?: true,
              ...
            >
          },
          domain: MyApp.Core,
          method: "post",
          submit_errors: [],
          id: "form_instances_0",
          transform_errors: nil,
          post_process_errors: nil,
          original_data: nil,
          transform_params: nil,
          prepare_params: nil,
          prepare_source: nil,
          raw_params: %{
            "_form_type" => "read",
            "_persistent_id" => "0",
            "_touched" => "_form_type,_persistent_id,_touched,dossier_number",
            "dossier_number" => "2024-1"
          },
          warn_on_unhandled_errors?: true,
          any_removed?: false,
          added?: false,
          changed?: false,
          touched_forms: MapSet.new(["_form_type", "_persistent_id", "_touched",
           "dossier_number"]),
          valid?: false,
          errors: true,
          submitted_once?: true,
          just_submitted?: true,
          ...
        >
      ]
    },
    domain: MyApp.Meetings,
    method: "post",
    submit_errors: [],
    id: "form",
    transform_errors: nil,
    post_process_errors: nil,
    original_data: nil,
    transform_params: nil,
    prepare_params: nil,
    prepare_source: nil,
    raw_params: %{
      "date" => "1111-01-11",
      "instances" => %{
        "0" => %{
          "_form_type" => "read",
          "_persistent_id" => "0",
          "_touched" => "_form_type,_persistent_id,_touched,dossier_number",
          "dossier_number" => "2024-1"
        }
      }
    },
    warn_on_unhandled_errors?: true,
    any_removed?: false,
    added?: false,
    changed?: true,
    touched_forms: MapSet.new(["date", "instances"]),
    valid?: false,
    errors: true,
    submitted_once?: true,
    just_submitted?: true,
    ...
  >,
  impl: Phoenix.HTML.FormData.AshPhoenix.Form,
  id: "form",
  name: "form",
  data: nil,
  action: nil,
  hidden: [_touched: "date,instances", _form_type: "create"],
  params: %{
    "date" => "1111-01-11",
    "instances" => %{
      "0" => %{
        "_form_type" => "read",
        "_persistent_id" => "0",
        "_touched" => "_form_type,_persistent_id,_touched,dossier_number",
        "dossier_number" => "2024-1"
      }
    }
  },
  errors: [],
  options: [method: "post"],
  index: nil
}

I can’t really explain what is happening here. Do I have to clean up these forms somehow? Is this a bug?

First Post!

FlyingNoodle

FlyingNoodle

I’m sry I can’t figure out how I can edit my original post (I don’t see a button anywhere?).

What particulary confuses me is this bit in the error form:

_update: #AshPhoenix.Form<
              resource: ExEbau.Meetings.MeetingInstance,
              action: :create,
              type: :create,
              params: %{},
              source: #Ash.Changeset<
                domain: ExEbau.Meetings,
                action_type: :create,
                action: :create,
                attributes: %{},
                relationships: %{},
                errors: [
                  %Ash.Error.Changes.Required{
                    field: :meeting_id,
                    type: :attribute,
                    resource: ExEbau.Meetings.MeetingInstance,
                    splode: Ash.Error,
                    bread_crumbs: [],
                    vars: [],
                    path: [],
                    stacktrace: #Splode.Stacktrace<>,
                    class: :invalid
                  },
                  %Ash.Error.Changes.Required{
                    field: :instance_id,
                    type: :attribute,
                    resource: ExEbau.Meetings.MeetingInstance,
                    splode: Ash.Error,
                    bread_crumbs: [],
                    vars: [],
                    path: [],
                    stacktrace: #Splode.Stacktrace<>,
                    class: :invalid
                  }
                ],

Why is it trying to create this relationship now? I can’t know the :meeting_id at this point since it hasn’t been created yet. The instance_id is known and I’m not sure why it’s not passing that to this create action.

Where Next?

Popular in Questions Top

mgjohns61585
Could someone help me? I’m making my first elixir program, number guessing game. I can’t figure out how to convert the user’s guess from ...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lists...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
hariharasudhan94
Lets say i have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; "XXX...
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
hariharasudhan94
Lets say i have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; "XXX...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

We're in Beta

About us Mission Statement