How to properly create nested form for 1-N relations?

I followed this tutorial: Nested model forms with Phoenix LiveView - Tutorials and screencasts for Elixir, Phoenix and LiveView

I have a schema called Folha, defined as:

  schema "folhas" do
    field :competencia, :string

    belongs_to :empresa, Empresa
    has_many :itens, ItensFolha

    timestamps()
  end

  @doc false
  def changeset(folha, attrs) do
    folha
    |> cast(attrs, [:competencia, :empresa_id])
    |> validate_required([:competencia, :empresa_id])
    |> parse_string(:competencia)
    |> foreign_key_constraint(:empresa_id)
    |> cast_assoc(:itens, required: true, with: &ItensFolha.changeset/2)
  end

And the ItensFolha is defined as:

  schema "itens_folha" do
    field :adicional_noturno, :decimal
    field :bonificacao, :decimal
    field :descontos, :decimal
    field :dias_faltas, :integer
    field :finalizado?, :boolean, default: false
    field :hora_extra, :decimal

    # fields to enable nested form
    field :temp_id, :string, virtual: true
    field :delete, :boolean, virtual: true

    belongs_to :folha, Folha
    belongs_to :funcionario, Funcionario

    timestamps()
  end

  @doc false
  def changeset(itens_folha, attrs) do
    itens_folha
    |> cast(attrs, @fields)
    |> validate_required(@required_fields)
    |> foreign_key_constraint(:funcionario)
    |> maybe_mark_for_deletion()
  end

  defp maybe_mark_for_deletion(%{data: %{id: nil}} = changeset), do: changeset

  defp maybe_mark_for_deletion(changeset) do
    if get_change(changeset, :delete) do
      %{changeset | action: :delete}
    else
      changeset
    end
  end

On the live view side, I have the scaffold files: index.ex, show.ex, form_component.ex and it’s templates!

My Folha form is:

<h2><%= @title %></h2>

<%= f = form_for @changeset, "#",
  id: "folha-form",
  phx_target: @myself,
  phx_change: "validate",
  phx_submit: "save" %>
  <%= label f, :competencia %>
  <%= text_input f, :competencia %>
  <%= error_tag f, :competencia %>

  <%= label f, :empresa_id %>
  <%= text_input f, :empresa_id %>
  <%= error_tag f, :empresa_id %>

  <p>Item Folha</p>    
  
  <%= inputs_for f, :itens, fn i -> %>
    <%= label i, :dias_faltas %>
    <%= number_input i, :dias_faltas %>
    <%= error_tag i, :dias_faltas %>

    <%= label i, :hora_extra %>
    <%= number_input i, :hora_extra, step: "any" %>
    <%= error_tag i, :hora_extra %>

    <%= label i, :adicional_noturno %>
    <%= number_input i, :adicional_noturno, step: "any" %>
    <%= error_tag i, :adicional_noturno %>

    <%= label i, :descontos %>
    <%= number_input i, :descontos, step: "any" %>
    <%= error_tag i, :descontos %>

    <%= label i, :finalizado? %>
    <%= checkbox i, :finalizado? %>
    <%= error_tag i, :finalizado? %>

    <%= label i, :bonificacao %>
    <%= number_input i, :bonificacao, step: "any" %>
    <%= error_tag i, :bonificacao %>

    <%= label i, :funcionario_id %>
    <%= text_input i, :funcionario_id %>
    <%= error_tag i, :funcionario_id %>

    <%= if is_nil(i.data.temp_id) do %>
      <%= checkbox i, :delete %>
    <% else %>
      <%= hidden_input i, :temp_id %>
      <a href="#" phx-click="remove-item" phx-value-remove="<%= i.data.temp_id %>">&times</a>
    <% end %>
  <% end %>

  <a href="#" phx-click="add-item">Add a item folha</a>
  
  <%= submit "Save", phx_disable_with: "Saving..." %>
</form>

However, where I put the handle_event/3 “add-item” and “remove_item” clauses?

I tried to put on form_component.ex but when I clicked the button, it throws a “validate” event…
So I tried to put on index.ex, however, the socket on this live view does not have the field changeset, so I can’t to retrieve existing items. What I’m doing wrong?

I created a gist with some code: https://gist.github.com/a0e7230f312fb50fe681b984bac9b7a6

I also have all templates and live views for the ItensFolha schema, could I “embed” one form_component into another?

Also, my test fails as:

1) test Index saves new folha (ContsWeb.FolhaLiveTest)
     test/conts_web/live/folha_live_test.exs:34
     ** (ArgumentError) could not find non-disabled input, select or textarea with name "folha[itens][]" within:

         <input name="_csrf_token" type="hidden" value="ACJTGywjKjwjHhgWHSJNEToZXmsgfTo1Vo2yIMyef-oRSVusVAi8O1R_"/>
         <input id="folha-form_competencia" name="folha[competencia]" type="text" value=""/>
         <input id="folha-form_empresa_id" name="folha[empresa_id]" type="text" value=""/>

     code: |> render_submit()
     stacktrace:
       (phoenix_live_view 0.15.4) lib/phoenix_live_view/test/live_view_test.ex:885: Phoenix.LiveViewTest.call/2
       test/conts_web/live/folha_live_test.exs:52: (test)
1 Like

I struggled with this too looking at the same tutorial! What you need is the phx-target attribute on your add and remove links. You can see more about this in the docs. Now you can add your handle_events to the form component module, which will have access to the changeset in the socket assigns i.e. socket.assigns.changeset! Something like <a href="#" phx-click="add-item" phx-target="<%= @myself %>">Add a item folha</a> should do the trick!