windexoriginal

windexoriginal

Nesting .inputs_for/1 in a dyanmic form, what is the right approach?

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.

Marked As Solved

LostKobrakai

LostKobrakai

See the answer here:

Where Next?

Popular in Questions Top

chokchit
** (DBConnection.ConnectionError) connection not available and request was dropped from queue after 2733ms. You can configure how long re...
New
nobody
How to bind a phoenix app to a specific ip address? could not find anything about that, nowhere, unfortunately, but for me this is quite...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
stefanchrobot
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
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
yawaramin
In the Dialyzer docs ( dialyzer — OTP 29.0.2 (dialyzer 6.0.1) ), there is a way to turn off a specific warning for a function: -dialyzer...
New
vegabook
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

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29603 241
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
electic
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
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31013 112
New
ovidiubadita
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
AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31265 143
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement