aswinmohanme
Dynamic nested inputs_for not working when the last element is deleted
I am building a resume builder with the schema
defmodule ModernResume.Resumes.Resume do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "resumes" do
field :name, :string
field :email, :string
field :phone, :string
field :url, :string
field :summary, :string
belongs_to :profile, ModernResume.Profiles.Profile
has_many :educational_qualifications,
ModernResume.EducationalQualifications.EducationalQualification,
preload_order: [desc: :end_date],
on_replace: :delete
has_many :work_experiences,
ModernResume.WorkExperiences.WorkExperience,
preload_order: [desc: :end_date],
on_replace: :delete
has_many :projects,
ModernResume.Projects.Project,
on_replace: :delete
has_many :skills,
ModernResume.Skills.Skill,
on_replace: :delete
timestamps(type: :utc_datetime)
end
@doc false
def changeset(resume, attrs) do
resume
|> cast(attrs, [:name, :email, :phone, :url, :summary])
|> validate_required(:name)
|> cast_assoc(
:educational_qualifications,
with: &ModernResume.EducationalQualifications.EducationalQualification.changeset/2,
sort_param: :educational_qualifications_order,
drop_param: :educational_qualifications_delete
)
|> cast_assoc(
:work_experiences,
with: &ModernResume.WorkExperiences.WorkExperience.changeset/2,
sort_param: :work_experiences_order,
drop_param: :work_experiences_delete
)
|> cast_assoc(
:projects,
with: &ModernResume.Projects.Project.changeset/2,
sort_param: :projects_order,
drop_param: :projects_delete
)
|> cast_assoc(
:skills,
with: &ModernResume.Skills.Skill.changeset/2,
sort_param: :skills_order,
drop_param: :skills_delete
)
end
end
The Edit LiveView is
defmodule ModernResumeWeb.ResumeLive.Edit do
use ModernResumeWeb, :live_view
alias ModernResume.Resumes
alias ModernResume.TypesettingEngine
@impl true
def mount(%{"id" => resume_id}, _session, socket) do
resume =
Resumes.get_resume(resume_id)
changeset =
Resumes.change_resume(resume)
{:ok,
assign(socket,
resume: resume,
form: to_form(changeset),
pdf_base64: nil,
page_title: resume.name
)}
end
@impl true
def handle_event("validate", %{"resume" => resume_params}, socket) do
changeset =
socket.assigns.resume
|> Resumes.change_resume(resume_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))}
end
@impl true
def handle_event("save", %{"resume" => resume_params}, socket) do
case Resumes.update_resume(
socket.assigns.resume,
resume_params
) do
{:ok, resume} ->
changeset = Resumes.change_resume(resume)
latex_file_dir =
TypesettingEngine.to_latex!(resume)
|> write_latex_file!(resume.id)
System.cmd("latexmk", ["resume.tex", "-pdf"], cd: latex_file_dir, stderr_to_stdout: true)
pdf_file_path = Path.join(latex_file_dir, "resume.pdf")
pdf_base64 = File.read!(pdf_file_path) |> Base.encode64()
{:noreply,
assign(socket,
resume: resume,
form: to_form(changeset),
pdf_base64: pdf_base64
)}
{:error, changeset} ->
{:noreply,
assign(socket,
form: to_form(changeset)
)}
end
end
@impl true
def handle_params(_action, _, socket) do
header_text =
if socket.assigns.live_action do
Phoenix.Naming.humanize(socket.assigns.live_action)
else
"Profile"
end
{:noreply, socket |> assign(header_text: header_text)}
end
def write_latex_file!(content, dirname) do
tmp_dir = Path.join(System.tmp_dir!(), dirname)
File.mkdir_p!(tmp_dir)
tmp_file = Path.join(tmp_dir, "/resume.tex")
File.write!(tmp_file, content)
tmp_dir
end
end
This is the edit.html.heex file
<div class="min-w-[560px] w-[560px] border-r overflow-y-scroll pb-12">
<.simple_form id="resume-form" for={@form} phx-submit="save" phx-change="validate">
<div class="flex flex-row items-center justify-end container-box py-4 border-b">
<.button class="button-sm">
<.icon name="hero-document-check" /> Save and Publish
</.button>
</div>
<div class="flex flex-col items-stretch gap-4">
<div class="container-box flex flex-row items-center justify-between">
<h2 class="font-medium text-lg text-primary-700"><%= @header_text %></h2>
</div>
<div class="form-container container-box py-4 bg-primary-50">
<.input field={@form[:name]} label="Full name" placeholder="Your name" />
<.input field={@form[:email]} label="Email" />
<.input field={@form[:phone]} label="Phone" />
<.input field={@form[:url]} label="Website / Github / LinkedIn" />
</div>
</div>
<div class="flex flex-col items-stretch gap-4">
<div class="container-box flex flex-row items-center justify-between">
<h2 class="font-medium text-lg text-primary-700"><%= @header_text %></h2>
<label class="button-secondary button-sm">
<input
class="hidden"
type="checkbox"
name="resume[educational_qualifications_order][]"
/>
<.icon name="hero-plus-circle" /> Add school
</label>
</div>
<.inputs_for :let={f_eq} field={@form[:educational_qualifications]}>
<input
type="hidden"
name="resume[educational_qualifications_order][]"
value={f_eq.index}
/>
<div class="form-container bg-primary-50 container-box py-4">
<label class="button-outline button-xs self-end">
<input
type="checkbox"
name="resume[educational_qualifications_delete][]"
value={f_eq.index}
class="hidden "
/> <.icon name="hero-x-mark" /> Remove school
</label>
<.input field={f_eq[:institution]} label="Institution" placeholder="Your school name" />
<.input field={f_eq[:location]} label="Location" />
<.input field={f_eq[:degree]} label="Degree" />
<.input field={f_eq[:major]} label="Major" />
<.input field={f_eq[:score]} label="Score / GPA" />
<div class="flex flex-row gap-4">
<div>
<.input type="date" field={f_eq[:start_date]} label="Start date" />
</div>
<div>
<.input
type="date"
field={f_eq[:end_date]}
label="End date (empty if currently studying)"
/>
</div>
</div>
</div>
</.inputs_for>
</div>
<div :if={@live_action == :work_experiences} class="flex flex-col items-stretch gap-4">
<div class="container-box flex flex-row items-center justify-between">
<h2 class="font-medium text-lg text-primary-700"><%= @header_text %></h2>
<label class="button-secondary button-sm">
<input class="hidden" type="checkbox" name="resume[work_experiences_order][]" />
<.icon name="hero-plus-circle" /> Add company
</label>
</div>
<.inputs_for :let={f_we} field={@form[:work_experiences]}>
<input type="hidden" name="resume[work_experiences_order][]" value={f_we.index} />
<div class="form-container bg-primary-50 container-box py-4">
<label class="button-outline button-xs self-end">
<input
type="checkbox"
name="resume[work_experiences_delete][]"
value={f_we.index}
class="hidden "
/> <.icon name="hero-x-mark" /> Remove company
</label>
<.input field={f_we[:name]} label="Company name" placeholder="Your company name" />
<.input field={f_we[:location]} label="Location" />
<.input field={f_we[:position]} label="Position" />
<.input field={f_we[:url]} label="Company Website" />
<div class="flex flex-row gap-4">
<div>
<.input type="date" field={f_we[:start_date]} label="Start date" />
</div>
<div>
<.input
type="date"
field={f_we[:end_date]}
label="End date (empty if currently working)"
/>
</div>
</div>
<div>
<div class="mt-8 flex flex-row items-center justify-between mb-2">
<h3 class="font-medium">Highlights</h3>
<label class="button-secondary button-xs">
<input
type="checkbox"
class="hidden"
name={"resume[work_experiences][#{f_we.index}][highlights_order][]"}
/>
<.icon name="hero-plus-circle" /> Add highlight
</label>
</div>
<.inputs_for :let={f_we_h} field={f_we[:highlights]}>
<div class="py-4">
<div class="flex flex-row justify-end"></div>
<input
type="hidden"
name={"resume[work_experiences][#{f_we.index}][highlights_order][]"}
value={f_we_h.index}
/>
<div class="flex flex-row items-start gap-2">
<.input field={f_we_h[:description]} />
<label class="button-outline button-xs h-10 gap-1">
<input
type="checkbox"
name={"resume[work_experiences][#{f_we.index}][highlights_delete][]"}
value={f_we_h.index}
class="hidden"
/> <.icon name="hero-x-mark" />
</label>
</div>
</div>
</.inputs_for>
</div>
</div>
</.inputs_for>
</div>
<div :if={@live_action == :projects} class="flex flex-col items-stretch gap-4">
<div class="container-box flex flex-row items-center justify-between">
<h2 class="font-medium text-lg text-primary-700"><%= @header_text %></h2>
<label class="button-secondary button-sm">
<input type="checkbox" name="resume[projects_order][]" class="hidden" />
<.icon name="hero-plus-circle" /> Add project
</label>
</div>
<.inputs_for :let={f_p} field={@form[:projects]}>
<input type="hidden" name="resume[projects_order][]" value={f_p.index} />
<div class="form-container bg-primary-50 container-box py-4">
<label class="button-outline button-xs self-end">
<input
type="checkbox"
name="resume[projects_delete][]"
value={f_p.index}
class="hidden"
/> <.icon name="hero-x-mark" /> Remove project
</label>
<.input field={f_p[:name]} label="Name" />
<.input field={f_p[:description]} label="Description" />
<.input field={f_p[:url]} label="Link" />
</div>
</.inputs_for>
</div>
<div :if={@live_action == :skills} class="flex flex-col items-stretch gap-4">
<div class="container-box flex flex-row items-center justify-between">
<h2 class="font-medium text-lg text-primary-700"><%= @header_text %></h2>
<label class="button-secondary button-sm">
<input type="checkbox" name="resume[skills_order][]" class="hidden" />
<.icon name="hero-plus-circle" /> Add skill
</label>
</div>
<.inputs_for :let={f_s} field={@form[:skills]}>
<input type="hidden" name="resume[skills_order][]" value={f_s.index} />
<div class="form-container bg-primary-50 container-box py-4">
<label class="button-outline button-xs self-end">
<input
type="checkbox"
name="resume[skills_delete][]"
value={f_s.index}
class="hidden"
/> <.icon name="hero-x-mark" /> Remove skill
</label>
<.input field={f_s[:name]} label="Name" />
<.input field={f_s[:description]} label="Description" />
</div>
</.inputs_for>
</div>
<:actions></:actions>
</.simple_form>
</div>
I’m using the new sort_param and drop_param introduced in the latest ecto update. The issue is everything else works fine, but when I delete the last element of the educational_qualification or work_experience the form gets reset with the association still there. I can add and remove association until it’s the last association.
Here is a video recording https://www.youtube.com/watch?v=8ZqXnKPb0Ac
The validate function processes the changeset correctly, but once the save event is invoked an empty %{} is sent params. The delete params are missing. Any help would be greatly appreciated.
Marked As Solved
LostKobrakai
Last Post!
aswinmohanme
Popular in Questions
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
What is the proper way to load a module from a file in to IEX?
In the python world, doing something like this pretty standard:
from ....
New
Hey all,
I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar.
I p...
New
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
Other popular topics
Hi,
I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
Hi folks,
Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
New
If I have a post route which an argument:
post /my_post_route/:my_param1, MyController.my_post_handler
How would get the post params ...
New
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar.
I p...
New
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex









