Skip to content

Instantly share code, notes, and snippets.

@dsignr
Forked from LostKobrakai/form_live.ex
Created April 9, 2025 08:41
Show Gist options
  • Save dsignr/b22738be1fcdd0ed32c01f6ee15e973b to your computer and use it in GitHub Desktop.
Save dsignr/b22738be1fcdd0ed32c01f6ee15e973b 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment