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 3 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
181 changes: 181 additions & 0 deletions lib/phoenix_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2014,6 +2014,187 @@ 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])
|> Enum.reject(fn {_, v} -> v == nil end)
LostKobrakai marked this conversation as resolved.
Show resolved Hide resolved

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 %>
<.hidden_inputs :if={!@skip_hidden} form={finner} />
LostKobrakai marked this conversation as resolved.
Show resolved Hide resolved
<%= render_slot(@inner_block, finner) %>
<% end %>
"""
end

@doc """
Renders hidden inputs for a form.

[INSERT LVATTRDOCS]

This function is built on top of `Phoenix.HTML.Form.hidden_inputs_for/1`.

## Examples

```heex
<.form
:let={f}
for={@changeset}
phx-change="change_name"
>
<.inputs_for :let={f_nested} field={{:f, :nested}} skip_hidden>
<%= text_input f_nested, :name %>
<.hidden_inputs form={f_nested} />
</.inputs_for>
</.form>
```
"""

attr.(:form, Phoenix.HTML.Form,
required: true,
doc: "A %Phoenix.HTML.Form{} struct."
)

def hidden_inputs(%{form: form} = assigns) do
LostKobrakai marked this conversation as resolved.
Show resolved Hide resolved
inputs =
Enum.flat_map(form.hidden, fn
{field, values} when is_list(values) ->
id = input_id(form, field)
name = input_name(form, field)

values
|> Enum.with_index()
|> Enum.map(fn {value, index} ->
%{
id: id <> "_" <> Integer.to_string(index),
name: name <> "[]",
value: value
}
end)

{field, value} ->
[
%{
id: input_id(form, field),
name: input_name(form, field),
value: value
}
]
end)

assigns = assign(assigns, :inputs, inputs)

~H"""
<%= for opts <- @inputs do %>
<input type="hidden" {opts} />
<% end %>
"""
end

@spec input_name(Phoenix.HTML.Form.t() | atom, atom | String.t()) :: String.t()
defp input_name(form_or_name, field)

defp input_name(%{name: nil}, field), do: to_string(field)

defp input_name(%{name: name}, field) when is_atom(field) or is_binary(field),
do: "#{name}[#{field}]"

defp input_name(name, field) when (is_atom(name) and is_atom(field)) or is_binary(field),
do: "#{name}[#{field}]"

@spec input_id(Phoenix.HTML.Form.t() | atom, atom | String.t()) :: String.t()
defp input_id(%{id: nil}, field), do: "#{field}"

defp input_id(%{id: id}, field) when is_atom(field) or is_binary(field) do
"#{id}_#{field}"
end

defp input_id(name, field) when (is_atom(name) and is_atom(field)) or is_binary(field) do
"#{name}_#{field}"
end

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

Expand Down
Loading