Skip to content

Instantly share code, notes, and snippets.

@C-Sinclair
Created May 26, 2023 13:36
Show Gist options
  • Save C-Sinclair/a95bfd76bba9e174485268f8362877db to your computer and use it in GitHub Desktop.
Save C-Sinclair/a95bfd76bba9e174485268f8362877db to your computer and use it in GitHub Desktop.
defmodule LiveViewDynamicFormIssue do
@moduledoc """
Reproduction of a LiveView issue with dynamic nested forms
Try adding 2 items to the list, you aren't able to remove the first one
Try adding a 3rd item, you can remove the first one now. But then once again you can't remove the first one!
"""
use Phoenix.Component
def mount(_, _, socket) do
socket =
socket
|> assign(:task_id, nil)
|> assign(:task, nil)
|> assign(:task_changeset, nil)
|> assign(:task_form, nil)
{:ok, socket}
end
def handle_params(
%{
"task_id" => task_id
},
_,
socket
) do
task = %ProjectTask{id: task_id}
task_changeset = ProjectTask.changeset(task)
form = to_form(task_changeset)
socket =
socket
|> assign(:task, task)
|> assign(:task_id, task_id)
|> assign(:task_changeset, task_changeset)
|> assign(:task_form, form)
{:noreply, socket}
end
def handle_event("change_questions", params, socket) do
IO.inspect(params, label: "Params")
changeset =
socket.assigns.task
|> ProjectTask.changeset(params["project_task"] || %{})
|> ProjectTask.update_changeset_questions()
{:ok, task} =
Ecto.Changeset.apply_action(changeset, :update)
|> IO.inspect(label: "Task changeset with questions applied")
task_form =
changeset
|> to_form()
# HERE, questions state is not updated when new question is added
# |> IO.inspect(label: "Task changeset to form")
socket =
socket
|> assign(:task, task)
|> assign(:task_form, task_form)
{:noreply, socket}
end
def render(assigns) do
~H"""
<.form for={@task_form} phx-change="change_questions" class="flex flex-col">
<.inputs_for :let={question_form} field={@task_form[:questions]}>
<.question_form question_form={question_form} />
</.inputs_for>
<.add_question_btn />
</.form>
"""
end
def add_question_btn(assigns) do
~H"""
<label class="cursor-pointer mt-4 self-end">
<input type="checkbox" name="project_task[questions_sort][]" class="!hidden" />
<span class="text-sm text-indigo-blue">+ Add another question</span>
</label>
"""
end
def question_form(assigns) do
~H"""
<div class="flex flex-col relative border-bottom">
<label class="cursor-pointer absolute top-0 right-0">
<i class="fa-solid fa-trash" />
<input
type="checkbox"
name="project_task[questions_drop][]"
value={@question_form.index}
class="!hidden"
/>
</label>
<h5 class="mt-0">Question <%= @question_form.index + 1 %></h5>
<input type="hidden" name="project_task[questions_sort][]" value={@question_form.index} />
<%= Inputs.labelled_text_area(@question_form, :instruction_body, :small,
label: "Write your question instructions or steps"
) %>
<%= Inputs.labelled_text_area(@question_form, :question_body, :small,
label: "Write your question"
) %>
</div>
"""
end
end
defmodule ProjectTask do
use Ecto.Schema
alias Ecto.Changeset
schema "project_tasks" do
field(:description, :string)
has_many(:questions, ProjectTaskQuestion,
on_replace: :delete,
on_delete: :delete_all
)
timestamps(type: :utc_datetime_usec)
end
@fields [:description]
def changeset(project_task, params \\ %{}) do
project_task
|> Changeset.cast(params, @fields)
end
def update_changeset_questions(changeset) do
changeset
|> Changeset.cast_assoc(:questions,
with: &ProjectTaskQuestion.changeset/2,
sort_param: :questions_sort,
drop_param: :questions_drop
)
end
end
defmodule ProjectTaskQuestion do
use Ecto.Schema
alias Ecto.Changeset
schema "project_task_questions" do
field(:question_body, :string)
belongs_to(:project_task, ProjectTask)
timestamps(type: :utc_datetime_usec)
end
@fields [:question_body]
def changeset(project_task_question, params \\ %{}) do
project_task_question
|> Changeset.cast(params, @fields)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment