-
-
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 |
@gtaranti I've put up another revision, with reasonable tradeoffs from the form end of things. One could also make another parameter to changeset/2
, which tells the function when the changeset is used as input to the form and when as an output.
@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?
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...
Thanks, but now is it possible to display existing cities on load?