diff --git a/README.md b/README.md
index d316ab5..cfb3e92 100644
--- a/README.md
+++ b/README.md
@@ -2,11 +2,13 @@
[![Build Status](https://github.com/phoenixframework/phoenix_html/workflows/Tests/badge.svg)](https://github.com/phoenixframework/phoenix_html/actions?query=workflow%3ATests)
-Collection of helpers to generate and manipulate HTML contents.
+The default building blocks for working with HTML safely in Phoenix.
-Although this project was originally extracted from Phoenix,
-it does not depend on Phoenix and can be used with any Plug
-application (or even without Plug).
+This library provides three main functionalities:
+
+ * HTML safety
+ * Form abstractions
+ * A tiny JavaScript library to enhance applications
See the [docs](https://hexdocs.pm/phoenix_html/) for more information.
diff --git a/lib/phoenix_html.ex b/lib/phoenix_html.ex
index 74d2374..f0c72b6 100644
--- a/lib/phoenix_html.ex
+++ b/lib/phoenix_html.ex
@@ -6,7 +6,7 @@ defmodule Phoenix.HTML do
This library provides three main functionalities:
* HTML safety
- * Form handling (with CSRF protection)
+ * Form abstractions
* A tiny JavaScript library to enhance applications
## HTML safety
@@ -74,15 +74,19 @@ defmodule Phoenix.HTML do
"""
@doc false
- # TODO: Deprecate me
defmacro __using__(_) do
- quote do
- import Phoenix.HTML
- import Phoenix.HTML.Form
- import Phoenix.HTML.Link
- import Phoenix.HTML.Tag, except: [attributes_escape: 1]
- import Phoenix.HTML.Format
- end
+ raise """
+ use Phoenix.HTML is no longer supported in v4.0.
+
+ To keep compatibility with previous versions, \
+ add {:phoenix_html_helpers, "~> 1.0"} to your mix.exs deps
+ and then, instead of "use Phoenix.HTML", you might:
+
+ import Phoenix.HTML
+ import Phoenix.HTML.Form
+ use PhoenixHTMLHelpers
+
+ """
end
@typedoc "Guaranteed to be safe"
@@ -91,36 +95,6 @@ defmodule Phoenix.HTML do
@typedoc "May be safe or unsafe (i.e. it needs to be converted)"
@type unsafe :: Phoenix.HTML.Safe.t()
- @doc false
- @deprecated "use the ~H sigil instead"
- defmacro sigil_e(expr, opts) do
- handle_sigil(expr, opts, __CALLER__)
- end
-
- @doc false
- @deprecated "use the ~H sigil instead"
- defmacro sigil_E(expr, opts) do
- handle_sigil(expr, opts, __CALLER__)
- end
-
- defp handle_sigil({:<<>>, meta, [expr]}, [], caller) do
- options = [
- engine: Phoenix.HTML.Engine,
- file: caller.file,
- line: caller.line + 1,
- indentation: meta[:indentation] || 0
- ]
-
- EEx.compile_string(expr, options)
- end
-
- defp handle_sigil(_, _, _) do
- raise ArgumentError,
- "interpolation not allowed in ~e sigil. " <>
- "Remove the interpolation, use <%= %> to insert values, " <>
- "or use ~E to show the interpolation literally"
- end
-
@doc """
Marks the given content as raw.
diff --git a/lib/phoenix_html/form.ex b/lib/phoenix_html/form.ex
index 849da38..794252d 100644
--- a/lib/phoenix_html/form.ex
+++ b/lib/phoenix_html/form.ex
@@ -1,7 +1,4 @@
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.
@@ -31,7 +28,6 @@ defmodule Phoenix.HTML.Form do
alias Phoenix.HTML.Form
import Phoenix.HTML
- import Phoenix.HTML.Tag
@doc """
Defines the Phoenix.HTML.Form struct.
@@ -72,8 +68,7 @@ defmodule Phoenix.HTML.Form do
params: %{},
errors: [],
options: [],
- index: nil,
- action: nil
+ index: nil
@type t :: %Form{
source: Phoenix.HTML.FormData.t(),
@@ -85,8 +80,7 @@ defmodule Phoenix.HTML.Form do
errors: [{field, term}],
impl: module,
id: String.t(),
- index: nil | non_neg_integer,
- action: nil | String.t()
+ index: nil | non_neg_integer
}
@type field :: atom | String.t()
@@ -341,1501 +335,6 @@ defmodule Phoenix.HTML.Form do
[?<, 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 -> %>
-
-
-
-
- <%= 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("")])
- 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)
- #=>
-
- 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(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 `