-
Whilst I’m not sure its required, it doesn’t harm in having the extra hidden input which stores the index. If you ever plan to have the rooms orderable you will need it as it helps ecto identify the row
-
(and 4.) The following extract from the docs is essentially the butter behind how sort_param works:
:sort_param
- the parameter name which keeps a list of indexes to sort from the relation parameters. Unknown indexes are considered to be new entries. Non-listed indexes will come before any sorted ones.
Pressing the button adds the ‘new’ index to the list of rooms. As this is an unknown index, it create a new entry.
I don’t want to give you information overload, but I also want to be some-degree of helpful so ill do my best to be somewhere in the middle.
In regards to your final comment, embedded schemas are useful for if you don’t want to create a table in, lets say postgres.
If you do want to save the value (rooms) as a ‘json blob’ on the row (property), you can use either embed_one or embed_many in combination with an embedded schema.
But the TLDR is that it doesn’t matter if you using an has_many or embed_many, the LV it’s still the same. Still an inputs_for
, still calling the changeset
on the schema/embedded schema.
Taking a look at your snippet of code, I’ll go through bit by bit.
- The schema looks good. You are correct to use the
on_replace
as the docs say. Using a has_many is perfectly fine.
- The changeset looks mostly good, correctly using
cast_assoc
for your rooms
has_many, however the required: true
is not necessary. I don’t think that’s a valid option either(?).
- Moving onto the render, firstly we have the
form
.
<.simple_form for={@form} id="property-form" phx-submit="save">
You should add a phx-change="validate"
attribute. This will call a handle_event when any input in the form is changed. This is required so you can update the changeset and the form in the socket. Docs
The handle event would look something like this:
def handle_event("validate", %{"properties" => params}, socket) do
form =
%Property{}
|> Property.changeset(params)
|> to_form(action: :validate)
{:noreply, assign(socket, form: form)}
end
In this case, we are assuming you are creating a new property so we are creating a fresh %Property{}
, and passing in the latest params
of the form. action: :validate
runs any validations (ie your |> validate_required([:address])
). In my example at the bottom of my response, I have included an example of pulling the property from the socket making this compatible with both creating and viewing existing properties.
Adding the attribute above is important, as your buttons call the phx-change attribute. See the below
<button
type="button"
name="rooms[rooms_drop][]"
value={rf.index}
phx-click={JS.dispatch("change")}
>
</button>
Specifically, phx-click={JS.dispatch("change")}
phx- is automatically pre-pended onto the change
, calling the function above you have just created.
(it’s tucked away in the docs admittedly…)
- However, the
name
you have used for this button’s and hidden inputs is incorrect. It should be name="properties[rooms_drop][]"
, rather than name="rooms[rooms_drop][]"
. Same for sort, name="properties[rooms_sort][]"
, rather than name="rooms[rooms_sort][]"
.
- Finally, moving onto the
add_room
button. As your ‘room’ is rather unexciting, we can just use sort_param
. We would only need to use phx-click="add_room"
and the corresponding handle_event if you are doing some more complicated things with setting defaults of the room. And in this case, you are (currently) not.
So put that to the back of your head for now, and replace it with this
<button type="button" name="properties[rooms_sort][]" value="new" phx-click={JS.dispatch("change")}>
Add Room
</button>
You can now delete the def handle_event("add_room", _, socket) do
function as you won’t be needing it.
And that’s it, it should now add the room when you hit the button
Should anyawy…
# In lib/my_app/listings/property
alias MyApp.Listings.Room
schema "properties" do
field :address, :string
has_many :rooms, Room, on_replace: :delete
timestamps(type: :utc_datetime)
end
@doc false
def changeset(property, attrs) do
property
|> cast(attrs, [:address])
|> validate_required([:address])
|> cast_assoc(:rooms,
with: &Room.changeset/2,
sort_param: :rooms_sort,
drop_param: :rooms_drop
)
end
# In web/live/listing_live/form.ex
def render(assigns) do
~H"""
<h1>New Property</h1>
<.simple_form for={@form} id="property-form" phx-submit="save" phx-change="validate">
<.input field={@form[:address]} type="text" label="Property Address" />
<.inputs_for :let={rf} field={@form[:rooms]}>
<input type="hidden" name="properties[rooms_sort][]" value={rf.index} />
<.input type="text" field={rf[:size]} label="Room Size (sq ft)" />
<button
type="button"
name="properties[rooms_drop][]"
value={rf.index}
phx-click={JS.dispatch("change")}
>
</button>
</.inputs_for>
<input type="hidden" name="properties[rooms_drop][]" />
<button type="button" name="properties[rooms_sort][]" value="new" phx-click={JS.dispatch("change")}>
Add Room
</button>
<:actions>
<.button phx-disable-with="Saving...">Save Listing</.button>
</:actions>
</.simple_form>
"""
end
def mount(_params, _session, socket) do
property = %Property{} # in future, you could Repo.get this for when 'viewing' a property
changeset = Listings.change_property(property)
{:ok, assign(socket, property: property, form: to_form(changeset))}
end
def handle_event("validate", %{"properties" => params}, socket) do
form =
socket.assigns.property
|> Property.changeset(params)
|> to_form(action: :validate)
{:noreply, assign(socket, form: form)}
end
def handle_event("save", %{"property" => params}, socket) do
case Listings.create_property(params) do
{:ok, _property} ->
{:noreply,
socket
|> put_flash(:info, "Property created successfully")
|> redirect(to: ~p"/properties")}
{:error, changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end