1851 lines
57 KiB
Elixir
1851 lines
57 KiB
Elixir
defmodule Phoenix.HTML.Form do
|
|
# TODO: Remove action field from Form
|
|
# TODO: Keep only map implementation for form data
|
|
|
|
@moduledoc ~S"""
|
|
Define a `Phoenix.HTML.Form` struct and functions to interact with it.
|
|
|
|
## Access behaviour
|
|
|
|
The `Phoenix.HTML.Form` struct implements the `Access` behaviour.
|
|
When you do `form[field]`, it returns a `Phoenix.HTML.FormField`
|
|
struct with the `id`, `name`, `value`, and `errors` prefilled.
|
|
|
|
The field name can be either an atom or a string. If it is an atom,
|
|
it assumes the form keeps both data and errors as atoms. If it is a
|
|
string, it considers that data and errors are stored as strings for said
|
|
field. Forms backed by an `Ecto.Changeset` only support atom field names.
|
|
|
|
It is possible to "access" fields which do not exist in the source data
|
|
structure. A `Phoenix.HTML.FormField` struct will be dynamically created
|
|
with some attributes such as `name` and `id` populated.
|
|
"""
|
|
|
|
alias Phoenix.HTML.Form
|
|
import Phoenix.HTML
|
|
import Phoenix.HTML.Tag
|
|
|
|
@doc """
|
|
Defines the Phoenix.HTML.Form struct.
|
|
|
|
Its fields are:
|
|
|
|
* `:source` - the data structure given to `form_for/4` that
|
|
implements the form data protocol
|
|
|
|
* `:impl` - the module with the form data protocol implementation.
|
|
This is used to avoid multiple protocol dispatches.
|
|
|
|
* `:id` - the id to be used when generating input fields
|
|
|
|
* `:index` - the index of the struct in the form
|
|
|
|
* `:name` - the name to be used when generating input fields
|
|
|
|
* `:data` - the field used to store lookup data
|
|
|
|
* `:params` - the parameters associated with this form
|
|
|
|
* `:hidden` - a keyword list of fields that are required to
|
|
submit the form behind the scenes as hidden inputs
|
|
|
|
* `:options` - a copy of the options given when creating the
|
|
form via `form_for/4` without any form data specific key
|
|
|
|
* `:errors` - a keyword list of errors that are associated with
|
|
the form
|
|
"""
|
|
defstruct source: nil,
|
|
impl: nil,
|
|
id: nil,
|
|
name: nil,
|
|
data: nil,
|
|
action: nil,
|
|
hidden: [],
|
|
params: %{},
|
|
errors: [],
|
|
options: [],
|
|
index: nil
|
|
|
|
@type t :: %Form{
|
|
source: Phoenix.HTML.FormData.t(),
|
|
name: String.t(),
|
|
data: %{field => term},
|
|
action: nil | String.t() | atom(),
|
|
params: %{binary => term},
|
|
hidden: Keyword.t(),
|
|
options: Keyword.t(),
|
|
errors: [{field, term}],
|
|
impl: module,
|
|
id: String.t(),
|
|
index: nil | non_neg_integer
|
|
}
|
|
|
|
@type field :: atom | String.t()
|
|
|
|
@doc false
|
|
def fetch(%Form{} = form, field) when is_atom(field) do
|
|
fetch(form, field, Atom.to_string(field))
|
|
end
|
|
|
|
def fetch(%Form{} = form, field) when is_binary(field) do
|
|
fetch(form, field, field)
|
|
end
|
|
|
|
def fetch(%Form{}, field) do
|
|
raise ArgumentError,
|
|
"accessing a form with form[field] requires the field to be an atom or a string, got: #{inspect(field)}"
|
|
end
|
|
|
|
defp fetch(%{errors: errors} = form, field, field_as_string) do
|
|
{:ok,
|
|
%Phoenix.HTML.FormField{
|
|
errors: field_errors(errors, field),
|
|
field: field,
|
|
form: form,
|
|
id: input_id(form, field_as_string),
|
|
name: input_name(form, field_as_string),
|
|
value: input_value(form, field)
|
|
}}
|
|
end
|
|
|
|
@doc """
|
|
Returns a value of a corresponding form field.
|
|
|
|
The `form` should either be a `Phoenix.HTML.Form` or an atom.
|
|
The field is either a string or an atom. If the field is given
|
|
as an atom, it will attempt to look data with atom keys. If
|
|
a string, it will look data with string keys.
|
|
|
|
When a form is given, it will look for changes, then
|
|
fallback to parameters, and finally fallback to the default
|
|
struct/map value.
|
|
|
|
Since the function looks up parameter values too, there is
|
|
no guarantee that the value will have a certain type. For
|
|
example, a boolean field will be sent as "false" as a
|
|
parameter, and this function will return it as is. If you
|
|
need to normalize the result of `input_value`, see
|
|
`normalize_value/2`.
|
|
"""
|
|
@spec input_value(t | atom, field) :: term
|
|
def input_value(%{source: source, impl: impl} = form, field)
|
|
when is_atom(field) or is_binary(field) do
|
|
impl.input_value(source, form, field)
|
|
end
|
|
|
|
def input_value(name, _field) when is_atom(name), do: nil
|
|
|
|
@doc """
|
|
Returns an id of a corresponding form field.
|
|
|
|
The form should either be a `Phoenix.HTML.Form` emitted
|
|
by `form_for` or an atom.
|
|
"""
|
|
@spec input_id(t | atom, field) :: String.t()
|
|
def input_id(%{id: nil}, field), do: "#{field}"
|
|
|
|
def input_id(%{id: id}, field) when is_atom(field) or is_binary(field) do
|
|
"#{id}_#{field}"
|
|
end
|
|
|
|
def input_id(name, field) when (is_atom(name) and is_atom(field)) or is_binary(field) do
|
|
"#{name}_#{field}"
|
|
end
|
|
|
|
@doc """
|
|
Returns an id of a corresponding form field and value attached to it.
|
|
|
|
Useful for radio buttons and inputs like multiselect checkboxes.
|
|
"""
|
|
@spec input_id(t | atom, field, Phoenix.HTML.Safe.t()) :: String.t()
|
|
def input_id(name, field, value) do
|
|
{:safe, value} = html_escape(value)
|
|
value_id = value |> IO.iodata_to_binary() |> String.replace(~r/\W/u, "_")
|
|
input_id(name, field) <> "_" <> value_id
|
|
end
|
|
|
|
@doc """
|
|
Returns a name of a corresponding form field.
|
|
|
|
The first argument should either be a `Phoenix.HTML.Form` or an atom.
|
|
|
|
## Examples
|
|
|
|
iex> Phoenix.HTML.Form.input_name(:user, :first_name)
|
|
"user[first_name]"
|
|
"""
|
|
@spec input_name(t | atom, field) :: String.t()
|
|
def input_name(form_or_name, field)
|
|
|
|
def input_name(%{name: nil}, field), do: to_string(field)
|
|
|
|
def input_name(%{name: name}, field) when is_atom(field) or is_binary(field),
|
|
do: "#{name}[#{field}]"
|
|
|
|
def input_name(name, field) when (is_atom(name) and is_atom(field)) or is_binary(field),
|
|
do: "#{name}[#{field}]"
|
|
|
|
@doc """
|
|
Receives two forms structs and checks if the given field changed.
|
|
|
|
The field will have changed if either its associated value, errors,
|
|
action, or implementation changed. This is mostly used for optimization
|
|
engines as an extension of the `Access` behaviour.
|
|
"""
|
|
@spec input_changed?(t, t, field()) :: boolean()
|
|
def input_changed?(
|
|
%Form{
|
|
impl: impl1,
|
|
id: id1,
|
|
name: name1,
|
|
errors: errors1,
|
|
source: source1,
|
|
action: action1
|
|
} = form1,
|
|
%Form{
|
|
impl: impl2,
|
|
id: id2,
|
|
name: name2,
|
|
errors: errors2,
|
|
source: source2,
|
|
action: action2
|
|
} = form2,
|
|
field
|
|
)
|
|
when is_atom(field) or is_binary(field) do
|
|
impl1 != impl2 or id1 != id2 or name1 != name2 or action1 != action2 or
|
|
field_errors(errors1, field) != field_errors(errors2, field) or
|
|
impl1.input_value(source1, form1, field) != impl2.input_value(source2, form2, field)
|
|
end
|
|
|
|
@doc """
|
|
Returns the HTML validations that would apply to
|
|
the given field.
|
|
"""
|
|
@spec input_validations(t, field) :: Keyword.t()
|
|
def input_validations(%{source: source, impl: impl} = form, field)
|
|
when is_atom(field) or is_binary(field) do
|
|
impl.input_validations(source, form, field)
|
|
end
|
|
|
|
@doc """
|
|
Normalizes an input `value` according to its input `type`.
|
|
|
|
Certain HTML input values must be cast, or they will have idiosyncracies
|
|
when they are rendered. The goal of this function is to encapsulate
|
|
this logic. In particular:
|
|
|
|
* For "datetime-local" types, it converts `DateTime` and
|
|
`NaiveDateTime` to strings without the second precision
|
|
|
|
* For "checkbox" types, it returns a boolean depending on
|
|
whether the input is "true" or not
|
|
|
|
* For "textarea", it prefixes a newline to ensure newlines
|
|
won't be ignored on submission. This requires however
|
|
that the textarea is rendered with no spaces after its
|
|
content
|
|
"""
|
|
def normalize_value("datetime-local", %struct{} = value)
|
|
when struct in [NaiveDateTime, DateTime] do
|
|
<<date::10-binary, ?\s, hour_minute::5-binary, _rest::binary>> = struct.to_string(value)
|
|
{:safe, [date, ?T, hour_minute]}
|
|
end
|
|
|
|
def normalize_value("textarea", value) do
|
|
{:safe, value} = html_escape(value || "")
|
|
{:safe, [?\n | value]}
|
|
end
|
|
|
|
def normalize_value("checkbox", value) do
|
|
html_escape(value) == {:safe, "true"}
|
|
end
|
|
|
|
def normalize_value(_type, value) do
|
|
value
|
|
end
|
|
|
|
@doc """
|
|
Returns options to be used inside a select.
|
|
|
|
This is useful when building the select by hand.
|
|
It expects all options and one or more select values.
|
|
|
|
## Examples
|
|
|
|
options_for_select(["Admin": "admin", "User": "user"], "admin")
|
|
#=> <option value="admin" selected>Admin</option>
|
|
#=> <option value="user">User</option>
|
|
|
|
Multiple selected values:
|
|
|
|
options_for_select(["Admin": "admin", "User": "user", "Moderator": "moderator"],
|
|
["admin", "moderator"])
|
|
#=> <option value="admin" selected>Admin</option>
|
|
#=> <option value="user">User</option>
|
|
#=> <option value="moderator" selected>Moderator</option>
|
|
|
|
Groups are also supported:
|
|
|
|
options_for_select(["Europe": ["UK", "Sweden", "France"], ...], nil)
|
|
#=> <optgroup label="Europe">
|
|
#=> <option>UK</option>
|
|
#=> <option>Sweden</option>
|
|
#=> <option>France</option>
|
|
#=> </optgroup>
|
|
|
|
"""
|
|
def options_for_select(options, selected_values) do
|
|
{:safe,
|
|
escaped_options_for_select(
|
|
options,
|
|
selected_values |> List.wrap() |> Enum.map(&html_escape/1)
|
|
)}
|
|
end
|
|
|
|
defp escaped_options_for_select(options, selected_values) do
|
|
Enum.reduce(options, [], fn
|
|
{option_key, option_value}, acc ->
|
|
[acc | option(option_key, option_value, [], selected_values)]
|
|
|
|
options, acc when is_list(options) ->
|
|
{option_key, options} = Keyword.pop(options, :key)
|
|
|
|
option_key ||
|
|
raise ArgumentError,
|
|
"expected :key key when building <option> from keyword list: #{inspect(options)}"
|
|
|
|
{option_value, options} = Keyword.pop(options, :value)
|
|
|
|
option_value ||
|
|
raise ArgumentError,
|
|
"expected :value key when building <option> from keyword list: #{inspect(options)}"
|
|
|
|
[acc | option(option_key, option_value, options, selected_values)]
|
|
|
|
option, acc ->
|
|
[acc | option(option, option, [], selected_values)]
|
|
end)
|
|
end
|
|
|
|
defp option(group_label, group_values, [], value)
|
|
when is_list(group_values) or is_map(group_values) do
|
|
section_options = escaped_options_for_select(group_values, value)
|
|
option_tag("optgroup", [label: group_label], {:safe, section_options})
|
|
end
|
|
|
|
defp option(option_key, option_value, extra, value) do
|
|
option_key = html_escape(option_key)
|
|
option_value = html_escape(option_value)
|
|
attrs = extra ++ [selected: option_value in value, value: option_value]
|
|
option_tag("option", attrs, option_key)
|
|
end
|
|
|
|
defp option_tag(name, attrs, {:safe, body}) when is_binary(name) and is_list(attrs) do
|
|
{:safe, attrs} = Phoenix.HTML.attributes_escape(attrs)
|
|
[?<, name, attrs, ?>, body, ?<, ?/, name, ?>]
|
|
end
|
|
|
|
## TODO: Remove on v4.0
|
|
|
|
defimpl Phoenix.HTML.Safe do
|
|
def to_iodata(%{action: action, options: options}) do
|
|
IO.warn(
|
|
"rendering a Phoenix.HTML.Form as part of HTML is deprecated, " <>
|
|
"please extract the component you want to render instead. " <>
|
|
"If you want to build a form, use form_for/3 or <.form> in LiveView"
|
|
)
|
|
|
|
{:safe, contents} = form_tag(action, options)
|
|
contents
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
@spec form_for(Phoenix.HTML.FormData.t(), String.t(), Keyword.t()) :: Phoenix.HTML.Form.t()
|
|
def form_for(form_data, action, options) when is_list(options) do
|
|
IO.warn(
|
|
"form_for/3 without an anonymous function is deprecated. " <>
|
|
"If you are using HEEx templates, use the new Phoenix.Component.form/1 component"
|
|
)
|
|
|
|
%{Phoenix.HTML.FormData.to_form(form_data, options) | action: action}
|
|
end
|
|
|
|
## TODO: Move on v4.0
|
|
|
|
@doc """
|
|
Converts an attribute/form field into its humanize version.
|
|
|
|
iex> humanize(:username)
|
|
"Username"
|
|
iex> humanize(:created_at)
|
|
"Created at"
|
|
iex> humanize("user_id")
|
|
"User"
|
|
|
|
"""
|
|
def humanize(atom) when is_atom(atom), do: humanize(Atom.to_string(atom))
|
|
|
|
def humanize(bin) when is_binary(bin) do
|
|
bin =
|
|
if String.ends_with?(bin, "_id") do
|
|
binary_part(bin, 0, byte_size(bin) - 3)
|
|
else
|
|
bin
|
|
end
|
|
|
|
bin |> String.replace("_", " ") |> :string.titlecase()
|
|
end
|
|
|
|
@doc false
|
|
def form_for(form_data, action) do
|
|
form_for(form_data, action, [])
|
|
end
|
|
|
|
@doc """
|
|
Generates a form tag with a form builder and an anonymous function.
|
|
|
|
<%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %>
|
|
Name: <%= text_input f, :name %>
|
|
<% end %>
|
|
|
|
Forms may be used in two distinct scenarios:
|
|
|
|
* with changeset data - when information to populate
|
|
the form comes from a changeset. The changeset holds
|
|
rich information, which helps provide conveniences
|
|
|
|
* with map data - a simple map of parameters (such as
|
|
`Plug.Conn.params` can be given as data to the form)
|
|
|
|
We will explore all them below.
|
|
|
|
Note that if you are using HEEx templates, `form_for/4` is no longer
|
|
the preferred way to generate a form tag, and you should use
|
|
[`Phoenix.Component.form/1`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#form/1)
|
|
instead.
|
|
|
|
## With changeset data
|
|
|
|
The entry point for defining forms in Phoenix is with
|
|
the `form_for/4` function. For this example, we will
|
|
use `Ecto.Changeset`, which integrates nicely with Phoenix
|
|
forms via the `phoenix_ecto` package.
|
|
|
|
Imagine you have the following action in your controller:
|
|
|
|
def new(conn, _params) do
|
|
changeset = User.changeset(%User{})
|
|
render conn, "new.html", changeset: changeset
|
|
end
|
|
|
|
where `User.changeset/2` is defined as follows:
|
|
|
|
def changeset(user, params \\ %{}) do
|
|
Ecto.Changeset.cast(user, params, [:name, :age])
|
|
end
|
|
|
|
Now a `@changeset` assign is available in views which we
|
|
can pass to the form:
|
|
|
|
<%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %>
|
|
<label>
|
|
Name: <%= text_input f, :name %>
|
|
</label>
|
|
|
|
<label>
|
|
Age: <%= select f, :age, 18..100 %>
|
|
</label>
|
|
|
|
<%= submit "Submit" %>
|
|
<% end %>
|
|
|
|
`form_for/4` receives the `Ecto.Changeset` and converts it
|
|
to a form, which is passed to the function as the argument
|
|
`f`. All the remaining functions in this module receive
|
|
the form and automatically generate the input fields, often
|
|
by extracting information from the given changeset. For example,
|
|
if the user had a default value for age set, it will
|
|
automatically show up as selected in the form.
|
|
|
|
### A note on `:errors`
|
|
|
|
Even if `changeset.errors` is non-empty, errors will not be displayed in a
|
|
form if [the changeset
|
|
`:action`](https://hexdocs.pm/ecto/Ecto.Changeset.html#module-changeset-actions)
|
|
is `nil` or `:ignore`.
|
|
|
|
This is useful for things like validation hints on form fields, e.g. an empty
|
|
changeset for a new form. That changeset isn't valid, but we don't want to
|
|
show errors until an actual user action has been performed.
|
|
|
|
For example, if the user submits and a `Repo.insert/1` is called and fails on
|
|
changeset validation, the action will be set to `:insert` to show that an
|
|
insert was attempted, and the presence of that action will cause errors to be
|
|
displayed. The same is true for Repo.update/delete.
|
|
|
|
If you want to show errors manually you can also set the action yourself,
|
|
either directly on the `Ecto.Changeset` struct field or by using
|
|
`Ecto.Changeset.apply_action/2`. Since the action can be arbitrary, you can
|
|
set it to `:validate` or anything else to avoid giving the impression that a
|
|
database operation has actually been attempted.
|
|
|
|
## With map data
|
|
|
|
`form_for/4` expects as first argument any data structure that
|
|
implements the `Phoenix.HTML.FormData` protocol. By default,
|
|
Phoenix.HTML implements this protocol for `Map`.
|
|
|
|
This is useful when you are creating forms that are not backed
|
|
by any kind of data layer. Let's assume that we're submitting a
|
|
form to the `:new` action in the `FooController`:
|
|
|
|
<%= form_for @conn.params, Routes.foo_path(@conn, :new), fn f -> %>
|
|
<%= text_input f, :contents %>
|
|
<%= submit "Search" %>
|
|
<% end %>
|
|
|
|
Once the form is submitted, the form contents will be set directly
|
|
as the parameters root, such as `conn.params["contents"]`. If you
|
|
prefer, you can pass the `:as` option to configure them to be nested:
|
|
|
|
<%= form_for @conn.params["search"] || %{}, Routes.foo_path(@conn, :new), [as: :search], fn f -> %>
|
|
<%= text_input f, :contents %>
|
|
<%= submit "Search" %>
|
|
<% end %>
|
|
|
|
In the example above, all form contents are now set inside `conn.params["search"]`
|
|
thanks to the `[as: :search]` option.
|
|
|
|
## Nested inputs
|
|
|
|
If your data layer supports embedding or nested associations,
|
|
you can use `inputs_for` to attach nested data to the form.
|
|
|
|
Imagine the following Ecto schemas:
|
|
|
|
defmodule User do
|
|
use Ecto.Schema
|
|
|
|
schema "users" do
|
|
field :name
|
|
embeds_one :permalink, Permalink
|
|
end
|
|
|
|
def changeset(user \\ %User{}, params) do
|
|
user
|
|
|> Ecto.Changeset.cast(params, [:name])
|
|
|> Ecto.Changeset.cast_embed(:permalink)
|
|
end
|
|
end
|
|
|
|
defmodule Permalink do
|
|
use Ecto.Schema
|
|
|
|
embedded_schema do
|
|
field :url
|
|
end
|
|
end
|
|
|
|
In the form, you can now do this:
|
|
|
|
<%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %>
|
|
<%= text_input f, :name %>
|
|
|
|
<%= inputs_for f, :permalink, fn fp -> %>
|
|
<%= text_input fp, :url %>
|
|
<% end %>
|
|
<% end %>
|
|
|
|
The default option can be given to populate the fields if none
|
|
is given:
|
|
|
|
<%= inputs_for f, :permalink, [default: %Permalink{title: "default"}], fn fp -> %>
|
|
<%= text_input fp, :url %>
|
|
<% end %>
|
|
|
|
`inputs_for/4` can be used to work with single entities or
|
|
collections. When working with collections, `:prepend` and
|
|
`:append` can be used to add entries to the collection
|
|
stored in the changeset.
|
|
|
|
## CSRF protection
|
|
|
|
CSRF protection is a mechanism to ensure that the user who rendered
|
|
the form is the one actually submitting it. This module generates a
|
|
CSRF token by default. Your application should check this token on
|
|
the server to prevent attackers from making requests on your server on
|
|
behalf of other users. Phoenix checks this token by default.
|
|
|
|
When posting a form with a host in its address, such as "//host.com/path"
|
|
instead of only "/path", Phoenix will include the host signature in the
|
|
token, and will only validate the token if the accessed host is the same as
|
|
the host in the token. This is to avoid tokens from leaking to third-party
|
|
applications. If this behaviour is problematic, you can generate a
|
|
non-host-specific token with `Plug.CSRFProtection.get_csrf_token/0` and
|
|
pass it to the form generator via the `:csrf_token` option.
|
|
|
|
## Options
|
|
|
|
* `:as` - the server side parameter in which all params for this
|
|
form will be collected (i.e. `as: :user_params` would mean all fields
|
|
for this form will be accessed as `conn.params.user_params` server
|
|
side). Automatically inflected when a changeset is given.
|
|
|
|
* `:method` - the HTTP method. If the method is not "get" nor "post",
|
|
an input tag with name `_method` is generated along-side the form tag.
|
|
Defaults to "post".
|
|
|
|
* `:multipart` - when true, sets enctype to "multipart/form-data".
|
|
Required when uploading files.
|
|
|
|
* `:csrf_token` - for "post" requests, the form tag will automatically
|
|
include an input tag with name `_csrf_token`. When set to false, this
|
|
is disabled.
|
|
|
|
* `:errors` - use this to manually pass a keyword list of errors to the form
|
|
(for example from `conn.assigns[:errors]`). This option is only used when a
|
|
connection is used as the form source and it will make the errors available
|
|
under `f.errors`.
|
|
|
|
* `:id` - the ID of the form attribute. If an ID is given, all form inputs
|
|
will also be prefixed by the given ID.
|
|
|
|
All other options will be passed as HTML attributes, such as `class: "foo"`.
|
|
"""
|
|
@spec form_for(Phoenix.HTML.FormData.t(), String.t(), (t -> Phoenix.HTML.unsafe())) ::
|
|
Phoenix.HTML.safe()
|
|
@spec form_for(Phoenix.HTML.FormData.t(), String.t(), Keyword.t(), (t -> Phoenix.HTML.unsafe())) ::
|
|
Phoenix.HTML.safe()
|
|
def form_for(form_data, action, options \\ [], fun) when is_function(fun, 1) do
|
|
form = %{Phoenix.HTML.FormData.to_form(form_data, options) | action: action}
|
|
html_escape([form_tag(action, form.options), fun.(form), raw("</form>")])
|
|
end
|
|
|
|
@doc false
|
|
def inputs_for(form, field) when is_atom(field) or is_binary(field),
|
|
do: inputs_for(form, field, [])
|
|
|
|
@doc false
|
|
def inputs_for(%{impl: impl} = form, field, options)
|
|
when (is_atom(field) or is_binary(field)) and is_list(options) do
|
|
IO.warn(
|
|
"inputs_for/3 without an anonymous function is deprecated. " <>
|
|
"If you are using HEEx templates, use the new Phoenix.Component.inputs_for/1 component"
|
|
)
|
|
|
|
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.
|
|
|
|
See `form_for/4` for examples of using this function.
|
|
|
|
## Options
|
|
|
|
* `:id` - the id to be used in the form, defaults to the
|
|
concatenation of the given `field` to the parent form id
|
|
|
|
* `:as` - the name to be used in the form, defaults to the
|
|
concatenation of the given `field` to the parent form name
|
|
|
|
* `:default` - the value to use if none is available
|
|
|
|
* `:prepend` - the values to prepend when rendering. This only
|
|
applies if the field value is a list and no parameters were
|
|
sent through the form.
|
|
|
|
* `:append` - the values to append when rendering. This only
|
|
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 access `form.hidden` to generate them manually
|
|
within the supplied callback.
|
|
|
|
"""
|
|
@spec inputs_for(t, field, (t -> Phoenix.HTML.unsafe())) :: Phoenix.HTML.safe()
|
|
@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])
|
|
|> Keyword.merge(options)
|
|
|
|
forms = impl.to_form(form.source, form, field, options)
|
|
|
|
html_escape(
|
|
Enum.map(forms, fn form ->
|
|
if skip do
|
|
fun.(form)
|
|
else
|
|
[hidden_inputs_for(form), fun.(form)]
|
|
end
|
|
end)
|
|
)
|
|
end
|
|
|
|
@mapping %{
|
|
"url" => :url_input,
|
|
"email" => :email_input,
|
|
"search" => :search_input,
|
|
"password" => :password_input
|
|
}
|
|
|
|
@doc """
|
|
Gets the input type for a given field.
|
|
|
|
If the underlying input type is a `:text_field`,
|
|
a mapping could be given to further inflect
|
|
the input type based solely on the field name.
|
|
The default mapping is:
|
|
|
|
%{"url" => :url_input,
|
|
"email" => :email_input,
|
|
"search" => :search_input,
|
|
"password" => :password_input}
|
|
|
|
"""
|
|
@spec input_type(t, field) :: atom
|
|
def input_type(%{impl: impl, source: source} = form, field, mapping \\ @mapping)
|
|
when is_atom(field) or is_binary(field) do
|
|
type = impl.input_type(source, form, field)
|
|
|
|
if type == :text_input do
|
|
field = field_to_string(field)
|
|
|
|
Enum.find_value(mapping, type, fn {k, v} ->
|
|
String.contains?(field, k) && v
|
|
end)
|
|
else
|
|
type
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Generates a text input.
|
|
|
|
The form should either be a `Phoenix.HTML.Form` emitted
|
|
by `form_for` or an atom.
|
|
|
|
All given options are forwarded to the underlying input,
|
|
default values are provided for id, name and value if
|
|
possible.
|
|
|
|
## Examples
|
|
|
|
# Assuming form contains a User schema
|
|
text_input(form, :name)
|
|
#=> <input id="user_name" name="user[name]" type="text" value="">
|
|
|
|
text_input(:user, :name)
|
|
#=> <input id="user_name" name="user[name]" type="text" value="">
|
|
|
|
"""
|
|
def text_input(form, field, opts \\ []) do
|
|
generic_input(:text, form, field, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a hidden input.
|
|
|
|
See `text_input/3` for example and docs.
|
|
"""
|
|
def hidden_input(form, field, opts \\ []) do
|
|
generic_input(:hidden, form, field, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates hidden inputs for the given form inputs.
|
|
|
|
See `inputs_for/2` and `inputs_for/3`.
|
|
"""
|
|
@spec hidden_inputs_for(t) :: list(Phoenix.HTML.safe())
|
|
def hidden_inputs_for(form) do
|
|
Enum.flat_map(form.hidden, fn {k, v} ->
|
|
hidden_inputs_for(form, k, v)
|
|
end)
|
|
end
|
|
|
|
defp hidden_inputs_for(form, k, values) when is_list(values) do
|
|
id = input_id(form, k)
|
|
name = input_name(form, k)
|
|
|
|
for {v, index} <- Enum.with_index(values) do
|
|
hidden_input(form, k,
|
|
id: id <> "_" <> Integer.to_string(index),
|
|
name: name <> "[]",
|
|
value: v
|
|
)
|
|
end
|
|
end
|
|
|
|
defp hidden_inputs_for(form, k, v) do
|
|
[hidden_input(form, k, value: v)]
|
|
end
|
|
|
|
@doc """
|
|
Generates an email input.
|
|
|
|
See `text_input/3` for example and docs.
|
|
"""
|
|
def email_input(form, field, opts \\ []) do
|
|
generic_input(:email, form, field, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a number input.
|
|
|
|
See `text_input/3` for example and docs.
|
|
"""
|
|
def number_input(form, field, opts \\ []) do
|
|
generic_input(:number, form, field, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a password input.
|
|
|
|
For security reasons, the form data and parameter values
|
|
are never re-used in `password_input/3`. Pass the value
|
|
explicitly if you would like to set one.
|
|
|
|
See `text_input/3` for example and docs.
|
|
"""
|
|
def password_input(form, field, opts \\ []) do
|
|
opts =
|
|
opts
|
|
|> Keyword.put_new(:type, "password")
|
|
|> Keyword.put_new(:id, input_id(form, field))
|
|
|> Keyword.put_new(:name, input_name(form, field))
|
|
|
|
tag(:input, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates an url input.
|
|
|
|
See `text_input/3` for example and docs.
|
|
"""
|
|
def url_input(form, field, opts \\ []) do
|
|
generic_input(:url, form, field, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a search input.
|
|
|
|
See `text_input/3` for example and docs.
|
|
"""
|
|
def search_input(form, field, opts \\ []) do
|
|
generic_input(:search, form, field, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a telephone input.
|
|
|
|
See `text_input/3` for example and docs.
|
|
"""
|
|
def telephone_input(form, field, opts \\ []) do
|
|
generic_input(:tel, form, field, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a color input.
|
|
|
|
See `text_input/3` for example and docs.
|
|
"""
|
|
def color_input(form, field, opts \\ []) do
|
|
generic_input(:color, form, field, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a range input.
|
|
|
|
See `text_input/3` for example and docs.
|
|
"""
|
|
def range_input(form, field, opts \\ []) do
|
|
generic_input(:range, form, field, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a date input.
|
|
|
|
See `text_input/3` for example and docs.
|
|
"""
|
|
def date_input(form, field, opts \\ []) do
|
|
generic_input(:date, form, field, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a datetime-local input.
|
|
|
|
See `text_input/3` for example and docs.
|
|
"""
|
|
def datetime_local_input(form, field, opts \\ []) do
|
|
value = Keyword.get(opts, :value, input_value(form, field))
|
|
opts = Keyword.put(opts, :value, normalize_value("datetime-local", value))
|
|
|
|
generic_input(:"datetime-local", form, field, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a time input.
|
|
|
|
## Options
|
|
|
|
* `:precision` - Allowed values: `:minute`, `:second`, `:millisecond`.
|
|
Defaults to `:minute`.
|
|
|
|
All other options are forwarded. See `text_input/3` for example and docs.
|
|
|
|
## Examples
|
|
|
|
time_input form, :time
|
|
#=> <input id="form_time" name="form[time]" type="time" value="23:00">
|
|
|
|
time_input form, :time, precision: :second
|
|
#=> <input id="form_time" name="form[time]" type="time" value="23:00:00">
|
|
|
|
time_input form, :time, precision: :millisecond
|
|
#=> <input id="form_time" name="form[time]" type="time" value="23:00:00.000">
|
|
"""
|
|
def time_input(form, field, opts \\ []) do
|
|
{precision, opts} = Keyword.pop(opts, :precision, :minute)
|
|
value = opts[:value] || input_value(form, field)
|
|
opts = Keyword.put(opts, :value, truncate_time(value, precision))
|
|
|
|
generic_input(:time, form, field, opts)
|
|
end
|
|
|
|
defp truncate_time(%Time{} = time, :minute) do
|
|
time
|
|
|> Time.to_string()
|
|
|> String.slice(0, 5)
|
|
end
|
|
|
|
defp truncate_time(%Time{} = time, precision) do
|
|
time
|
|
|> Time.truncate(precision)
|
|
|> Time.to_string()
|
|
end
|
|
|
|
defp truncate_time(value, _), do: value
|
|
|
|
defp generic_input(type, form, field, opts)
|
|
when is_list(opts) and (is_atom(field) or is_binary(field)) do
|
|
opts =
|
|
opts
|
|
|> Keyword.put_new(:type, type)
|
|
|> Keyword.put_new(:id, input_id(form, field))
|
|
|> Keyword.put_new(:name, input_name(form, field))
|
|
|> Keyword.put_new(:value, input_value(form, field))
|
|
|> Keyword.update!(:value, &maybe_html_escape/1)
|
|
|
|
tag(:input, opts)
|
|
end
|
|
|
|
defp maybe_html_escape(nil), do: nil
|
|
defp maybe_html_escape(value), do: html_escape(value)
|
|
|
|
@doc """
|
|
Generates a textarea input.
|
|
|
|
All given options are forwarded to the underlying input,
|
|
default values are provided for id, name and textarea
|
|
content if possible.
|
|
|
|
## Examples
|
|
|
|
# Assuming form contains a User schema
|
|
textarea(form, :description)
|
|
#=> <textarea id="user_description" name="user[description]"></textarea>
|
|
|
|
## New lines
|
|
|
|
Notice the generated textarea includes a new line after
|
|
the opening tag. This is because the HTML spec says new
|
|
lines after tags must be ignored, and all major browser
|
|
implementations do that.
|
|
|
|
Therefore, in order to avoid new lines provided by the user
|
|
from being ignored when the form is resubmitted, we
|
|
automatically add a new line before the text area
|
|
value.
|
|
"""
|
|
def textarea(form, field, opts \\ []) do
|
|
opts =
|
|
opts
|
|
|> Keyword.put_new(:id, input_id(form, field))
|
|
|> Keyword.put_new(:name, input_name(form, field))
|
|
|
|
{value, opts} = Keyword.pop(opts, :value, input_value(form, field))
|
|
content_tag(:textarea, normalize_value("textarea", value), opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a file input.
|
|
|
|
It requires the given form to be configured with `multipart: true`
|
|
when invoking `form_for/4`, otherwise it fails with `ArgumentError`.
|
|
|
|
See `text_input/3` for example and docs.
|
|
"""
|
|
def file_input(form, field, opts \\ []) do
|
|
if match?(%Form{}, form) and !form.options[:multipart] do
|
|
raise ArgumentError,
|
|
"file_input/3 requires the enclosing form_for/4 " <>
|
|
"to be configured with multipart: true"
|
|
end
|
|
|
|
opts =
|
|
opts
|
|
|> Keyword.put_new(:type, :file)
|
|
|> Keyword.put_new(:id, input_id(form, field))
|
|
|> Keyword.put_new(:name, input_name(form, field))
|
|
|
|
opts =
|
|
if opts[:multiple] do
|
|
Keyword.update!(opts, :name, &"#{&1}[]")
|
|
else
|
|
opts
|
|
end
|
|
|
|
tag(:input, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a submit button to send the form.
|
|
|
|
## Examples
|
|
|
|
submit do: "Submit"
|
|
#=> <button type="submit">Submit</button>
|
|
|
|
"""
|
|
def submit([do: _] = block_option), do: submit([], block_option)
|
|
|
|
@doc """
|
|
Generates a submit button to send the form.
|
|
|
|
All options are forwarded to the underlying button tag.
|
|
When called with a `do:` block, the button tag options
|
|
come first.
|
|
|
|
## Examples
|
|
|
|
submit "Submit"
|
|
#=> <button type="submit">Submit</button>
|
|
|
|
submit "Submit", class: "btn"
|
|
#=> <button class="btn" type="submit">Submit</button>
|
|
|
|
submit [class: "btn"], do: "Submit"
|
|
#=> <button class="btn" type="submit">Submit</button>
|
|
|
|
"""
|
|
def submit(value, opts \\ [])
|
|
|
|
def submit(opts, [do: _] = block_option) do
|
|
opts = Keyword.put_new(opts, :type, "submit")
|
|
|
|
content_tag(:button, opts, block_option)
|
|
end
|
|
|
|
def submit(value, opts) do
|
|
opts = Keyword.put_new(opts, :type, "submit")
|
|
|
|
content_tag(:button, value, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a reset input to reset all the form fields to
|
|
their original state.
|
|
|
|
All options are forwarded to the underlying input tag.
|
|
|
|
## Examples
|
|
|
|
reset "Reset"
|
|
#=> <input type="reset" value="Reset">
|
|
|
|
reset "Reset", class: "btn"
|
|
#=> <input type="reset" value="Reset" class="btn">
|
|
|
|
"""
|
|
def reset(value, opts \\ []) do
|
|
opts =
|
|
opts
|
|
|> Keyword.put_new(:type, "reset")
|
|
|> Keyword.put_new(:value, value)
|
|
|
|
tag(:input, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a radio button.
|
|
|
|
Invoke this function for each possible value you want
|
|
to be sent to the server.
|
|
|
|
## Examples
|
|
|
|
# Assuming form contains a User schema
|
|
radio_button(form, :role, "admin")
|
|
#=> <input id="user_role_admin" name="user[role]" type="radio" value="admin">
|
|
|
|
## Options
|
|
|
|
All options are simply forwarded to the underlying HTML tag.
|
|
"""
|
|
def radio_button(form, field, value, opts \\ []) do
|
|
escaped_value = html_escape(value)
|
|
|
|
opts =
|
|
opts
|
|
|> Keyword.put_new(:type, "radio")
|
|
|> Keyword.put_new(:id, input_id(form, field, escaped_value))
|
|
|> Keyword.put_new(:name, input_name(form, field))
|
|
|
|
opts =
|
|
if escaped_value == html_escape(input_value(form, field)) do
|
|
Keyword.put_new(opts, :checked, true)
|
|
else
|
|
opts
|
|
end
|
|
|
|
tag(:input, [value: escaped_value] ++ opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a checkbox.
|
|
|
|
This function is useful for sending boolean values to the server.
|
|
|
|
## Examples
|
|
|
|
# Assuming form contains a User schema
|
|
checkbox(form, :famous)
|
|
#=> <input name="user[famous]" type="hidden" value="false">
|
|
#=> <input checked="checked" id="user_famous" name="user[famous]" type="checkbox" value="true">
|
|
|
|
## Options
|
|
|
|
* `:checked_value` - the value to be sent when the checkbox is checked.
|
|
Defaults to "true"
|
|
|
|
* `:hidden_input` - controls if this function will generate a hidden input
|
|
to submit the unchecked value or not. Defaults to "true"
|
|
|
|
* `:unchecked_value` - the value to be sent when the checkbox is unchecked,
|
|
Defaults to "false"
|
|
|
|
* `:value` - the value used to check if a checkbox is checked or unchecked.
|
|
The default value is extracted from the form data if available
|
|
|
|
All other options are forwarded to the underlying HTML tag.
|
|
|
|
## Hidden fields
|
|
|
|
Because an unchecked checkbox is not sent to the server, Phoenix
|
|
automatically generates a hidden field with the unchecked_value
|
|
*before* the checkbox field to ensure the `unchecked_value` is sent
|
|
when the checkbox is not marked. Set `hidden_input` to false If you
|
|
don't want to send values from unchecked checkbox to the server.
|
|
"""
|
|
def checkbox(form, field, opts \\ []) do
|
|
opts =
|
|
opts
|
|
|> Keyword.put_new(:type, "checkbox")
|
|
|> Keyword.put_new(:name, input_name(form, field))
|
|
|
|
{value, opts} = Keyword.pop(opts, :value, input_value(form, field))
|
|
{checked_value, opts} = Keyword.pop(opts, :checked_value, true)
|
|
{unchecked_value, opts} = Keyword.pop(opts, :unchecked_value, false)
|
|
{hidden_input, opts} = Keyword.pop(opts, :hidden_input, true)
|
|
|
|
# We html escape all values to be sure we are comparing
|
|
# apples to apples. After all, we may have true in the data
|
|
# but "true" in the params and both need to match.
|
|
checked_value = html_escape(checked_value)
|
|
unchecked_value = html_escape(unchecked_value)
|
|
|
|
opts =
|
|
opts
|
|
|> Keyword.put_new_lazy(:checked, fn ->
|
|
value = html_escape(value)
|
|
value == checked_value
|
|
end)
|
|
|> Keyword.put_new_lazy(:id, fn ->
|
|
if String.ends_with?(opts[:name], "[]"),
|
|
do: input_id(form, field, checked_value),
|
|
else: input_id(form, field)
|
|
end)
|
|
|
|
if hidden_input do
|
|
hidden_opts = [type: "hidden", value: unchecked_value]
|
|
|
|
html_escape([
|
|
tag(:input, hidden_opts ++ Keyword.take(opts, [:name, :disabled, :form])),
|
|
tag(:input, [value: checked_value] ++ opts)
|
|
])
|
|
else
|
|
html_escape([
|
|
tag(:input, [value: checked_value] ++ opts)
|
|
])
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Generates a select tag with the given `options`.
|
|
|
|
`options` are expected to be an enumerable which will be used to
|
|
generate each respective `option`. The enumerable may have:
|
|
|
|
* keyword lists - each keyword list is expected to have the keys
|
|
`:key` and `:value`. Additional keys such as `:disabled` may
|
|
be given to customize the option.
|
|
|
|
* two-item tuples - where the first element is an atom, string or
|
|
integer to be used as the option label and the second element is
|
|
an atom, string or integer to be used as the option value
|
|
|
|
* atom, string or integer - which will be used as both label and value
|
|
for the generated select
|
|
|
|
## Optgroups
|
|
|
|
If `options` is map or keyword list where the first element is a string,
|
|
atom or integer and the second element is a list or a map, it is assumed
|
|
the key will be wrapped in an `<optgroup>` and the value will be used to
|
|
generate `<options>` nested under the group.
|
|
|
|
## Examples
|
|
|
|
# Assuming form contains a User schema
|
|
select(form, :age, 0..120)
|
|
#=> <select id="user_age" name="user[age]">
|
|
#=> <option value="0">0</option>
|
|
#=> ...
|
|
#=> <option value="120">120</option>
|
|
#=> </select>
|
|
|
|
select(form, :role, ["Admin": "admin", "User": "user"])
|
|
#=> <select id="user_role" name="user[role]">
|
|
#=> <option value="admin">Admin</option>
|
|
#=> <option value="user">User</option>
|
|
#=> </select>
|
|
|
|
select(form, :role, [[key: "Admin", value: "admin", disabled: true],
|
|
[key: "User", value: "user"]])
|
|
#=> <select id="user_role" name="user[role]">
|
|
#=> <option value="admin" disabled="disabled">Admin</option>
|
|
#=> <option value="user">User</option>
|
|
#=> </select>
|
|
|
|
You can also pass a prompt:
|
|
|
|
select(form, :role, ["Admin": "admin", "User": "user"], prompt: "Choose your role")
|
|
#=> <select id="user_role" name="user[role]">
|
|
#=> <option value="">Choose your role</option>
|
|
#=> <option value="admin">Admin</option>
|
|
#=> <option value="user">User</option>
|
|
#=> </select>
|
|
|
|
And customize the prompt like any other entry:
|
|
|
|
select(form, :role, ["Admin": "admin", "User": "user"], prompt: [key: "Choose your role", disabled: true])
|
|
#=> <select id="user_role" name="user[role]">
|
|
#=> <option value="" disabled="">Choose your role</option>
|
|
#=> <option value="admin">Admin</option>
|
|
#=> <option value="user">User</option>
|
|
#=> </select>
|
|
|
|
If you want to select an option that comes from the database,
|
|
such as a manager for a given project, you may write:
|
|
|
|
select(form, :manager_id, Enum.map(@managers, &{&1.name, &1.id}))
|
|
#=> <select id="manager_id" name="project[manager_id]">
|
|
#=> <option value="1">Mary Jane</option>
|
|
#=> <option value="2">John Doe</option>
|
|
#=> </select>
|
|
|
|
Finally, if the values are a list or a map, we use the keys for
|
|
grouping:
|
|
|
|
select(form, :country, ["Europe": ["UK", "Sweden", "France"]], ...)
|
|
#=> <select id="user_country" name="user[country]">
|
|
#=> <optgroup label="Europe">
|
|
#=> <option>UK</option>
|
|
#=> <option>Sweden</option>
|
|
#=> <option>France</option>
|
|
#=> </optgroup>
|
|
#=> ...
|
|
#=> </select>
|
|
|
|
## Options
|
|
|
|
* `:prompt` - an option to include at the top of the options. It may be
|
|
a string or a keyword list of attributes and the `:key`
|
|
|
|
* `:selected` - the default value to use when none was sent as parameter
|
|
|
|
Be aware that a `:multiple` option will not generate a correctly
|
|
functioning multiple select element. Use `multiple_select/4` instead.
|
|
|
|
All other options are forwarded to the underlying HTML tag.
|
|
"""
|
|
def select(form, field, options, opts \\ []) when is_atom(field) or is_binary(field) do
|
|
{selected, opts} = selected(form, field, opts)
|
|
options_html = options_for_select(options, selected)
|
|
|
|
{options_html, opts} =
|
|
case Keyword.pop(opts, :prompt) do
|
|
{nil, opts} -> {options_html, opts}
|
|
{prompt, opts} -> {[prompt_option(prompt) | options_html], opts}
|
|
end
|
|
|
|
opts =
|
|
opts
|
|
|> Keyword.put_new(:id, input_id(form, field))
|
|
|> Keyword.put_new(:name, input_name(form, field))
|
|
|
|
content_tag(:select, options_html, opts)
|
|
end
|
|
|
|
defp prompt_option(prompt) when is_list(prompt) do
|
|
{prompt_key, prompt_opts} = Keyword.pop(prompt, :key)
|
|
|
|
prompt_key ||
|
|
raise ArgumentError,
|
|
"expected :key key when building a prompt select option with a keyword list: " <>
|
|
inspect(prompt)
|
|
|
|
prompt_option(prompt_key, prompt_opts)
|
|
end
|
|
|
|
defp prompt_option(key) when is_binary(key), do: prompt_option(key, [])
|
|
|
|
defp prompt_option(key, opts) when is_list(opts) do
|
|
content_tag(:option, key, Keyword.put_new(opts, :value, ""))
|
|
end
|
|
|
|
@doc """
|
|
Generates a select tag with the given `options`.
|
|
|
|
Values are expected to be an Enumerable containing two-item tuples
|
|
(like maps and keyword lists) or any Enumerable where the element
|
|
will be used both as key and value for the generated select.
|
|
|
|
## Examples
|
|
|
|
# Assuming form contains a User schema
|
|
multiple_select(form, :roles, ["Admin": 1, "Power User": 2])
|
|
#=> <select id="user_roles" name="user[roles][]">
|
|
#=> <option value="1">Admin</option>
|
|
#=> <option value="2">Power User</option>
|
|
#=> </select>
|
|
|
|
multiple_select(form, :roles, ["Admin": 1, "Power User": 2], selected: [1])
|
|
#=> <select id="user_roles" name="user[roles][]">
|
|
#=> <option value="1" selected="selected">Admin</option>
|
|
#=> <option value="2">Power User</option>
|
|
#=> </select>
|
|
|
|
When working with structs, associations, and embeds, you will need to tell
|
|
Phoenix how to extract the value out of the collection. For example,
|
|
imagine `user.roles` is a list of `%Role{}` structs. You must call it as:
|
|
|
|
multiple_select(form, :roles, ["Admin": 1, "Power User": 2],
|
|
selected: Enum.map(@user.roles, &(&1.id))
|
|
|
|
The `:selected` option will mark the given IDs as selected unless the form
|
|
is being resubmitted. When resubmitted, it uses the form params as values.
|
|
|
|
When used with Ecto, you will typically do a query to retrieve the IDs from
|
|
the database:
|
|
|
|
from r in Role, where: r.id in ^(params["roles"] || [])
|
|
|
|
And then use `Ecto.Changeset.put_assoc/2` to insert the new roles into the user.
|
|
|
|
## Options
|
|
|
|
* `:selected` - the default options to be marked as selected. The values
|
|
on this list are ignored in case ids have been set as parameters.
|
|
|
|
All other options are forwarded to the underlying HTML tag.
|
|
"""
|
|
def multiple_select(form, field, options, opts \\ []) do
|
|
{selected, opts} = selected(form, field, opts)
|
|
|
|
opts =
|
|
opts
|
|
|> Keyword.put_new(:id, input_id(form, field))
|
|
|> Keyword.put_new(:name, input_name(form, field) <> "[]")
|
|
|> Keyword.put_new(:multiple, "")
|
|
|
|
content_tag(:select, options_for_select(options, selected), opts)
|
|
end
|
|
|
|
defp selected(form, field, opts) do
|
|
{value, opts} = Keyword.pop(opts, :value)
|
|
{selected, opts} = Keyword.pop(opts, :selected)
|
|
|
|
if value != nil do
|
|
{value, opts}
|
|
else
|
|
param = field_to_string(field)
|
|
|
|
case form do
|
|
%{params: %{^param => sent}} ->
|
|
{sent, opts}
|
|
|
|
_ ->
|
|
{selected || input_value(form, field), opts}
|
|
end
|
|
end
|
|
end
|
|
|
|
## Datetime
|
|
|
|
@doc ~S'''
|
|
Generates select tags for datetime.
|
|
|
|
Warning: This functionality is best provided by browsers nowadays.
|
|
Consider using `datetime_local_input/3` instead.
|
|
|
|
## Examples
|
|
|
|
# Assuming form contains a User schema
|
|
datetime_select form, :born_at
|
|
#=> <select id="user_born_at_year" name="user[born_at][year]">...</select> /
|
|
#=> <select id="user_born_at_month" name="user[born_at][month]">...</select> /
|
|
#=> <select id="user_born_at_day" name="user[born_at][day]">...</select> —
|
|
#=> <select id="user_born_at_hour" name="user[born_at][hour]">...</select> :
|
|
#=> <select id="user_born_at_min" name="user[born_at][minute]">...</select>
|
|
|
|
If you want to include the seconds field (hidden by default), pass `second: []`:
|
|
|
|
# Assuming form contains a User schema
|
|
datetime_select form, :born_at, second: []
|
|
|
|
If you want to configure the years range:
|
|
|
|
# Assuming form contains a User schema
|
|
datetime_select form, :born_at, year: [options: 1900..2100]
|
|
|
|
You are also able to configure `:month`, `:day`, `:hour`, `:minute` and
|
|
`:second`. All options given to those keys will be forwarded to the
|
|
underlying select. See `select/4` for more information.
|
|
|
|
For example, if you are using Phoenix with Gettext and you want to localize
|
|
the list of months, you can pass `:options` to the `:month` key:
|
|
|
|
# Assuming form contains a User schema
|
|
datetime_select form, :born_at, month: [
|
|
options: [
|
|
{gettext("January"), "1"},
|
|
{gettext("February"), "2"},
|
|
{gettext("March"), "3"},
|
|
{gettext("April"), "4"},
|
|
{gettext("May"), "5"},
|
|
{gettext("June"), "6"},
|
|
{gettext("July"), "7"},
|
|
{gettext("August"), "8"},
|
|
{gettext("September"), "9"},
|
|
{gettext("October"), "10"},
|
|
{gettext("November"), "11"},
|
|
{gettext("December"), "12"},
|
|
]
|
|
]
|
|
|
|
You may even provide your own `localized_datetime_select/3` built on top of
|
|
`datetime_select/3`:
|
|
|
|
defp localized_datetime_select(form, field, opts \\ []) do
|
|
opts =
|
|
Keyword.put(opts, :month, options: [
|
|
{gettext("January"), "1"},
|
|
{gettext("February"), "2"},
|
|
{gettext("March"), "3"},
|
|
{gettext("April"), "4"},
|
|
{gettext("May"), "5"},
|
|
{gettext("June"), "6"},
|
|
{gettext("July"), "7"},
|
|
{gettext("August"), "8"},
|
|
{gettext("September"), "9"},
|
|
{gettext("October"), "10"},
|
|
{gettext("November"), "11"},
|
|
{gettext("December"), "12"},
|
|
])
|
|
|
|
datetime_select(form, field, opts)
|
|
end
|
|
|
|
## Options
|
|
|
|
* `:value` - the value used to select a given option.
|
|
The default value is extracted from the form data if available.
|
|
|
|
* `:default` - the default value to use when none was given in
|
|
`:value` and none is available in the form data
|
|
|
|
* `:year`, `:month`, `:day`, `:hour`, `:minute`, `:second` - options passed
|
|
to the underlying select. See `select/4` for more information.
|
|
The available values can be given in `:options`.
|
|
|
|
* `:builder` - specify how the select can be build. It must be a function
|
|
that receives a builder that should be invoked with the select name
|
|
and a set of options. See builder below for more information.
|
|
|
|
## Builder
|
|
|
|
The generated datetime_select can be customized at will by providing a
|
|
builder option. Here is an example from EEx:
|
|
|
|
<%= datetime_select form, :born_at, builder: fn b -> %>
|
|
Date: <%= b.(:day, []) %> / <%= b.(:month, []) %> / <%= b.(:year, []) %>
|
|
Time: <%= b.(:hour, []) %> : <%= b.(:minute, []) %>
|
|
<% end %>
|
|
|
|
Although we have passed empty lists as options (they are required), you
|
|
could pass any option there and it would be given to the underlying select
|
|
input.
|
|
|
|
In practice, we recommend you to create your own helper with your default
|
|
builder:
|
|
|
|
def my_datetime_select(form, field, opts \\ []) do
|
|
builder = fn b ->
|
|
assigns = %{b: b}
|
|
|
|
~H"""
|
|
Date: <%= @b.(:day, []) %> / <%= @b.(:month, []) %> / <%= @b.(:year, []) %>
|
|
Time: <%= @b.(:hour, []) %> : <%= @b.(:minute, []) %>
|
|
"""
|
|
end
|
|
|
|
datetime_select(form, field, [builder: builder] ++ opts)
|
|
end
|
|
|
|
Then you are able to use your own datetime_select throughout your whole
|
|
application.
|
|
|
|
## Supported date values
|
|
|
|
The following values are supported as date:
|
|
|
|
* a map containing the `year`, `month` and `day` keys (either as strings or atoms)
|
|
* a tuple with three elements: `{year, month, day}`
|
|
* a string in ISO 8601 format
|
|
* `nil`
|
|
|
|
## Supported time values
|
|
|
|
The following values are supported as time:
|
|
|
|
* a map containing the `hour` and `minute` keys and an optional `second` key (either as strings or atoms)
|
|
* a tuple with three elements: `{hour, min, sec}`
|
|
* a tuple with four elements: `{hour, min, sec, usec}`
|
|
* `nil`
|
|
|
|
'''
|
|
def datetime_select(form, field, opts \\ []) do
|
|
value = Keyword.get(opts, :value, input_value(form, field) || Keyword.get(opts, :default))
|
|
|
|
builder =
|
|
Keyword.get(opts, :builder) ||
|
|
fn b ->
|
|
date = date_builder(b, opts)
|
|
time = time_builder(b, opts)
|
|
html_escape([date, raw(" — "), time])
|
|
end
|
|
|
|
builder.(datetime_builder(form, field, date_value(value), time_value(value), opts))
|
|
end
|
|
|
|
@doc """
|
|
Generates select tags for date.
|
|
|
|
Warning: This functionality is best provided by browsers nowadays.
|
|
Consider using `date_input/3` instead.
|
|
|
|
Check `datetime_select/3` for more information on options and supported values.
|
|
"""
|
|
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.(datetime_builder(form, field, date_value(value), nil, opts))
|
|
end
|
|
|
|
defp date_builder(b, _opts) do
|
|
html_escape([b.(:year, []), raw(" / "), b.(:month, []), raw(" / "), b.(:day, [])])
|
|
end
|
|
|
|
defp date_value(%{"year" => year, "month" => month, "day" => day}),
|
|
do: %{year: year, month: month, day: day}
|
|
|
|
defp date_value(%{year: year, month: month, day: day}),
|
|
do: %{year: year, month: month, day: day}
|
|
|
|
defp date_value({{year, month, day}, _}), do: %{year: year, month: month, day: day}
|
|
defp date_value({year, month, day}), do: %{year: year, month: month, day: day}
|
|
|
|
defp date_value(nil), do: %{year: nil, month: nil, day: nil}
|
|
|
|
defp date_value(string) when is_binary(string) do
|
|
string
|
|
|> Date.from_iso8601!()
|
|
|> date_value
|
|
end
|
|
|
|
defp date_value(other), do: raise(ArgumentError, "unrecognized date #{inspect(other)}")
|
|
|
|
@doc """
|
|
Generates select tags for time.
|
|
|
|
Warning: This functionality is best provided by browsers nowadays.
|
|
Consider using `time_input/3` instead.
|
|
|
|
Check `datetime_select/3` for more information on options and supported values.
|
|
"""
|
|
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.(datetime_builder(form, field, nil, time_value(value), opts))
|
|
end
|
|
|
|
defp time_builder(b, opts) do
|
|
time = html_escape([b.(:hour, []), raw(" : "), b.(:minute, [])])
|
|
|
|
if Keyword.get(opts, :second) do
|
|
html_escape([time, raw(" : "), b.(:second, [])])
|
|
else
|
|
time
|
|
end
|
|
end
|
|
|
|
defp time_value(%{"hour" => hour, "minute" => min} = map),
|
|
do: %{hour: hour, minute: min, second: Map.get(map, "second", 0)}
|
|
|
|
defp time_value(%{hour: hour, minute: min} = map),
|
|
do: %{hour: hour, minute: min, second: Map.get(map, :second, 0)}
|
|
|
|
defp time_value({_, {hour, min, sec}}),
|
|
do: %{hour: hour, minute: min, second: sec}
|
|
|
|
defp time_value({hour, min, sec}),
|
|
do: %{hour: hour, minute: min, second: sec}
|
|
|
|
defp time_value(nil), do: %{hour: nil, minute: nil, second: nil}
|
|
|
|
defp time_value(string) when is_binary(string) do
|
|
string
|
|
|> Time.from_iso8601!()
|
|
|> time_value
|
|
end
|
|
|
|
defp time_value(other), do: raise(ArgumentError, "unrecognized time #{inspect(other)}")
|
|
|
|
@months [
|
|
{"January", "1"},
|
|
{"February", "2"},
|
|
{"March", "3"},
|
|
{"April", "4"},
|
|
{"May", "5"},
|
|
{"June", "6"},
|
|
{"July", "7"},
|
|
{"August", "8"},
|
|
{"September", "9"},
|
|
{"October", "10"},
|
|
{"November", "11"},
|
|
{"December", "12"}
|
|
]
|
|
|
|
map =
|
|
&Enum.map(&1, fn i ->
|
|
pre = if i < 10, do: "0"
|
|
{"#{pre}#{i}", i}
|
|
end)
|
|
|
|
@days map.(1..31)
|
|
@hours map.(0..23)
|
|
@minsec map.(0..59)
|
|
|
|
defp datetime_builder(form, field, date, time, parent) do
|
|
id = Keyword.get(parent, :id, input_id(form, field))
|
|
name = Keyword.get(parent, :name, input_name(form, field))
|
|
|
|
fn
|
|
:year, opts when date != nil ->
|
|
{year, _, _} = :erlang.date()
|
|
|
|
{value, opts} =
|
|
datetime_options(:year, (year - 5)..(year + 5), id, name, parent, date, opts)
|
|
|
|
select(:datetime, :year, value, opts)
|
|
|
|
:month, opts when date != nil ->
|
|
{value, opts} = datetime_options(:month, @months, id, name, parent, date, opts)
|
|
select(:datetime, :month, value, opts)
|
|
|
|
:day, opts when date != nil ->
|
|
{value, opts} = datetime_options(:day, @days, id, name, parent, date, opts)
|
|
select(:datetime, :day, value, opts)
|
|
|
|
:hour, opts when time != nil ->
|
|
{value, opts} = datetime_options(:hour, @hours, id, name, parent, time, opts)
|
|
select(:datetime, :hour, value, opts)
|
|
|
|
:minute, opts when time != nil ->
|
|
{value, opts} = datetime_options(:minute, @minsec, id, name, parent, time, opts)
|
|
select(:datetime, :minute, value, opts)
|
|
|
|
:second, opts when time != nil ->
|
|
{value, opts} = datetime_options(:second, @minsec, id, name, parent, time, opts)
|
|
select(:datetime, :second, value, opts)
|
|
end
|
|
end
|
|
|
|
defp datetime_options(type, values, id, name, parent, datetime, opts) do
|
|
opts = Keyword.merge(Keyword.get(parent, type, []), opts)
|
|
suff = Atom.to_string(type)
|
|
|
|
{value, opts} = Keyword.pop(opts, :options, values)
|
|
|
|
{value,
|
|
opts
|
|
|> Keyword.put_new(:id, id <> "_" <> suff)
|
|
|> Keyword.put_new(:name, name <> "[" <> suff <> "]")
|
|
|> Keyword.put_new(:value, Map.get(datetime, type))}
|
|
end
|
|
|
|
@doc """
|
|
Generates a label tag.
|
|
|
|
Useful when wrapping another input inside a label.
|
|
|
|
## Examples
|
|
|
|
label do
|
|
radio_button :user, :choice, "Choice"
|
|
end
|
|
#=> <label>...</label>
|
|
|
|
label class: "control-label" do
|
|
radio_button :user, :choice, "Choice"
|
|
end
|
|
#=> <label class="control-label">...</label>
|
|
|
|
"""
|
|
def label(do_block)
|
|
|
|
def label(do: block) do
|
|
content_tag(:label, block, [])
|
|
end
|
|
|
|
def label(opts, do: block) when is_list(opts) do
|
|
content_tag(:label, block, opts)
|
|
end
|
|
|
|
@doc """
|
|
Generates a label tag for the given field.
|
|
|
|
The form should either be a `Phoenix.HTML.Form` emitted
|
|
by `form_for` or an atom.
|
|
|
|
All given options are forwarded to the underlying tag.
|
|
A default value is provided for `for` attribute but can
|
|
be overridden if you pass a value to the `for` option.
|
|
Text content would be inferred from `field` if not specified
|
|
as either a function argument or string value in a block.
|
|
|
|
To wrap a label around an input, see `label/1`.
|
|
|
|
## Examples
|
|
|
|
# Assuming form contains a User schema
|
|
label(form, :name, "Name")
|
|
#=> <label for="user_name">Name</label>
|
|
|
|
label(:user, :email, "Email")
|
|
#=> <label for="user_email">Email</label>
|
|
|
|
label(:user, :email)
|
|
#=> <label for="user_email">Email</label>
|
|
|
|
label(:user, :email, class: "control-label")
|
|
#=> <label for="user_email" class="control-label">Email</label>
|
|
|
|
label :user, :email do
|
|
"E-mail Address"
|
|
end
|
|
#=> <label for="user_email">E-mail Address</label>
|
|
|
|
label :user, :email, "E-mail Address", class: "control-label"
|
|
#=> <label class="control-label" for="user_email">E-mail Address</label>
|
|
|
|
label :user, :email, class: "control-label" do
|
|
"E-mail Address"
|
|
end
|
|
#=> <label class="control-label" for="user_email">E-mail Address</label>
|
|
|
|
"""
|
|
def label(form, field) when is_atom(field) or is_binary(field) do
|
|
label(form, field, humanize(field), [])
|
|
end
|
|
|
|
@doc """
|
|
See `label/2`.
|
|
"""
|
|
def label(form, field, text_or_do_block_or_attributes)
|
|
|
|
def label(form, field, do: block) do
|
|
label(form, field, [], do: block)
|
|
end
|
|
|
|
def label(form, field, opts) when is_list(opts) do
|
|
label(form, field, humanize(field), opts)
|
|
end
|
|
|
|
def label(form, field, text) do
|
|
label(form, field, text, [])
|
|
end
|
|
|
|
@doc """
|
|
See `label/2`.
|
|
"""
|
|
def label(form, field, text, do_block_or_attributes)
|
|
|
|
def label(form, field, opts, do: block) when is_list(opts) do
|
|
opts = Keyword.put_new(opts, :for, input_id(form, field))
|
|
content_tag(:label, block, opts)
|
|
end
|
|
|
|
def label(form, field, text, opts) when is_list(opts) do
|
|
opts = Keyword.put_new(opts, :for, input_id(form, field))
|
|
content_tag(:label, text, opts)
|
|
end
|
|
|
|
# Normalize field name to string version
|
|
defp field_to_string(field) when is_atom(field), do: Atom.to_string(field)
|
|
defp field_to_string(field) when is_binary(field), do: field
|
|
|
|
# Helper for getting field errors, handling string fields
|
|
defp field_errors(errors, field)
|
|
when is_list(errors) and (is_atom(field) or is_binary(field)) do
|
|
for {^field, error} <- errors, do: error
|
|
end
|
|
end
|