many_to_many checkbox Phoenix 1.5

Sis Ccr
4 min readDec 31, 2020
many to many

In my application to search for rooms I have added some features in room. Like availity of parking space, Internet, pets friendly, kids friendly and some more. When adding the rooms for rent user will have check the check box to indicate the availability of the features. This features will further mapped to the many to many relation in our database.

Node: I have added common pitfalls and their solution at the last.

mix phx.gen.context Amenities Amenity amenities name

Before migrating let’s add unique constraint in the migration file

mix ecto.migrate

I already Had a Rooms Context and its schema (schema shown below), Next I generated juction table called room_amienites

mix phx.gen.schema Rooms.RoomAmenity room_amenities room_id:references:rooms amenity_id:references:amenities

mix ecto.migrate

Adding many_to_many relations in Room schema.

Adding many_to_many relations in Amenity schema.

And adding belongs_to association in RoomAmenity

That complete the setup for many to many relation.

next I add seed for Amenities

mix run priv/repo/seeds.exs

Now We have Amenities lets Add a form

first lets pass amenities from controller to template new.html.eex

Next I add following in template file. This will render checkboxes for each value in amenities

ameniteis checkbox view

checkout official docs for phoenix html for the options I am using in checkbox. It took some time to figure out. Specially the hidden_input, by default It sends both selected and not selected options. I set it to false so that It only sends the selected options. also the name property of the checkbox. name: room[amenities][] this will send params amenities inside the room map. and amenities are array. for experience web dev this might not be a problem but For non web dev like me it was tricky to understand. Make sure to read about html checkboxes too.

then in room_controller create action, if there are any errors we pass the amenities and and changeset

The load_amenities function in Room context is simply preloading any exisiting amenities .

def load_amenities(room) do
Repo.preload(room, [ :amenities])
end

We are almost done. we have sent the ids of the selected amenites to room controller create action. If we submit with selected association we can see the selected ids inside the request printed in console. Of course it will fail :) as we are not done yet.

Parameters: %{"_csrf_token" => "dGlzBGJ5MwskKlRmKhYWeQ4YJhM1Kzwc3ZCB1-DMjbl2nbfIZpLBGYNI", "room" => %{"address" => "", "amenities" => ["1", "3", "6"], "lat" => "", "long" => "", "number_of_rooms" => "", "price" => ""}}

Now lets add the association. with the room

In out rooms context lets add functions to create and associate Room and Amenities

Then in context animities.ex add the following

Now the amenities can be added. But there is one problem. I had other fields in my form, like price of room, latitude, longitude, address etc. When I selected the amenities only and submitted the form I was expecting the validation error But I got

Pitfall-1:

ArgumentError. List in Phoenix.HTML and templates may contain only List. got Invalid entry: This took a long time figuring out. I could not do it myself so asked it in stackoverflow

Since the room change set is invalid, There is no point in adding association. since I added the association, it appeared in the changeset like

changes: %{
amenities: [
#Ecto.Changeset<action: :update, changes: %{}, errors: [],
data: #Tailwind.Amenities.Amenity<>, valid?: true>
],

And the error is complaining that html can only render regular list, not the list of Ecto.changeset. So I tweak my create_room function like this.

This did solve the crash problem, but raises another issue.

Pitfall-2: All the checkboxes That I check was lost. That was not so user friendly.

fortunately, I learned hard way to use virtual fields.

I added virtual fields for selected amenities in Room.ex schema. Virtual fields are fields that are not in db but in schema only. I save all the ids comming from the form in this field.

field :selected_amenities, {:array, :string}, default: [], virtual: true

I again changed the create_rooms function in Room context like this

Here when the room validation fails, I insert the selected amenities ids in the :selected_parking field of Room.ex. The changeset.data is actually the Room model passed to the form when the validation is failed. Then in form I can have access to the selected fields and rechecked all previously selected fields.

load filter is same as previous. I am just preloading both parking and amenities field.

def load_filters(room) do
room |> Repo.preload([:parkings, :amenities]
end

My form now look like this. Helper function get_amenity_value checks if the value is inside the selected_amenities field or not. If it is, then it addes the id to value field. When value field and the checked_value field is same the checkbox is marked as checked.

Helper function check if the value is in the selected virtual field or not. If present then return the same value.

and thats if fully functional checkboxes for many to many relationships. A long journey and plenty of tricks to learn.

--

--