Repository: phoenixframework/phoenix_html
Pull request #292 (2020-02-27 12:16:30)

Add Phoenix.HTML.Form.inputs_for/3
Fixes https://github.com/phoenixframework/phoenix_html/issues/291

TODO: 

- [x] Add docs and specs
- [x] Tests

feliperenan: This is still under development. I opened the PR because I have one question regarding hidden inputs. 

I'm not sure how to proceed here. The way I have implemented, `inputs_for/3` will not generate `hidden_inputs` automatically as `inputs_for/4` does. Developers will have to generate them on their own:

```Elixir
for {k, v} <- company_form.hidden do
  hidden_input(company_form, k, value: v)
end
```

I've two suggestions to mitigate that:

1) Accept this trade-off and leave for developers to generate them. We could also provide a public function for that, something like:

```eex
<%= for f <- inputs_for(f, :children) do %>
  <%= hidden_inputs(f) %>

  <%= input f, :name %>
<% end %>
```

2) Return the generated `hidden_fields` along with the form in a tuple format:

```eex
<%- {forms, hidden_inputs} - inputs_for(f, :children) %>
<%= for f <- forms do %>
  <%= hidden_inputs %>

  <%= input f, :name %>
<% end %>
```

Could you help me with that @josevalim 🙌 ?
josevalim: I think one of the things you need to have in mind is that someone can call `inputs_for` for a single association. In this case, if you do:

```elixir
<%= i = inputs_for(f, :child) do %>
```

I *think* the above will output a form. So first of all, we need a way to say that the no form should be outputted in this case. About the hidden inputs part though, I agree with you, I think it should be a separate function that users will invoke.
feliperenan: I'm not sure if I got this:

> I think the above will output a form.

The way it is, if you call it like you said:

```eex
<%= i = inputs_for(f, :child) %>
```

You will get the following error:

```
** (ArgumentError) lists in Phoenix.HTML and templates may only contain integers representing bytes, binaries or other lists, got invalid entry: %Phoenix.HTML.Form{...}
```

That's because `i` will always (as far as I can tell) be list even though it's an single association. So the correct way would be either:

```eex
# Notice that I'm using <%- instead of <%=. If you use <%= here, you will get the same error above.
<%- [i] = inputs_for(f, :child) %>
```

or

```eex
<%= for i <- inputs_for(f, :child) do %>
```

Does that sound right for you or am I missing something?

josevalim: > Does that sound ok for you or am I missing something? 🤔

Oops, I forgot it always returned lists. So we are good!
josevalim: > Does that sound ok for you or am I missing something? 🤔

Oops, I forgot it always returned lists. So we are good!
feliperenan: @josevalim It is ready for review. I have also introduced [this function](https://github.com/phoenixframework/phoenix_html/pull/292/files#diff-149975a43f9e90f073bd077072ba7d50R655) to help with hidden inputs.
feliperenan: lol fixing the wrong rebase! :see_no_evil: 

Patch:
diff --git a/lib/phoenix_html/form.ex b/lib/phoenix_html/form.ex
index 56ad72c..88696fc 100644
--- a/lib/phoenix_html/form.ex
+++ b/lib/phoenix_html/form.ex
@@ -372,6 +372,46 @@ defmodule Phoenix.HTML.Form do
     html_escape([form_tag(action, options), fun.(form), raw("</form>")])
   end
 
+  @doc """
+  Same as `inputs_for(form, field, [])`.
+  """
+  @spec inputs_for(t, field) :: list(Phoenix.HTML.Form.t())
+  def inputs_for(form, field) when is_atom(field), do: inputs_for(form, field, [])
+
+  @doc """
+  Generate a new form builder for the given parameter in form **without** an
+  anonymous function.
+
+  This functionality exists mostly for integration with `Phoenix.LiveView`
+  that replaces the anonymous function for returning the generated forms
+  instead.
+
+  Keep in mind that this function does not generate hidden inputs automatically
+  like `inputs/4`. To generate them you need to explicit do it by yourself.
+
+      <%= f = form_for @changeset, Routes.user_path(@conn, :create), opts %>
+        Name: <%= text_input f, :name %>
+
+        <%= for friend_form = inputs_for(f, :friends) do %>
+          # for generating hidden inputs.
+          <%= hidden_inputs_for(company_form) %>
+          <%= text_input friend_form, :name %>
+        <% end %>
+      </form>
+
+  See `inputs_for/4` for the available options.
+  """
+  @spec inputs_for(t, field, Keyword.t()) :: list(Phoenix.HTML.Form.t())
+  def inputs_for(%{impl: impl} = form, field, options)
+      when is_atom(field) and is_list(options) do
+    options =
+      form.options
+      |> Keyword.take([:multipart])
+      |> Keyword.merge(options)
+
+    impl.to_form(form.source, form, field, options)
+  end
+
   @doc """
   Generate a new form builder for the given parameter in form.
 
@@ -418,8 +458,7 @@ defmodule Phoenix.HTML.Form do
         if skip do
           fun.(form)
         else
-          hidden = Enum.map(form.hidden, fn {k, v} -> hidden_input(form, k, value: v) end)
-          [hidden, fun.(form)]
+          [hidden_inputs_for(form), fun.(form)]
         end
       end)
     )
@@ -598,6 +637,16 @@ defmodule Phoenix.HTML.Form do
     generic_input(:hidden, form, field, opts)
   end
 
+  @doc """
+  Generates hidden inputs for the given form.
+  """
+  @spec hidden_inputs_for(t) :: list(Phoenix.HTML.safe())
+  def hidden_inputs_for(form) do
+    for {k, v} <- form.hidden do
+      hidden_input(form, k, value: v)
+    end
+  end
+
   @doc """
   Generates an email input.
 


Test patch:
diff --git a/test/phoenix_html/form_test.exs b/test/phoenix_html/form_test.exs
index e8288eb..e800146 100644
--- a/test/phoenix_html/form_test.exs
+++ b/test/phoenix_html/form_test.exs
@@ -259,6 +259,36 @@ defmodule Phoenix.HTML.FormTest do
     end
   end
 
+  describe "inputs_for/3" do
+    test "generate a new form builder for the given parameter" do
+      conn = conn()
+
+      form =
+        form_for(conn, "/", [as: :user], fn form ->
+          for company_form <- inputs_for(form, :company) do
+            text_input(company_form, :name)
+          end
+        end)
+        |> safe_to_string()
+
+      assert form =~ ~s(<input id="user_company_name" name="user[company][name]" type="text">)
+    end
+
+    test "support options" do
+      conn = conn()
+
+      form =
+        form_for(conn, "/", [as: :user], fn form ->
+          for company_form <- inputs_for(form, :company, as: :new_company, id: :custom_id) do
+            text_input(company_form, :name)
+          end
+        end)
+        |> safe_to_string()
+
+      assert form =~ ~s(<input id="custom_id_name" name="new_company[name]" type="text">)
+    end
+  end
+
   describe "inputs_for/4" do
     test "generate a new form builder for the given parameter" do
       conn = conn()
@@ -407,6 +437,14 @@ defmodule Phoenix.HTML.FormTest do
              ~s(<input id="key" name="search[key][]" type="hidden" value="foo">)
   end
 
+  describe "hidden_inputs_for/1" do
+    test "generates hidden fields from the given form" do
+      form = %{form_for(conn(), "/") | hidden: [id: 1]}
+
+      assert hidden_inputs_for(form) == [hidden_input(form, :id, value: 1)]
+    end
+  end
+
   ## email_input/3
 
   test "email_input/3" do