defmodule PhoenixHTMLHelpers.Form do @moduledoc ~S""" Functions for generating forms and inputs HTML. """ alias Phoenix.HTML.Form import Phoenix.HTML import Phoenix.HTML.Form import PhoenixHTMLHelpers.Tag @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 an `Ecto.Changeset` 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 -> %> <%= 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(), (Form.t() -> Phoenix.HTML.unsafe())) :: Phoenix.HTML.safe() @spec form_for( Phoenix.HTML.FormData.t(), String.t(), Keyword.t(), (Form.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) html_escape([form_tag(action, form.options), fun.(form), raw("")]) end @doc false def inputs_for(form, field) when is_atom(field) or is_binary(field), do: inputs_for(form, field, []) @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(Form.t(), Form.field(), (Form.t() -> Phoenix.HTML.unsafe())) :: Phoenix.HTML.safe() @spec inputs_for(Form.t(), Form.field(), Keyword.t(), (Form.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(Form.t(), Form.field()) :: atom def input_type(%{impl: impl, source: source} = form, field, mapping \\ @mapping) when is_atom(field) or is_binary(field) do type = if function_exported?(impl, :input_type, 3) do impl.input_type(source, form, field) else :text_input end 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) #=> text_input(:user, :name) #=> """ 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(Form.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 #=> time_input form, :time, precision: :second #=> time_input form, :time, precision: :millisecond #=> """ 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) #=> ## 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" #=> """ 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" #=> submit "Submit", class: "btn" #=> submit [class: "btn"], do: "Submit" #=> """ 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" #=> reset "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") #=> ## 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) #=> #=> ## 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 `` and the value will be used to generate `` nested under the group. ## Examples # Assuming form contains a User schema select(form, :age, 0..120) #=> select(form, :role, ["Admin": "admin", "User": "user"]) #=> select(form, :role, [[key: "Admin", value: "admin", disabled: true], [key: "User", value: "user"]]) #=> You can also pass a prompt: select(form, :role, ["Admin": "admin", "User": "user"], prompt: "Choose your role") #=> And customize the prompt like any other entry: select(form, :role, ["Admin": "admin", "User": "user"], prompt: [key: "Choose your role", disabled: true]) #=> 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})) #=> Finally, if the values are a list or a map, we use the keys for grouping: select(form, :country, ["Europe": ["UK", "Sweden", "France"]], ...) #=> ## 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]) #=> multiple_select(form, :roles, ["Admin": 1, "Power User": 2], selected: [1]) #=> 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 #=> / #=> / #=> — #=> : #=> 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 class: "control-label" do radio_button :user, :choice, "Choice" end #=> """ 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(:user, :email, "Email") #=> label(:user, :email) #=> label(:user, :email, class: "control-label") #=> label :user, :email do "E-mail Address" end #=> label :user, :email, "E-mail Address", class: "control-label" #=> label :user, :email, class: "control-label" do "E-mail Address" end #=> """ 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 end