Repository: phoenixframework/phoenix_html (elixir)

Issue:
`inputs_for` option to not automatically generate the id field
Currently a structure like the following is not possible using `inputs_for` because the hidden id input is always added as sibling to the user defined markup. Maybe the markup for the id input could optionally be supplied to the callback, so the user can place it somewhere.

```eex
<ol>
  <%= inputs_for f, :fields, fn finner -> %>
    <li>[…]</li>
  <% end %>
</ol>
```

```html
<ol>
  <input type="hidden" …>
  <li>[…]</li>

  <input type="hidden" …>
  <li>[…]</li>

  <input type="hidden" …>
  <li>[…]</li>
</ol>
```


Pull request description:
Allow `inputs_for` to skip hidden fields for more control over the final markup
Fixes #258

Continue the @LostKobrakai work https://github.com/phoenixframework/phoenix_html/pull/259

Patch:
diff --git a/lib/phoenix_html/form.ex b/lib/phoenix_html/form.ex
index d4d925a..e6c80e2 100644
--- a/lib/phoenix_html/form.ex
+++ b/lib/phoenix_html/form.ex
@@ -400,10 +400,17 @@ defmodule Phoenix.HTML.Form do
       applies if the field value is a list and no parameters were
       sent through the form.
 
+    * `:skip_hidden` - skip the automatic rendering of hidden
+      fields to allow for more tight control over the generated
+      markup. You can use `hidden_form_inputs(form)` to
+      generate them manually within the supplied callback.
+
   """
   @spec inputs_for(t, field, Keyword.t(), (t -> Phoenix.HTML.unsafe())) :: Phoenix.HTML.safe()
   def inputs_for(%{impl: impl} = form, field, options \\ [], fun)
       when is_atom(field) or is_binary(field) do
+    {skip, options} = Keyword.pop(options, :skip_hidden, false)
+
     options =
       form.options
       |> Keyword.take([:multipart])
@@ -413,12 +420,21 @@ defmodule Phoenix.HTML.Form do
 
     html_escape(
       Enum.map(forms, fn form ->
-        hidden = Enum.map(form.hidden, fn {k, v} -> hidden_input(form, k, value: v) end)
-        [hidden, fun.(form)]
+        if skip do
+          fun.(form)
+        else
+          [hidden_form_inputs(form), fun.(form)]
+        end
       end)
     )
   end
 
+  @doc false
+  @spec hidden_form_inputs(t) :: [Phoenix.HTML.safe()]
+  def hidden_form_inputs(form) do
+    Enum.map(form.hidden, fn {k, v} -> hidden_input(form, k, value: v) end)
+  end
+
   @doc """
   Returns a value of a corresponding form field.
 
@@ -1384,7 +1400,7 @@ defmodule Phoenix.HTML.Form do
   """
   def date_select(form, field, opts \\ []) do
     value = Keyword.get(opts, :value, input_value(form, field) || Keyword.get(opts, :default))
-    builder = Keyword.get(opts, :builder) || &date_builder(&1, opts)
+    builder = Keyword.get(opts, :builder) || (&date_builder(&1, opts))
     builder.(datetime_builder(form, field, date_value(value), nil, opts))
   end
 
@@ -1418,7 +1434,7 @@ defmodule Phoenix.HTML.Form do
   """
   def time_select(form, field, opts \\ []) do
     value = Keyword.get(opts, :value, input_value(form, field) || Keyword.get(opts, :default))
-    builder = Keyword.get(opts, :builder) || &time_builder(&1, opts)
+    builder = Keyword.get(opts, :builder) || (&time_builder(&1, opts))
     builder.(datetime_builder(form, field, nil, time_value(value), opts))
   end
 
diff --git a/lib/phoenix_html/form_data.ex b/lib/phoenix_html/form_data.ex
index cb1bd7a..b1ab70e 100644
--- a/lib/phoenix_html/form_data.ex
+++ b/lib/phoenix_html/form_data.ex
@@ -91,6 +91,7 @@ defimpl Phoenix.HTML.FormData, for: [Plug.Conn, Atom] do
     {append, opts} = Keyword.pop(opts, :append, [])
     {name, opts} = Keyword.pop(opts, :as)
     {id, opts} = Keyword.pop(opts, :id)
+    {hidden, opts} = Keyword.pop(opts, :hidden, [])
 
     id = to_string(id || form.id <> "_#{field}")
     name = to_string(name || form.name <> "[#{field}]")
@@ -107,6 +108,7 @@ defimpl Phoenix.HTML.FormData, for: [Plug.Conn, Atom] do
             name: name,
             data: default,
             params: params || %{},
+            hidden: hidden,
             options: opts
           }
         ]
@@ -133,6 +135,7 @@ defimpl Phoenix.HTML.FormData, for: [Plug.Conn, Atom] do
             name: name <> "[" <> index_string <> "]",
             data: data,
             params: params,
+            hidden: hidden,
             options: opts
           }
         end


Test patch:
diff --git a/test/phoenix_html/form_test.exs b/test/phoenix_html/form_test.exs
index d43cdf2..63e7990 100644
--- a/test/phoenix_html/form_test.exs
+++ b/test/phoenix_html/form_test.exs
@@ -260,6 +260,53 @@ defmodule Phoenix.HTML.FormTest do
     end
   end
 
+  describe "inputs_for/4" do
+    test "generate a new form builder for the given parameter" do
+      conn = conn()
+
+      form =
+          form_for(conn, "/", [as: :user], fn form ->
+            inputs_for(form, :company, fn company_form ->
+              text_input company_form, :name
+            end)
+          end)
+          |> safe_to_string()
+
+      assert form =~ ~s(<input name="_utf8" type="hidden" value="✓">)
+      assert form =~ ~s(<input id="user_company_name" name="user[company][name]" type="text">)
+    end
+
+    test "generate a new form builder with hidden inputs when they are present" do
+      conn = conn()
+
+      form =
+          form_for(conn, "/", [as: :user], fn form ->
+            inputs_for(form, :company, [hidden: [id: 1]],fn company_form ->
+              text_input company_form, :name
+            end)
+          end)
+          |> safe_to_string()
+
+      assert form =~ ~s(input id="user_company_id" name="user[company][id]" type="hidden" value="1">)
+      assert form =~ ~s(<input id="user_company_name" name="user[company][name]" type="text">)
+    end
+
+    test "skip hidden inputs" do
+      conn = conn()
+
+      form =
+          form_for(conn, "/", [as: :user], fn form ->
+            inputs_for(form, :company, [skip_hidden: true, hidden: [id: 1]],fn company_form ->
+              text_input company_form, :name
+            end)
+          end)
+          |> safe_to_string()
+
+      refute form =~ ~s(input id="user_company_id" name="user[company][id]" type="hidden" value="1">)
+      assert form =~ ~s(<input id="user_company_name" name="user[company][name]" type="text">)
+    end
+  end
+
   ## text_input/3
 
   test "text_input/3" do