-
-
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?