I’m trying to get nested form_for attributes in LV (phoenix 1.8 rc 0, liveview 1.0) by following the patterns in `inputs_for/1 but I’m not having any luck.
The associations:
#Job can have many accomplishments, accomplishments can have many sub-accomplishments
defmodule Resume.Jobs.Job do
use Ecto.Schema
import Ecto.Changeset
schema "jobs" do
field :title, :string
field :company, :string
field :start_date, :date
field :end_date, :date
has_many :accomplishments, Resume.Accomplishments.Accomplishment, on_replace: :delete
timestamps(type: :utc_datetime)
end
def all_in_one_changeset(job, attrs) do
job
|> cast(attrs, [:title, :company, :start_date, :end_date])
|> validate_required([:title, :company, :start_date, :end_date])
|> cast_assoc(:accomplishments,
with: &Resume.Accomplishments.Accomplishment.changeset_from_job/2,
sort_param: :accomp_sort,
drop_param: :accomp_drop
)
end
end
defmodule Resume.Accomplishments.Accomplishment do
use Ecto.Schema
import Ecto.Changeset
schema "accomplishments" do
field :name, :string
field :description, :string
field :job_id, :id
has_many :sub_accomplishments, Resume.SubAccomplishments.SubAccomplishment
timestamps(type: :utc_datetime)
end
@doc """
Used by job when casting with associated accomplishments.
"""
def changeset_from_job(accomplishment, attrs) do
accomplishment
|> cast(attrs, [:name, :description])
|> validate_required([:name, :description])
|> cast_assoc(
:sub_accomplishments,
with: &Resume.SubAccomplishments.SubAccomplishment.changeset_from_accomplishment/2,
sort_param: :sub_accomp_sort,
drop_param: :sub_accomp_drop
)
end
end
defmodule Resume.SubAccomplishments.SubAccomplishment do
use Ecto.Schema
import Ecto.Changeset
schema "subaccomplishments" do
field :name, :string
field :description, :string
field :accomplishment_id, :id
timestamps(type: :utc_datetime)
end
@doc """
Used by accomplishment when casting sub_accomplishment.
"""
def changeset_from_accomplishment(sub_accomplishment, attrs) do
sub_accomplishment
|> cast(attrs, [:name, :description])
|> validate_required([:name, :description])
end
end
Following the guide in the docs, I was able to get a dynamic form that allowed adding multiple accomplishments to jobs. My naive approach was to nest the inputs_for sub-accomplishments inside of the inputs_for for accomplishments:
def render(assigns) do
~H"""
<.form for={@form} id="job-form" phx-change="validate" phx-submit="save">
<.input field={@form[:title]} type="text" label="Title" />
<.input field={@form[:company]} type="text" label="Company" />
<.input field={@form[:start_date]} type="date" label="Start date" />
<.input field={@form[:end_date]} type="date" label="End date" />
<.inputs_for :let={accomp} field={@form[:accomplishments]}>
<input type="hidden" name="job[accomp_sort][]" value={accomp.index} />
<.input type="text" field={accomp[:name]} label="name" />
<.input type="text" field={accomp[:description]} label="description" />
<.inputs_for :let={sub_accomp} field={accomp[:sub_accomplishments]}>
<input type="text" name="accomp-index" value="1" />
<input type="hidden" name="accomplishments[sub_accomp_sort][]" value={sub_accomp.index} />
<.input type="text" field={sub_accomp[:name]} label="name" />
<.input type="text" field={sub_accomp[:description]} label="description" />
<div class="mt-2 mb-4 w-30">
<button
type="button"
name="accomplishments[sub_accomp_drop][]"
value={sub_accomp.index}
class="btn btn-secondary w-full"
phx-click={JS.dispatch("change")}
>
Remove
</button>
</div>
</.inputs_for>
<input type="hidden" name="accomplishments[sub_accomp_drop][]" />
<div class="mt-4 mb-6 w-30">
<button
type="button"
name="accomplishments[sub_accomp_sort][]"
value={"new-#{accomp.index}"}
class="btn w-full"
phx-click={JS.dispatch("change")}
phx-value-accompid="1"
>
Add subaccomp
</button>
</div>
<div class="mt-2 mb-4 w-30">
<button
type="button"
name="job[accomp_drop][]"
value={accomp.index}
class="btn btn-secondary w-full"
phx-click={JS.dispatch("change")}
>
Remove
</button>
</div>
</.inputs_for>
<input type="hidden" name="job[accomp_drop][]" />m, to_form(Jobs.change_with_all_children(job)))
end
<div class="mt-4 mb-6 w-30">
<button
type="button"
name="job[accomp_sort][]"
value="new"
class="btn w-full"
phx-click={JS.dispatch("change")}
>
Add accomp
</button>
</div>
<footer>
<.button phx-disable-with="Saving..." variant="primary">Save Job</.button>
<.button navigate={return_path(@current_scope, @return_to, @job)}>Cancel</.button>
</footer>
</.form>
end
@impl true
def mount(params, _session, socket) do
{:ok,
socket
|> assign(:return_to, return_to(params["return_to"]))m, to_form(Jobs.change_with_all_children(job)))
end
|> apply_action(socket.assigns.live_action, params)}
end
defp apply_action(socket, :edit, %{"id" => id}) do
job = Jobs.get_job!(socket.assigns.current_scope, id)
socket
|> assign(:page_title, "Edit Job")
|> assign(:job, job)
|> assign(:form, to_form(Jobs.change_with_all_children(job)))
end
def handle_event("validate", %{"job" => job_params} = full_params, socket) do
changeset = Jobs.change_with_all_children(socket.assigns.job, job_params)
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
end
"""
What I have noticed is that the sub_accomplishments are sent in the params under a separate “accomplishments” key from the rest of the “job” form. The result is that the sub-accomplishments sections are never rendered.
I’ve been trying to merge the accomplishments parameters into the job parameters which do contain an “accomplishments” key, but keeping the index of the parent accomplishment in sync with the child sub-accomplishments feels like I’m swimming against the current.
Hopefully I’m missing something very obvious.