-
-
Save LostKobrakai/ce5385bd118189a24d60893188612de9 to your computer and use it in GitHub Desktop.
defmodule NestedWeb.FormLive do | |
use NestedWeb, :live_view | |
require Logger | |
defmodule Form do | |
use Ecto.Schema | |
import Ecto.Changeset | |
embedded_schema do | |
field :name, :string | |
embeds_many :cities, City, on_replace: :delete do | |
field :name, :string | |
end | |
end | |
def changeset(form, params) do | |
form | |
|> cast(params, [:name]) | |
|> validate_required([:name]) | |
# When string "[]" is detected, make it an empty list | |
# Doing that after the cast on `changeset.params` guarantees string keys | |
# Only works if `cast/4` is used though, which should be the case with forms | |
|> then(fn changeset -> | |
if changeset.params["cities"] == "[]" do | |
Map.update!(changeset, :params, &Map.put(&1, "cities", [])) | |
else | |
changeset | |
end | |
end) | |
|> cast_embed(:cities, with: &city_changeset/2) | |
end | |
def city_changeset(city, params) do | |
city | |
|> cast(params, [:name]) | |
|> validate_required([:name]) | |
end | |
end | |
def render(assigns) do | |
~H""" | |
<.form for={@changeset} let={f} phx-change="validate" phx-submit="submit"> | |
<%= label f, :name %> | |
<%= text_input f, :name %> | |
<%= error_tag f, :name %> | |
<fieldset> | |
<legend>Cities</legend> | |
<%# Hidden input will make sure "cities" is a key in `params` map for no cities to persist %> | |
<%# Needs to be before `inputs_for` to not overwrite cities if present %> | |
<%= hidden_input f, :cities, value: "[]" %> | |
<%= for f_city <- inputs_for(f, :cities) do %> | |
<div> | |
<%= hidden_inputs_for(f_city) %> | |
<%= label f_city, :name %> | |
<%= text_input f_city, :name %> | |
<%= error_tag f_city, :name %> | |
<button type="button" phx-click="delete-city" phx-value-index={f_city.index}>Delete</button> | |
</div> | |
<% end %> | |
<button type="button" phx-click="add-city">Add</button> | |
</fieldset> | |
<%= submit "Submit" %> | |
</.form> | |
""" | |
end | |
def mount(_, _, socket) do | |
base = %Form{ | |
id: "4e4d0944-60b3-4a09-a075-008a94ce9b9e", | |
name: "Somebody", | |
cities: [ | |
%Form.City{ | |
id: "26d59961-3b19-4602-b40c-77a0703cedb5", | |
name: "Berlin" | |
}, | |
%Form.City{ | |
id: "330a8f72-3fb1-4352-acf2-d871803cd152", | |
name: "Singapour" | |
} | |
] | |
} | |
changeset = Form.changeset(base, %{}) | |
{:ok, assign(socket, base: base, changeset: changeset)} | |
end | |
def handle_event("add-city", _, socket) do | |
socket = | |
update(socket, :changeset, fn changeset -> | |
existing = Ecto.Changeset.get_field(changeset, :cities, []) | |
Ecto.Changeset.put_embed(changeset, :cities, existing ++ [%{}]) | |
end) | |
{:noreply, socket} | |
end | |
def handle_event("delete-city", %{"index" => index}, socket) do | |
index = String.to_integer(index) | |
socket = | |
update(socket, :changeset, fn changeset -> | |
existing = Ecto.Changeset.get_field(changeset, :cities, []) | |
Ecto.Changeset.put_embed(changeset, :cities, List.delete_at(existing, index)) | |
end) | |
{:noreply, socket} | |
end | |
def handle_event("validate", %{"form" => params}, socket) do | |
changeset = | |
socket.assigns.base | |
|> Form.changeset(params) | |
|> struct!(action: :validate) | |
{:noreply, assign(socket, changeset: changeset)} | |
end | |
def handle_event("submit", %{"form" => params}, socket) do | |
changeset = Form.changeset(socket.assigns.base, params) | |
case Ecto.Changeset.apply_action(changeset, :insert) do | |
{:ok, data} -> | |
Logger.info("Submitted the following data: \n#{inspect(data, pretty: true)}") | |
socket = put_flash(socket, :info, "Submitted successfully") | |
{:noreply, assign(socket, changeset: changeset)} | |
{:error, changeset} -> | |
{:noreply, assign(socket, changeset: changeset)} | |
end | |
end | |
end |
This is bothering me .. for all the beauty in how ecto and forms work together, why is this case not handled in cast_assoc() ? I don't understand the design of ecto/forms to the level you do, however, this just doesn't seem right to have to do this ?
# When string "[]" is detected, make it an empty list
# Doing that after the cast on `changeset.params` guarantees string keys
# Only works if `cast/4` is used though, which should be the case with forms
|> then(fn changeset ->
if changeset.params["cities"] == "[]" do
Map.update!(changeset, :params, &Map.put(&1, "cities", []))
else
changeset
end
end)
BTW> @LostKobrakai - Thank you for the amazing code example .. in the ecosystem, it is really difficult to find reference examples of how things should be done. You have achieved a mastery level here ..
@milangupta1 This is not a fault of ecto, but of how html form encoding works. I've created a more in depth version of the above (working without that hack) here: https://kobrakai.de/kolumne/one-to-many-liveview-form
What is update
on #L105 ?
@dennym Are you asking which function it is? https://hexdocs.pm/phoenix_live_view/0.19.3/Phoenix.Component.html#update/3
@dennym Are you asking which function it is? https://hexdocs.pm/phoenix_live_view/0.19.3/Phoenix.Component.html#update/3
Thank you very much. Sometimes its hard to tell where some functions come from...
@LostKobrakai that's nice - thanks! I've noticed one issue tho. When invoking
add-city
(or my replacement) it will clear up all values from fields in the form (not saved values). how do preserve such data?