Search code examples
elixirmany-to-manyassociationsphoenix-frameworkecto

phoenix-framework many-to-many relationship with checkbox raises argumentError when validation fails


I am learning elixir and phoenix framework with an application that has form to add a room. room may have many-to-many relation with parkings and amenities. parkings and amenities are like tags that are checked via checkbox, other than that room has price, address, lat, long and other field. Every thing works well and room is created when I fill all the corect values. But throws ArgumentError when I check the parking or amenities field and leave other field empty and submit.

Request: POST /rooms
** (exit) an exception was raised:
    ** (ArgumentError) lists in Phoenix.HTML and templates may only contain integers representing bytes, binaries or other lists, got invalid entry: #Ecto.Changeset<action: :update, changes: %{}, errors: [], data: #Tailwind.Parkings.Parking<>, valid?: true>

This is the changeset that is thrown when the validation failed.

Ecto.Changeset<
  action: nil,
  changes: %{
    amenities: [
      #Ecto.Changeset<action: :update, changes: %{}, errors: [],
       data: #Tailwind.Amenities.Amenity<>, valid?: true>
    ],
    parkings: [
      #Ecto.Changeset<action: :update, changes: %{}, errors: [],
       data: #Tailwind.Parkings.Parking<>, valid?: true>
    ]
  },
  errors: [
    address: {"can't be blank", [validation: :required]},
    price: {"can't be blank", [validation: :required]},
    number_of_rooms: {"can't be blank", [validation: :required]},
    lat: {"can't be blank", [validation: :required]},
    long: {"can't be blank", [validation: :required]}
  ],
  data: #Tailwind.Rooms.Room<>,
  valid?: false
>

My best guess is that the amenities and parkings values inside the changes are the cause of the problem. Instead of simple list there is a changeset in amenities and parkings field. Even though my guess is right, I do not have enough understanding why it is behaving like this and how it is supposed to be.

I am adding association with put_assoc function in my context.

def create_room(attrs \\ %{}) do
    %Room{}
    |> Room.changeset(attrs)
    |> put_parking_association(attrs["parkings"])
    |> put_amenity_association(attrs["amenities"])
    |> Repo.insert()
  end

and put_amenity_association function is simply geting all the values with that id from db and add association with them.

 defp put_amenity_association(changeset, attrs) do
    amenities = Tailwind.Amenities.get_amenities(attrs)
    Ecto.Changeset.put_assoc(changeset, :amenities, amenities)
  end


 def get_amenities(ids) do
    Repo.all(from a in Tailwind.Amenities.Amenity, where: a.id in ^ids)
  end

and I am rendering form like this in template.

      <%= for amenity <- @amenities do %>
      <div class="flex items-center py-2 sm:w-1/4">
        <%= checkbox f, :amenities,
         checked_value: amenity.id,
         hidden_input: false,
         name: "room[amenities][]",

         class: "h-5 w-5 focus:ring-gray-900 focus:ring-1 bg-gray-900 text-gray-600" %>
        <span class="ml-3 text-sm"><%= amenity.name %></span>

      <% end %>
    <span class="text-red-600 px-2">
      <%= error_tag f, :amenities  %>
    </span>

I am dwelling over this without any result for few days and feeling directionless. Little sunshine over this matter would be a lot of help to move forward.

update:

with accepted solution I change my create_room function as follows:

def create_room(attrs \\ %{}) do
    case Room.changeset(%Room{}, attrs) do
      %Ecto.Changeset{valid?: false} = changes ->
        changes
        |> apply_action(:insert)

      # |> Repo.insert()

      changeset ->
        changeset
        |> put_parking_association(attrs["parkings"])
        |> put_amenity_association(attrs["amenities"])
        |> IO.inspect()
        |> Repo.insert()
    end
  end

Solution

  • When the required fields are missing, Room.changeset(attrs) returns an invalid changeset that is not modified any further by any subsequent calls (because it’s already invalid.)

    It gets passed directly to Repo.insert/2 returns {:error, changeset} back.

    So what you need would be probably to handle not only the happy path but the errors as well.

    def create_room(attrs \\ %{}) do
      case Room.changeset(%Room{}, attrs) do
        %Ecto.Changeset{valid?: false} -> handle_error(...)
    
        changeset ->
          changeset
          |> put_parking_association(attrs["parking"])
          |> put_amenity_association(attrs["amenities"])
          |> Repo.insert()
      end
    
      ...
    

    The error you see is probably induced way down the stack, but the reason is alongside the above.