Skip to content

Instantly share code, notes, and snippets.

@LostKobrakai
Last active April 12, 2024 18:56
Show Gist options
  • Star 34 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save LostKobrakai/ce5385bd118189a24d60893188612de9 to your computer and use it in GitHub Desktop.
Save LostKobrakai/ce5385bd118189a24d60893188612de9 to your computer and use it in GitHub Desktop.
Phoenix LiveView form with nested embeds and add/delete buttons
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
@dennym
Copy link

dennym commented Jul 2, 2023

@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...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment