Search code examples
htmlformselixirphoenix-framework

Customize child index of inputs_for form output


I am trying to create a form with a list of addresses and allowing the user to delete or create new addresses through Javascript. I’ve used Rails’ fields_for with a child_index: 'new_record' for a similar functionality where I replaced the new_record string with a timestamp when appending the form fields through Javascript (as explained by this Pluralsight tutorial: https://www.pluralsight.com/guides/ruby-on-rails-nested-attributes).

The trick I am looking for is to have similar rendering functionality in Phoenix is how to generate a nested form with a given / custom child index. Currently I am struggling with something like this;

<%= form_for @changeset, Routes.user_path(@conn, :update, @user.id), [data: [controller: "nested-form", action: "nested-form#submit"]], fn f -> %>
  <div data-target="nested-form.records">
    <%= inputs_for f, :addresses, fn fp -> %>
      <%= render __MODULE__, "_address_fields.html", form: fp %>
    <% end %>
  </div>

  <template data-target="nested-form.template">
    <%# How do I get a template input group here? %>
    <%# In Rails I could do `f.fields_for :addresses, Address.new, child_index: 'NEW_RECORD'` %>
  </template>
<% end %>

But I can't figure out how to generate a inputs_for with Phoenix to create such template.


Solution

  • As @TheAnh commented, the answer is discussed in this gist (https://gist.github.com/mjrode/c2939ee7786b157aab131761c8fb89a9). To provide an answer, I've solved it with the following helper method;

    defmodule MyAppWeb.UserView do
      use MyAppWeb, :view
    
      def template_inputs_for(changeset, field, fun) do
        form = Phoenix.HTML.FormData.to_form(changeset, [])
    
        inputs_for(form, field, fn form ->
          id = String.replace(form.id, ~r/\d$/, "NEW_RECORD")
          name = String.replace(form.name, ~r/\[\d\]$/, "[NEW_RECORD]")
    
          fun.(%{form | id: id, name: name})
        end)
      end
    end
    

    Which can then be used to generate a "template" field group;

    <%= form_for @changeset, Routes.user_path(@conn, :update, @user.id), [data: [controller: "nested-form", action: "nested-form#submit"]], fn f -> %>
      <div data-target="nested-form.records">
        <%= inputs_for f, :addresses, fn fp -> %>
          <%= render __MODULE__, "_address_fields.html", form: fp %>
        <% end %>
      </div>
    
      <template data-target="nested-form.template">
        <%= template_inputs_for User.changeset(%User{addresses: [%Address{}]}), :addresses, fn f -> %>
          <%= render __MODULE__, "_address_fields.html", form: f %>
        <% end %>
      </template>
    <% end %>