Problems with nested form and many-to-many relationship

Hi all,

I’ve been trying to figure this out all day. I’m still really new Phoenix so I’m not exactly sure where to look to try to resolve this issue.

I am working with a form that has a many-to-many relationship. The form has nested inputs. I believe I have set up the relationships properly, and I have also set up the proper changesets. When I try to render the form, I get the following error: no function clause matching in Ecto.Changeset.cast/4.

Here’s the form which is for a %WebApp.Meetings.Meeting struct.

<%= form_for @changeset, @action, fn f -> %>
  <%= label f, :title %>
  <%= text_input f, :title %>
  <%= error_tag f, :title %>

  <%= label f, :description %>
  <%= text_input f, :description %>
  <%= error_tag f, :description %>

  <%= inputs_for f, :users, fn user_form -> %>
    <%= text_input user_form, :id %>
  <% end %>

  <div>
    <%= submit "Save" %>
  </div>
<% end %>

The error originates from the <%= inputs_for f call.

Here’s the stack trace:

Here’s the changeset called by the form:

defmodule WebApp.Meetings.Meeting do
  def changeset(meeting, attrs) do
    meeting
    |> cast(attrs, [:title, :description, :utc_datetime, :timezone])
    |> cast_assoc(:users, with: &WebApp.Accounts.User.meetings_changeset/2)
  end
end

And here’s the changeset for the association:

defmodule WebApp.Accounts.User do
  def meetings_changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :id])
  end
end

Tinkering around the code, I’ve determined that the last cast seems to be the issue and I don’t really understand why.

Hoping someone can help!

Can you show what gets logged if you do this:

  def changeset(meeting, attrs) do
    meeting
    |> IO.inspect(label: "Meeting")
    |> cast(attrs |> IO.inspect(label: "meeting attrs"), [:title, :description, :utc_datetime, :timezone])
    |> cast_assoc(:users, with: &WebApp.Accounts.User.meetings_changeset/2)
  end

  def meetings_changeset(user, attrs) do
    user
    |> IO.inspect(label: "user")
    |> cast(attrs |> IO.inspect(label: "user attrs"), [:email, :id])
  end

Thanks for the help @Adzz! Here’s the output.

Meeting: %WebApp.Meetings.Meeting{
  __meta__: #Ecto.Schema.Metadata<:built, "meetings">,
  description: nil,
  id: nil,
  inserted_at: nil,
  owner: #Ecto.Association.NotLoaded<association :owner is not loaded>,
  owner_id: nil,
  participants: #Ecto.Association.NotLoaded<association :participants is not loaded>,
  timezone: nil,
  title: nil,
  updated_at: nil,
  users: [
    %{
      email: "test@email.com", 
      first_name: "test",
      id: 1,
      last_name: "test",
      timezone: "-6"
    }
  ],
  utc_datetime: nil,
  utc_offset: nil
}
meeting attrs: %{}
user: %{
  email: "test@email.com",
  first_name: "test",
  id: 1,
  last_name: "test",
  timezone: "-6"
}
user attrs: %{}

Hi Johnny,

Would you be able to give me more information about your use case? I had to make some assumptions while setting up a test project, and could not reproduce the error.

My Meeting Struct looks like

defmodule WebApp.Meetings.Meeting do
  use Ecto.Schema
  import Ecto.Changeset

  schema "meetins" do
    field :description, :string
    field :timezone, :string
    field :title, :string
    field :utc_datetime, :utc_datetime_usec

    has_many :users, WebApp.Accounts.User

    timestamps()
  end

  @doc false
  def changeset(meeting, attrs) do
    meeting
    |> cast(attrs, [:title, :description, :utc_datetime, :timezone])
    |> cast_assoc(:users, with: &WebApp.Accounts.User.meetings_changeset/2)
  end
end

and my User Struct looks like

defmodule WebApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :id])
    |> validate_required([:email, :id])
  end

  def meetings_changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :id])
  end
end

I’m slightly confused on the usage of :id in your relationship inputs, as that column is usually auto generated.

Generally for a many-to-many relationship, you would have an intermediary MeetingUsers schema or similar, and be specifying the user id in the form that way. As is it seems like you’re trying to insert users directly as children of the meeting struct.

Edit: I should also mention I did create a User before trying to view the Meeting form.

There is something wrong using cast_assoc with many to many.

I would use a put_assoc in this case, because User and Meeting are independant in a many to many relationship.

What will You do with many meetings, with many (same) users participating? With put_assoc, You can…

Hi @dbaer and @kokolegorille, thanks for your help! I wasn’t near my computer this weekend, so apologies for the delay.

To give a little more context:

  • There’s a polymorphic many-to-many relationship between User and Meeting through a join table Participants.
  • I want to create new meetings and use existing users that are already in the system.
  • The UX/interaction I’m working on is that the currently logged in user can see a list of existing users in the system.
    • They can click a button ‘Create Meeting’
    • It brings them to this form and has prepopulated in the nested form the user selected in the previous step

I hope that helps.

Okay, so instead of casting assoc directly to your users schema you’ll want to cast to a changeset in your Participants schema instead:

cast_assoc(:participants, with: &WebApp.Meeting.Participant.changeset/2)

ensuring Participant has the appropriate belongs_to :meeting set.

Thanks @dbaer. I’ll give this a try!

I’m getting a similar error, but now with arity 2.
no function clause matching in Ecto.Changeset.change/2

You need to update participant changeset, to accept 2 params (which is not the default)

The first param should be a participant.

Please show this part… WebApp.Meeting.Participant.changeset

  def changeset(participant, attrs) do
    participant
    |> IO.inspect(label: "Participant")
    |> cast(attrs |> IO.inspect(label: "Participant Attrs"), [:user_id, :meeting_id])
    |> validate_required([:user_id, :meeting_id])
  end

It looks good… You don’t have more error stack?

It’s similar as last time. Different line numbers.

This is the code at Line 371 of changeset.ex

  def change({data, types}, changes) when is_map(data) do

    change(%Changeset{data: data, types: Enum.into(types, %{}), valid?: true}, changes)

  end

Ok, I managed to fix this error by changing the nested form in form.html.eex to reference :participants instead of :users.

  <%= inputs_for f, :participants, fn user_form -> %>
    <%= text_input user_form, :user_id %>
  <% end %>

But the user is not showing up in the form…

It might be helpful to show what’s in my meeting_controller.ex

Here’s the new function:

  def new(conn, %{"participant" => participant}) do
    p = WebApp.Accounts.get_profile_by_email(participant)
    changeset = Meetings.change_meeting(%Meeting{users: [p.user]})
    render(conn, "new.html", changeset: changeset, particpant_profile: p)
  end

I changed Line 2 to participants: when populating the Meetings struct like so:

    changeset = Meetings.change_meeting(%Meeting{participants: [p.user]})

And now an error is coming from the Participant.changeset function, specifically this line:

|> cast(attrs |> IO.inspect(label: "Participant Attrs"), [:user_id, :meeting_id])

The error is: no function clause matching in Ecto.Changeset.cast/4

Is the user supposed to be able to select from a list of registered users in that form?

You could pass a reduced user list to a select tag in your participants assoc in the meeting form, as the 4th argument to select. i.e. select user_form, :user_id, @users.

Select needs that list to be a list of tuples, with each tuple containing {name, value}.

At this point, I’m wondering why the cast call is trying to call a function with an arity of 4? It doesn’t make sense to me how that happens?

Still having trouble with this. Anyone else out there that might be able to help? Would greatly appreciate it!