Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.inputs_for component #2411

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 4 additions & 17 deletions guides/client/form-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,28 +169,15 @@ requires explicitly setting the `:value` in your markup, for example:

## Nested inputs

Nested inputs are handled using `inputs_for` form helpers. There are two versions
of `inputs_for` - one that takes an anonymous function and one that doesn't. The version
that takes an anonymous function won't work properly with LiveView as it prevents rendering
of LiveComponents. Instead of using this:
Nested inputs are handled using `.inputs_for` function component. By default
it will add the necessary hidden input fields for tracking ids of Ecto associations.

```heex
<%= inputs_for f, :friend, fn fp -> %>
<%= text_input fp, :url %>
<% end %>
```

you should use this:

```heex
<%= for fp <- inputs_for(f, :friends) do %>
<%= hidden_inputs_for(fp) %>
<.inputs_for :let={fp} field={{f, :friends}}>
<%= text_input fp, :name %>
<% end %>
</.inputs_for>
```

Note that you will need to include a call to `hidden_inputs_for` as the version of inputs_for that does not take an anonymous function also does not automatically generate any necessary hidden fields for tracking ids of Ecto associations.

## File inputs

LiveView forms support [reactive file inputs](uploads.md),
Expand Down
106 changes: 106 additions & 0 deletions lib/phoenix_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2014,6 +2014,112 @@ defmodule Phoenix.Component do
defp form_method(method) when method in ~w(get post), do: {method, nil}
defp form_method(method) when is_binary(method), do: {"post", method}

@doc """
Renders nested form inputs for associations or embeds.

[INSERT LVATTRDOCS]

This function is built on top of `Phoenix.HTML.Form.inputs_for/3`.

For more information about options and how to build inputs, see `Phoenix.HTML.Form`.

## Examples

```heex
<.form
:let={f}
for={@changeset}
phx-change="change_name"
>
<.inputs_for :let={f_nested} field={{:f, :nested}}>
<%= text_input f_nested, :name %>
</.inputs_for>
</.form>
```
"""
attr.(:field, :any,
required: true,
doc: "A %Phoenix.HTML.Form{}/field name tuple, for example: {f, :email}."
)

attr.(:id, :string,
doc: """
The id to be used in the form, defaults to the concatenation of the given
field to the parent form id.
"""
)

attr.(:as, :atom,
doc: """
The name to be used in the form, defaults to the concatenation of the given
field to the parent form name.
"""
)

attr.(:default, :any, doc: "The value to use if none is available.")

attr.(:prepend, :list,
doc: """
The values to prepend when rendering. This only applies if the field value
is a list and no parameters were sent through the form.
"""
)

attr.(:append, :list,
doc: """
The values to append when rendering. This only applies if the field value
is a list and no parameters were sent through the form.
"""
)

attr.(:skip_hidden, :boolean,
default: false,
doc: """
Skip the automatic rendering of hidden fields to allow for more tight control
over the generated markup. You can access `form.hidden` or use `.hidden_inputs/1`
to generate them manually.
"""
)

slot.(:inner_block, required: true, doc: "The content rendered for each nested form.")

def inputs_for(assigns) do
{form, field} = assigns[:field] || raise ArgumentError, "missing :field assign to inputs_for"

options = assigns |> Map.take([:id, :as, :default, :append, :prepend]) |> Keyword.new()

options =
form.options
|> Keyword.take([:multipart])
|> Keyword.merge(options)

assigns =
assigns
|> assign(:field, nil)
|> assign(:forms, form.impl.to_form(form.source, form, field, options))

~H"""
<%= for finner <- @forms do %>
<%= unless @skip_hidden do %>
<%= for {name, value_or_values} <- finner.hidden,
name = name_for_value_or_values(finner, name, value_or_values),
value <- List.wrap(value_or_values) do %>
<input type="hidden" name={name} value={value} />
<% end %>
<% end %>
<%= render_slot(@inner_block, finner) %>
<% end %>
"""
end

defp name_for_value_or_values(form, field, values) when is_list(values) do
Phoenix.HTML.Form.input_name(form, field) <> "[]"
end

defp name_for_value_or_values(form, field, _value) do
Phoenix.HTML.Form.input_name(form, field)
end

@doc """
Generates a link for live and href navigation.

Expand Down
147 changes: 147 additions & 0 deletions test/phoenix_component/components_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,153 @@ defmodule Phoenix.LiveView.ComponentsTest do
end
end

describe "inputs_for" do
test "raises when missing required assigns" do
assert_raise ArgumentError, ~r/missing :field assign/, fn ->
assigns = %{}

template = ~H"""
<.form :let={_f} for={:myform}>
<.inputs_for :let={finner}>
<%= text_input finner, :foo %>
</.inputs_for>
</.form>
"""

parse(template)
end
end

test "generates nested inputs with no options" do
assigns = %{}

template = ~H"""
<.form :let={f} for={:myform}>
<.inputs_for :let={finner} field={{f, :inner}}>
<%= text_input finner, :foo %>
</.inputs_for>
</.form>
"""

html = parse(template)

assert [
{"form", [],
[
{"input",
[
{"id", "myform_inner_foo"},
{"name", "myform[inner][foo]"},
{"type", "text"}
], []}
]}
] = html
end

test "with naming options" do
assigns = %{}

template = ~H"""
<.form :let={f} for={:myform}>
<.inputs_for :let={finner} field={{f, :inner}} id="test" as={:name}>
<%= text_input finner, :foo %>
</.inputs_for>
</.form>
"""

html = parse(template)

assert [
{"form", [],
[
{"input",
[
{"id", "test_foo"},
{"name", "name[foo]"},
{"type", "text"}
], []}
]}
] = html
end

test "with default map option" do
assigns = %{}

template = ~H"""
<.form :let={f} for={:myform}>
<.inputs_for :let={finner} field={{f, :inner}} default={%{foo: "123"}}>
<%= text_input finner, :foo %>
</.inputs_for>
</.form>
"""

html = parse(template)

assert [
{"form", [],
[
{"input",
[
{"id", "myform_inner_foo"},
{"name", "myform[inner][foo]"},
{"type", "text"},
{"value", "123"}
], []}
]}
] = html
end

test "with default list and list related options" do
assigns = %{}

template = ~H"""
<.form :let={f} for={:myform}>
<.inputs_for
:let={finner}
field={{f, :inner}}
default={[%{foo: "456"}]}
prepend={[%{foo: "123"}]}
append={[%{foo: "789"}]}
>
<%= text_input finner, :foo %>
</.inputs_for>
</.form>
"""

html = parse(template)

assert [
{"form", [],
[
{
"input",
[
{"id", "myform_inner_0_foo"},
{"name", "myform[inner][0][foo]"},
{"type", "text"},
{"value", "123"}
],
[]
},
{"input",
[
{"id", "myform_inner_1_foo"},
{"name", "myform[inner][1][foo]"},
{"type", "text"},
{"value", "456"}
], []},
{"input",
[
{"id", "myform_inner_2_foo"},
{"name", "myform[inner][2][foo]"},
{"type", "text"},
{"value", "789"}
], []}
]}
] = html
end
end

describe "live_file_input/1" do
test "renders attributes" do
assigns = %{
Expand Down