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 `` 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 - # 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 diff --git a/lib/phoenix_html/form_data.ex b/lib/phoenix_html/form_data.ex index 035d5dd..02eec75 100644 --- a/lib/phoenix_html/form_data.ex +++ b/lib/phoenix_html/form_data.ex @@ -73,16 +73,9 @@ defprotocol Phoenix.HTML.FormData do """ @spec input_validations(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field()) :: Keyword.t() def input_validations(data, form, field) - - @doc """ - Receives the given field and returns its input type (:text_input, - :select, etc). Returns `nil` if the type is unknown. - """ - @spec input_type(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field()) :: atom | nil - def input_type(data, form, field) end -defimpl Phoenix.HTML.FormData, for: [Plug.Conn, Atom, Map] do +defimpl Phoenix.HTML.FormData, for: Map do def to_form(conn_or_atom_or_map, opts) do {name, params, opts} = name_params_and_opts(conn_or_atom_or_map, opts) {errors, opts} = Keyword.pop(opts, :errors, []) @@ -104,39 +97,18 @@ defimpl Phoenix.HTML.FormData, for: [Plug.Conn, Atom, Map] do } end - case @for do - Atom -> - defp name_params_and_opts(atom, opts) do - {params, opts} = Keyword.pop(opts, :params, %{}) - {Atom.to_string(atom), params, opts} - end - - Map -> - defp name_params_and_opts(map, opts) do - with {key, _, _} when is_atom(key) <- :maps.next(:maps.iterator(map)) do - IO.warn( - "a map with atom keys was given to a form. Maps are always considered " <> - "parameters and therefore must have string keys, got: #{inspect(map)}" - ) - end - - case Keyword.pop(opts, :as) do - {nil, opts} -> {nil, map, opts} - {name, opts} -> {to_string(name), map, opts} - end - end - - Plug.Conn -> - defp name_params_and_opts(conn, opts) do - case Keyword.pop(opts, :as) do - {nil, opts} -> - {nil, conn.params, opts} + defp name_params_and_opts(map, opts) do + with {key, _, _} when is_atom(key) <- :maps.next(:maps.iterator(map)) do + IO.warn( + "a map with atom keys was given to a form. Maps are always considered " <> + "parameters and therefore must have string keys, got: #{inspect(map)}" + ) + end - {name, opts} -> - name = to_string(name) - {name, Map.get(conn.params, name) || %{}, opts} - end - end + case Keyword.pop(opts, :as) do + {nil, opts} -> {nil, map, opts} + {name, opts} -> {to_string(name), map, opts} + end end def to_form(conn_or_atom_or_map, form, field, opts) when is_atom(field) or is_binary(field) do @@ -206,7 +178,6 @@ defimpl Phoenix.HTML.FormData, for: [Plug.Conn, Atom, Map] do end end - def input_type(_conn_or_atom_or_map, _form, _field), do: :text_input def input_validations(_conn_or_atom_or_map, _form, _field), do: [] # Normalize field name to string version diff --git a/lib/phoenix_html/format.ex b/lib/phoenix_html/format.ex deleted file mode 100644 index f5a4e5d..0000000 --- a/lib/phoenix_html/format.ex +++ /dev/null @@ -1,79 +0,0 @@ -defmodule Phoenix.HTML.Format do - @moduledoc false - - @doc ~S""" - Returns text transformed into HTML using simple formatting rules. - - Two or more consecutive newlines `\n\n` or `\r\n\r\n` are considered as a - paragraph and text between them is wrapped in `

` tags. - One newline `\n` or `\r\n` is considered as a linebreak and a `
` tag is inserted. - - ## Examples - - iex> text_to_html("Hello\n\nWorld") |> safe_to_string - "

Hello

\n

World

\n" - - iex> text_to_html("Hello\nWorld") |> safe_to_string - "

Hello
\nWorld

\n" - - iex> opts = [wrapper_tag: :div, attributes: [class: "p"]] - ...> text_to_html("Hello\n\nWorld", opts) |> safe_to_string - "
Hello
\n
World
\n" - - ## Options - - * `:escape` - if `false` does not html escape input (default: `true`) - * `:wrapper_tag` - tag to wrap each paragraph (default: `:p`) - * `:attributes` - html attributes of the wrapper tag (default: `[]`) - * `:insert_brs` - if `true` insert `
` for single line breaks (default: `true`) - - """ - @spec text_to_html(Phoenix.HTML.unsafe(), Keyword.t()) :: Phoenix.HTML.safe() - def text_to_html(string, opts \\ []) do - escape? = Keyword.get(opts, :escape, true) - wrapper_tag = Keyword.get(opts, :wrapper_tag, :p) - attributes = Keyword.get(opts, :attributes, []) - insert_brs? = Keyword.get(opts, :insert_brs, true) - - string - |> maybe_html_escape(escape?) - |> String.split(["\n\n", "\r\n\r\n"], trim: true) - |> Enum.filter(¬_blank?/1) - |> Enum.map(&wrap_paragraph(&1, wrapper_tag, attributes, insert_brs?)) - |> Phoenix.HTML.html_escape() - end - - defp maybe_html_escape(string, true), - do: string |> Phoenix.HTML.Engine.html_escape() |> IO.iodata_to_binary() - - defp maybe_html_escape(string, false), - do: string - - defp not_blank?("\r\n" <> rest), do: not_blank?(rest) - defp not_blank?("\n" <> rest), do: not_blank?(rest) - defp not_blank?(" " <> rest), do: not_blank?(rest) - defp not_blank?(""), do: false - defp not_blank?(_), do: true - - defp wrap_paragraph(text, tag, attributes, insert_brs?) do - [Phoenix.HTML.Tag.content_tag(tag, insert_brs(text, insert_brs?), attributes), ?\n] - end - - defp insert_brs(text, false) do - text - |> split_lines() - |> Enum.intersperse(?\s) - |> Phoenix.HTML.raw() - end - - defp insert_brs(text, true) do - text - |> split_lines() - |> Enum.map(&Phoenix.HTML.raw/1) - |> Enum.intersperse([Phoenix.HTML.Tag.tag(:br), ?\n]) - end - - defp split_lines(text) do - String.split(text, ["\n", "\r\n"], trim: true) - end -end diff --git a/lib/phoenix_html/link.ex b/lib/phoenix_html/link.ex deleted file mode 100644 index 38d2506..0000000 --- a/lib/phoenix_html/link.ex +++ /dev/null @@ -1,204 +0,0 @@ -defmodule Phoenix.HTML.Link do - @moduledoc """ - Conveniences for working with links and URLs in HTML. - """ - - import Phoenix.HTML.Tag - - @doc """ - Generates a link to the given URL. - - ## Examples - - link("hello", to: "/world") - #=> hello - - link("hello", to: URI.parse("https://elixir-lang.org")) - #=> hello - - link("", to: "/world") - #=> <hello> - - link("", to: "/world", class: "btn") - #=> <hello> - - link("delete", to: "/the_world", data: [confirm: "Really?"]) - #=> delete - - # If you supply a method other than `:get`: - link("delete", to: "/everything", method: :delete) - #=> delete - - # You can use a `do ... end` block too: - link to: "/hello" do - "world" - end - #=> world - - ## Options - - * `:to` - the page to link to. This option is required - - * `:method` - the method to use with the link. In case the - method is not `:get`, the link is generated inside the form - which sets the proper information. In order to submit the - form, JavaScript must be enabled - - * `:csrf_token` - a custom token to use for links with a method - other than `:get`. - - All other options are forwarded to the underlying `` tag. - - ## Data attributes - - Data attributes are added as a keyword list passed to the `data` key. - The following data attributes are supported: - - * `data-confirm` - shows a confirmation prompt before - generating and submitting the form when `:method` - is not `:get`. - - ## CSRF Protection - - By default, CSRF tokens are generated through `Plug.CSRFProtection`. - """ - def link(text, opts) - - def link(opts, do: contents) when is_list(opts) do - link(contents, opts) - end - - def link(_text, opts) when not is_list(opts) do - raise ArgumentError, "link/2 requires a keyword list as second argument" - end - - def link(text, opts) do - {to, opts} = pop_required_option!(opts, :to, "expected non-nil value for :to in link/2") - {method, opts} = Keyword.pop(opts, :method, :get) - - if method == :get do - # Call link attributes to validate `to` - [data: data] = link_attributes(to, []) - content_tag(:a, text, [href: data[:to]] ++ Keyword.delete(opts, :csrf_token)) - else - {csrf_token, opts} = Keyword.pop(opts, :csrf_token, true) - opts = Keyword.put_new(opts, :rel, "nofollow") - [data: data] = link_attributes(to, method: method, csrf_token: csrf_token) - content_tag(:a, text, [data: data, href: data[:to]] ++ opts) - end - end - - @doc """ - Generates a button tag that uses the Javascript function handleClick() - (see phoenix_html.js) to submit the form data. - - Useful to ensure that links that change data are not triggered by - search engines and other spidering software. - - ## Examples - - button("hello", to: "/world") - #=> - - button("hello", to: "/world", method: :get, class: "btn") - #=> - - ## Options - - * `:to` - the page to link to. This option is required - - * `:method` - the method to use with the button. Defaults to :post. - - All other options are forwarded to the underlying button input. - - When the `:method` is set to `:get` and the `:to` URL contains query - parameters the generated form element will strip the parameters in accordance - with the [W3C](https://www.w3.org/TR/html401/interact/forms.html#h-17.13.3.4) - form specification. - - ## Data attributes - - Data attributes are added as a keyword list passed to the - `data` key. The following data attributes are supported: - - * `data-confirm` - shows a confirmation prompt before generating and - submitting the form. - """ - def button(opts, do: contents) do - button(contents, opts) - end - - def button(text, opts) do - {to, opts} = pop_required_option!(opts, :to, "option :to is required in button/2") - - {link_opts, opts} = - opts - |> Keyword.put_new(:method, :post) - |> Keyword.split([:method, :csrf_token]) - - content_tag(:button, text, link_attributes(to, link_opts) ++ opts) - end - - defp pop_required_option!(opts, key, error_message) do - {value, opts} = Keyword.pop(opts, key) - - unless value do - raise ArgumentError, error_message - end - - {value, opts} - end - - defp link_attributes(to, opts) do - to = valid_destination!(to) - method = Keyword.get(opts, :method, :get) - data = [method: method, to: to] - - data = - if method == :get do - data - else - case Keyword.get(opts, :csrf_token, true) do - true -> [csrf: Phoenix.HTML.Tag.csrf_token_value(to)] ++ data - false -> data - csrf when is_binary(csrf) -> [csrf: csrf] ++ data - end - end - - [data: data] - end - - defp valid_destination!(%URI{} = uri) do - valid_destination!(URI.to_string(uri)) - end - - defp valid_destination!({:safe, to}) do - {:safe, valid_string_destination!(IO.iodata_to_binary(to))} - end - - defp valid_destination!({other, to}) when is_atom(other) do - [Atom.to_string(other), ?:, to] - end - - defp valid_destination!(to) do - valid_string_destination!(IO.iodata_to_binary(to)) - end - - @valid_uri_schemes ~w(http: https: ftp: ftps: mailto: news: irc: gopher:) ++ - ~w(nntp: feed: telnet: mms: rtsp: svn: tel: fax: xmpp:) - - for scheme <- @valid_uri_schemes do - defp valid_string_destination!(unquote(scheme) <> _ = string), do: string - end - - defp valid_string_destination!(to) do - if not match?("/" <> _, to) and String.contains?(to, ":") do - raise ArgumentError, """ - unsupported scheme given as link. In case you want to link to an - unknown or unsafe scheme, such as javascript, use a tuple: {:javascript, rest}\ - """ - else - to - end - end -end diff --git a/lib/phoenix_html/tag.ex b/lib/phoenix_html/tag.ex deleted file mode 100644 index 5af5c08..0000000 --- a/lib/phoenix_html/tag.ex +++ /dev/null @@ -1,314 +0,0 @@ -defmodule Phoenix.HTML.Tag do - @moduledoc ~S""" - Helpers related to producing HTML tags within templates. - - > Note: with the addition of the HEEx template engine to - > Phoenix applications, the functions in this module have - > lost a bit of relevance and must only be used in special - > circumstances. - > - > Whenever possible, prefer to use the HEEx template engine - > instead of the functions here. For example, instead of: - > - > <%= content_tag :div, class: @class do %> - > Hello - > <% end %> - > - > Do: - > - >
- > Hello - >
- - > Note: the examples in this module use `safe_to_string/1` - > imported from `Phoenix.HTML` for readability. - """ - - import Phoenix.HTML, except: [attributes_escape: 1] - - @csrf_param "_csrf_token" - - @doc ~S""" - Creates an HTML tag with the given name and options. - - iex> safe_to_string tag(:br) - "
" - iex> safe_to_string tag(:input, type: "text", name: "user_id") - "" - - ## Data attributes - - In order to add custom data attributes you need to pass - a tuple containing :data atom and a keyword list - with data attributes' names and values as the first element - in the tag's attributes keyword list: - - iex> safe_to_string tag(:input, [data: [foo: "bar"], id: "some_id"]) - "" - - ## Boolean values - - In case an attribute contains a boolean value, its key - is repeated when it is true, as expected in HTML, or - the attribute is completely removed if it is false: - - iex> safe_to_string tag(:audio, autoplay: "autoplay") - "
hello) - end - - test "link with put/delete using a custom csrf token" do - assert safe_to_string(link("hello", to: "/world", method: :put)) =~ - ~r(hello) - end - - test "button with post using a custom csrf token" do - assert safe_to_string(button("hello", to: "/world")) =~ - ~r() - end - - test "form_tag for post using a custom csrf token" do - assert safe_to_string(form_tag("/")) =~ ~r( - - - )mx - end - - test "form_tag for other method using a custom csrf token" do - assert safe_to_string(form_tag("/", method: :put)) =~ ~r( - - - - )mx - end -end diff --git a/test/phoenix_html/form_test.exs b/test/phoenix_html/form_test.exs index 65e3063..139ffe8 100644 --- a/test/phoenix_html/form_test.exs +++ b/test/phoenix_html/form_test.exs @@ -239,1553 +239,4 @@ defmodule Phoenix.HTML.FormTest do } end end - - ## TODO: Move on v4.0 - - defp search_params do - %{ - "key" => "value", - "time" => ~T[01:02:03.004005], - "datetime" => %{ - "year" => "2020", - "month" => "4", - "day" => "17", - "hour" => "2", - "minute" => "11", - "second" => "13" - }, - "naive_datetime" => ~N[2000-01-01 10:00:42] - } - end - - describe "form_for/4 with map" do - test "with :as" do - search_params = search_params() - - form = - safe_to_string( - form_for(search_params, "/", [as: :search], fn f -> - assert f.impl == Phoenix.HTML.FormData.Map - assert f.name == "search" - assert f.source == search_params - assert f.params["key"] == "value" - "" - end) - ) - - assert form =~ ~s(
) - end - - test "without :as" do - form = - safe_to_string( - form_for(search_params(), "/", fn f -> - text_input(f, :key) - end) - ) - - assert form =~ ~s() - end - - test "with custom options" do - form = - safe_to_string( - form_for(search_params(), "/", [as: :search, method: :put, multipart: true], fn f -> - refute f.options[:name] - assert f.options[:multipart] == true - assert f.options[:method] == :put - "" - end) - ) - - assert form =~ - ~s() - - assert form =~ ~s() - end - - test "is html safe" do - form = safe_to_string(form_for(search_params(), "/", [as: :search], fn _ -> "<>" end)) - assert form =~ ~s(<>
) - end - - test "with type and validations" do - form = - safe_to_string( - form_for(search_params(), "/", [as: :search], fn f -> - assert input_type(f, :hello) == :text_input - assert input_type(f, :email) == :email_input - assert input_type(f, :search) == :search_input - assert input_type(f, :password) == :password_input - assert input_type(f, :special_url) == :url_input - assert input_type(f, :number, %{"number" => :number_input}) == :number_input - assert input_validations(f, :hello) == [] - "" - end) - ) - - assert form =~ " - for {field, {message, _}} <- f.errors do - Phoenix.HTML.Tag.content_tag(:span, humanize(field) <> " " <> message, - class: "errors" - ) - end - end) - ) - - assert form =~ ~s(Field error message!) - end - end - - defp conn do - Plug.Test.conn(:get, "/foo", %{"search" => search_params()}) - end - - def conn_form(fun, opts \\ [as: :search]) do - mark = "--PLACEHOLDER--" - - contents = - safe_to_string( - form_for(conn(), "/", opts, fn f -> - html_escape([mark, fun.(f), mark]) - end) - ) - - [_, inner, _] = String.split(contents, mark) - inner - end - - describe "form_for/4 with connection" do - test "with :as" do - conn = conn() - - form = - safe_to_string( - form_for(conn, "/", [as: :search], fn f -> - assert f.impl == Phoenix.HTML.FormData.Plug.Conn - assert f.name == "search" - assert f.source == conn - assert f.params["key"] == "value" - "" - end) - ) - - assert form =~ ~s(
) - end - - test "without :as" do - form = - safe_to_string( - form_for(conn(), "/", fn f -> - text_input(f, :key) - end) - ) - - assert form =~ ~s() - end - - test "with custom options" do - form = - safe_to_string( - form_for(conn(), "/", [as: :search, method: :put, multipart: true], fn f -> - refute f.options[:name] - assert f.options[:multipart] == true - assert f.options[:method] == :put - "" - end) - ) - - assert form =~ - ~s() - - assert form =~ ~s() - end - - test "is html safe" do - form = safe_to_string(form_for(conn(), "/", [as: :search], fn _ -> "<>" end)) - assert form =~ ~s(<>
) - end - - test "with type and validations" do - form = - safe_to_string( - form_for(conn(), "/", [as: :search], fn f -> - assert input_type(f, :hello) == :text_input - assert input_type(f, :email) == :email_input - assert input_type(f, :search) == :search_input - assert input_type(f, :password) == :password_input - assert input_type(f, :special_url) == :url_input - assert input_type(f, :number, %{"number" => :number_input}) == :number_input - assert input_validations(f, :hello) == [] - "" - end) - ) - - assert form =~ " - for {field, {message, _}} <- f.errors do - Phoenix.HTML.Tag.content_tag(:span, humanize(field) <> " " <> message, - class: "errors" - ) - end - end) - ) - - assert form =~ ~s(Field error message!) - end - end - - describe "form_for/4 with atom" do - test "without params" do - form = - safe_to_string( - form_for(:search, "/", fn f -> - assert f.impl == Phoenix.HTML.FormData.Atom - assert f.name == "search" - assert f.source == :search - assert f.params == %{} - "" - end) - ) - - assert form =~ ~s(
) - end - - test "with params" do - form = - safe_to_string( - form_for(:search, "/", [params: search_params()], fn f -> - text_input(f, :key) - end) - ) - - assert form =~ ~s() - end - - test "with custom options" do - form = - safe_to_string( - form_for(:search, "/", [method: :put, multipart: true], fn f -> - refute f.options[:name] - assert f.options[:multipart] == true - assert f.options[:method] == :put - "" - end) - ) - - assert form =~ - ~s() - - assert form =~ ~s() - end - - test "is html safe" do - form = safe_to_string(form_for(conn(), "/", [as: :search], fn _ -> "<>" end)) - assert form =~ ~s(<>
) - end - - test "with type and validations" do - form = - safe_to_string( - form_for(:search, "/", [], fn f -> - assert input_type(f, :hello) == :text_input - assert input_type(f, :email) == :email_input - assert input_type(f, :search) == :search_input - assert input_type(f, :password) == :password_input - assert input_type(f, :special_url) == :url_input - assert input_type(f, :number, %{"number" => :number_input}) == :number_input - assert input_validations(f, :hello) == [] - "" - end) - ) - - assert form =~ " - for {field, {message, _}} <- f.errors do - Phoenix.HTML.Tag.content_tag(:span, humanize(field) <> " " <> message, - class: "errors" - ) - end - end) - ) - - assert form =~ ~s(Field error message!) - end - - test "with id prefix the form id in the input id" do - form = - safe_to_string( - form_for(:search, "/", [params: search_params(), id: "form_id"], fn f -> - text_input(f, :key) - end) - ) - - assert form =~ - ~s() - end - end - - describe "inputs_for/4" do - test "generate a new form builder for the given parameter" do - form = - form_for(%{}, "/", [as: :user], fn form -> - inputs_for(form, :company, fn company_form -> - text_input(company_form, :name) - end) - end) - |> safe_to_string() - - assert form =~ ~s() - end - - test "generate a new form builder with hidden inputs when they are present" do - form = - form_for(%{}, "/", [as: :user], fn form -> - inputs_for(form, :company, [hidden: [id: 1]], fn company_form -> - text_input(company_form, :name) - end) - end) - |> safe_to_string() - - assert form =~ - ~s(input id="user_company_id" name="user[company][id]" type="hidden" value="1">) - - assert form =~ ~s() - end - - test "skip hidden inputs" do - form = - form_for(%{}, "/", [as: :user], fn form -> - inputs_for(form, :company, [skip_hidden: true, hidden: [id: 1]], fn company_form -> - text_input(company_form, :name) - end) - end) - |> safe_to_string() - - refute form =~ - ~s(input id="user_company_id" name="user[company][id]" type="hidden" value="1">) - - assert form =~ ~s() - end - end - - ## text_input/3 - - test "text_input/3" do - assert safe_to_string(text_input(:search, :key)) == - ~s() - - assert safe_to_string( - text_input(:search, :key, value: "foo", id: "key", name: "search[key][]") - ) == ~s() - end - - test "text_input/3 with form" do - assert conn_form(&text_input(&1, :key)) == - ~s() - - assert conn_form(&text_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == - ~s() - end - - test "text_input/3 with form and data" do - assert conn_form(&text_input(put_in(&1.data[:key], "original"), :key)) == - ~s() - - assert conn_form(&text_input(put_in(&1.data[:no_key], "original"), :no_key)) == - ~s() - end - - ## textarea/3 - - test "textarea/3" do - assert safe_to_string(textarea(:search, :key)) == - ~s() - - assert safe_to_string(textarea(:search, :key)) == - ~s() - - assert safe_to_string(textarea(:search, :key, id: "key", name: "search[key][]")) == - ~s() - end - - test "textarea/3 with form" do - assert conn_form(&textarea(&1, :key)) == - ~s() - - assert conn_form(&textarea(&1, :key, value: "foo", id: "key", name: "search[key][]")) == - ~s() - end - - test "textarea/3 with non-binary type" do - assert conn_form(&textarea(&1, :key, value: :atom_value)) == - ~s() - end - - ## number_input/3 - - test "number_input/3" do - assert safe_to_string(number_input(:search, :key)) == - ~s() - - assert safe_to_string( - number_input(:search, :key, value: "foo", id: "key", name: "search[key][]") - ) == ~s() - end - - test "number_input/3 with form" do - assert conn_form(&number_input(&1, :key)) == - ~s() - - assert conn_form(&number_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == - ~s() - end - - ## hidden_input/3 - - test "hidden_input/3" do - assert safe_to_string(hidden_input(:search, :key)) == - ~s() - - assert safe_to_string( - hidden_input(:search, :key, value: "foo", id: "key", name: "search[key][]") - ) == ~s() - - assert safe_to_string( - hidden_input(:search, :key, value: true, id: "key", name: "search[key][]") - ) == ~s() - - assert safe_to_string( - hidden_input(:search, :key, value: false, id: "key", name: "search[key][]") - ) == ~s() - end - - test "hidden_input/3 with form" do - assert conn_form(&hidden_input(&1, :key)) == - ~s() - - assert conn_form(&hidden_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == - ~s() - end - - describe "hidden_inputs_for/1" do - test "generates hidden fields from the given form" do - form = %{Phoenix.HTML.FormData.to_form(conn(), []) | hidden: [id: 1]} - - assert hidden_inputs_for(form) == [hidden_input(form, :id, value: 1)] - end - - test "generates hidden fields for lists from the given form" do - form = %{Phoenix.HTML.FormData.to_form(conn(), []) | hidden: [field: ["a", "b", "c"]]} - - assert hidden_inputs_for(form) == - [ - hidden_input(form, :field, name: "field[]", id: "field_0", value: "a"), - hidden_input(form, :field, name: "field[]", id: "field_1", value: "b"), - hidden_input(form, :field, name: "field[]", id: "field_2", value: "c") - ] - end - end - - ## email_input/3 - - test "email_input/3" do - assert safe_to_string(email_input(:search, :key)) == - ~s() - - assert safe_to_string( - email_input(:search, :key, value: "foo", id: "key", name: "search[key][]") - ) == ~s() - end - - test "email_input/3 with form" do - assert conn_form(&email_input(&1, :key)) == - ~s() - - assert conn_form(&email_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == - ~s() - end - - ## password_input/3 - - test "password_input/3" do - assert safe_to_string(password_input(:search, :key)) == - ~s() - - assert safe_to_string( - password_input(:search, :key, value: "foo", id: "key", name: "search[key][]") - ) == ~s() - end - - test "password_input/3 with form" do - assert conn_form(&password_input(&1, :key)) == - ~s() - - assert conn_form(&password_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == - ~s() - end - - ## file_input/3 - - test "file_input/3" do - assert safe_to_string(file_input(:search, :key)) == - ~s() - - assert safe_to_string(file_input(:search, :key, id: "key", name: "search[key][]")) == - ~s() - - assert safe_to_string(file_input(:search, :key, multiple: true)) == - ~s() - end - - test "file_input/3 with form" do - assert_raise ArgumentError, fn -> - conn_form(&file_input(&1, :key)) - end - - assert conn_form(&file_input(&1, :key), multipart: true, as: :search) == - ~s() - end - - ## url_input/3 - - test "url_input/3" do - assert safe_to_string(url_input(:search, :key)) == - ~s() - - assert safe_to_string( - url_input(:search, :key, value: "foo", id: "key", name: "search[key][]") - ) == ~s() - end - - test "url_input/3 with form" do - assert conn_form(&url_input(&1, :key)) == - ~s() - - assert conn_form(&url_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == - ~s() - end - - ## search_input/3 - - test "search_input/3" do - assert safe_to_string(search_input(:search, :key)) == - ~s() - - assert safe_to_string( - search_input(:search, :key, value: "foo", id: "key", name: "search[key][]") - ) == ~s() - end - - test "search_input/3 with form" do - assert conn_form(&search_input(&1, :key)) == - ~s() - - assert conn_form(&search_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == - ~s() - end - - ## color_input/3 - - test "color_input/3" do - assert safe_to_string(color_input(:search, :key)) == - ~s() - - assert safe_to_string( - color_input(:search, :key, value: "#123456", id: "key", name: "search[key][]") - ) == ~s() - end - - test "color_input/3 with form" do - assert conn_form(&color_input(&1, :key)) == - ~s() - - assert conn_form(&color_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == - ~s() - end - - ## telephone_input/3 - - test "telephone_input/3" do - assert safe_to_string(telephone_input(:search, :key)) == - ~s() - - assert safe_to_string( - telephone_input(:search, :key, value: "foo", id: "key", name: "search[key][]") - ) == ~s() - end - - test "telephone_input/3 with form" do - assert conn_form(&telephone_input(&1, :key)) == - ~s() - - assert conn_form(&telephone_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == - ~s() - end - - ## range_input/3 - - test "range_input/3" do - assert safe_to_string(range_input(:search, :key)) == - ~s() - - assert safe_to_string( - range_input(:search, :key, value: "foo", id: "key", name: "search[key][]") - ) == ~s() - end - - test "range_input/3 with form" do - assert conn_form(&range_input(&1, :key)) == - ~s() - - assert conn_form(&range_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == - ~s() - end - - ## date_input/3 - - test "date_input/3" do - assert safe_to_string(date_input(:search, :key)) == - ~s() - - assert safe_to_string( - date_input(:search, :key, value: "foo", id: "key", name: "search[key][]") - ) == ~s() - - assert safe_to_string( - date_input(:search, :key, value: ~D[2017-09-21], id: "key", name: "search[key][]") - ) == ~s() - end - - test "date_input/3 with form" do - assert conn_form(&date_input(&1, :key)) == - ~s() - - assert conn_form(&date_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == - ~s() - - assert conn_form( - &date_input(&1, :key, value: ~D[2017-09-21], id: "key", name: "search[key][]") - ) == ~s() - end - - ## datetime_input/3 - - test "datetime_local_input/3" do - assert safe_to_string(datetime_local_input(:search, :key)) == - ~s() - - assert conn_form(&datetime_local_input(&1, :naive_datetime)) == - ~s() - - assert safe_to_string( - datetime_local_input(:search, :key, value: "foo", id: "key", name: "search[key][]") - ) == ~s() - - assert safe_to_string( - datetime_local_input( - :search, - :key, - value: ~N[2017-09-21 20:21:53], - id: "key", - name: "search[key][]" - ) - ) == - ~s() - end - - test "datetime_local_input/3 with %DateTime{}" do - assert safe_to_string( - datetime_local_input( - :search, - :key, - value: DateTime.from_naive!(~N[2021-05-13 04:20:20.836851], "Etc/UTC"), - id: "key", - name: "search[key][]" - ) - ) == - ~s() - end - - test "datetime_local_input/3 with form" do - assert conn_form(&datetime_local_input(&1, :key)) == - ~s() - - assert conn_form( - &datetime_local_input(&1, :key, value: "foo", id: "key", name: "search[key][]") - ) == ~s() - - assert conn_form( - &datetime_local_input( - &1, - :key, - value: ~N[2017-09-21 20:21:53], - id: "key", - name: "search[key][]" - ) - ) == - ~s() - end - - ## time_input/3 - - test "time_input/3" do - assert safe_to_string(time_input(:search, :key)) == - ~s() - - assert safe_to_string( - time_input(:search, :key, value: "foo", id: "key", name: "search[key][]") - ) == ~s() - - assert safe_to_string( - time_input(:search, :key, value: ~T[23:00:07.001], id: "key", name: "search[key][]") - ) == ~s() - end - - test "time_input/3 with form" do - assert conn_form(&time_input(&1, :key)) == - ~s() - - assert conn_form(&time_input(&1, :time)) == - ~s() - - assert conn_form(&time_input(&1, :time, precision: :second)) == - ~s() - - assert conn_form(&time_input(&1, :time, precision: :millisecond)) == - ~s() - - assert conn_form(&time_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == - ~s() - - assert conn_form( - &time_input(&1, :key, value: ~T[23:00:07.001], id: "key", name: "search[key][]") - ) == ~s() - end - - ## submit/2 - - test "submit/2" do - assert safe_to_string(submit("Submit")) == ~s() - - assert safe_to_string(submit("Submit", class: "btn")) == - ~s() - - assert safe_to_string(submit([class: "btn"], do: "Submit")) == - ~s() - - assert safe_to_string(submit(do: "Submit")) == ~s() - - assert safe_to_string(submit("")) == ~s() - - assert safe_to_string(submit("", class: "btn")) == - ~s() - - assert safe_to_string(submit([class: "btn"], do: "")) == - ~s() - - assert safe_to_string(submit(do: "")) == - ~s() - end - - ## reset/2 - - test "reset/2" do - assert safe_to_string(reset("Reset")) == ~s() - - assert safe_to_string(reset("Reset", class: "btn")) == - ~s() - end - - ## radio_button/4 - - test "radio_button/4" do - assert safe_to_string(radio_button(:search, :key, "admin")) == - ~s() - - assert safe_to_string(radio_button(:search, :key, "admin", checked: true)) == - ~s() - - assert safe_to_string(radio_button(:search, :key, "value with spaces")) == - ~s() - - assert safe_to_string(radio_button(:search, :key, "F✓o]o%b+a'R")) == - ~s() - end - - test "radio_button/4 with form" do - assert conn_form(&radio_button(&1, :key, :admin)) == - ~s() - - assert conn_form(&radio_button(&1, :key, :value)) == - ~s() - - assert conn_form(&radio_button(&1, :key, :value, checked: false)) == - ~s() - end - - ## checkbox/3 - - test "checkbox/3" do - assert safe_to_string(checkbox(:search, :key)) == - ~s() <> - ~s() - - assert safe_to_string(checkbox(:search, :key, value: "true")) == - ~s() <> - ~s() - - assert safe_to_string(checkbox(:search, :key, checked: true)) == - ~s() <> - ~s() - - assert safe_to_string(checkbox(:search, :key, checked: true, disabled: true)) == - ~s() <> - ~s() - - assert safe_to_string(checkbox(:search, :key, value: "true", checked: false)) == - ~s() <> - ~s() - - assert safe_to_string(checkbox(:search, :key, value: 0, checked_value: 1, unchecked_value: 0)) == - ~s() <> - ~s() - - assert safe_to_string(checkbox(:search, :key, value: 1, checked_value: 1, unchecked_value: 0)) == - ~s() <> - ~s() - - assert safe_to_string(checkbox(:search, :key, value: 1, hidden_input: false)) == - ~s() - - # Mimick a field of type {:array, Ecto.Enum}, for which `field_value` returns an array of atoms: - assert safe_to_string( - checkbox(:search, :key, - name: "search[key][]", - value: [:a, :b], - checked_value: "c", - checked: false, - hidden_input: false - ) - ) == - ~s() - end - - test "checkbox/3 with form" do - assert conn_form(&checkbox(&1, :key)) == - ~s() <> - ~s() - - assert conn_form(&checkbox(&1, :key, value: true)) == - ~s() <> - ~s() - - assert conn_form(&checkbox(&1, :key, checked_value: :value, unchecked_value: :novalue)) == - ~s() <> - ~s() - end - - # select/4 - - test "select/4" do - assert safe_to_string(select(:search, :key, ~w(foo bar))) == - ~s() - - assert safe_to_string(select(:search, :key, Foo: "foo", Bar: "bar")) == - ~s() - - assert safe_to_string( - select(:search, :key, [ - [key: "Foo", value: "foo"], - [key: "Bar", value: "bar", disabled: true] - ]) - ) == - ~s() - - assert safe_to_string( - select(:search, :key, [Foo: "foo", Bar: "bar"], prompt: "Choose your destiny") - ) == - ~s() - - assert safe_to_string( - select(:search, :key, [Foo: "foo", Bar: "bar"], - prompt: [key: "Choose your destiny", disabled: true] - ) - ) == - ~s() - - assert_raise ArgumentError, fn -> - select(:search, :key, [Foo: "foo", Bar: "bar"], prompt: []) - end - - assert safe_to_string(select(:search, :key, ~w(foo bar), value: "foo")) =~ - ~s() - - assert safe_to_string(select(:search, :key, ~w(foo bar), selected: "foo")) =~ - ~s() - end - - test "select/4 with form" do - assert conn_form(&select(&1, :key, ~w(value novalue), selected: "novalue")) == - ~s() - - assert conn_form(&select(&1, :other, ~w(value novalue), selected: "novalue")) == - ~s() - - assert conn_form( - &select( - &1, - :key, - [ - [value: "value", key: "Value", disabled: true], - [value: "novalue", key: "No Value"] - ], - selected: "novalue" - ) - ) == - ~s() - - assert conn_form( - &select( - put_in(&1.data[:other], "value"), - :other, - ~w(value novalue), - selected: "novalue" - ) - ) == - ~s() - - assert conn_form(&select(&1, :key, ~w(value novalue), value: "novalue")) == - ~s() - end - - test "select/4 with groups" do - assert conn_form( - &select(&1, :key, [{"foo", ~w(bar baz)}, {"qux", ~w(qux quz)}], value: "qux") - ) == - ~s() - - assert conn_form( - &select( - &1, - :key, - [foo: [{"1", "One"}, {"2", "Two"}], qux: ~w(qux quz)], - value: "qux" - ) - ) == - ~s() - - assert conn_form( - &select( - &1, - :key, - %{"foo" => %{"1" => "One", "2" => "Two"}, "qux" => ~w(qux quz)}, - value: "qux" - ) - ) == - ~s() - - assert conn_form( - &select( - &1, - :key, - %{"foo" => [{"1", "One"}, {"2", "Two"}], "qux" => ~w(qux quz)}, - value: "qux" - ) - ) == - ~s() - end - - # multiple_select/4 - - test "multiple_select/4" do - assert safe_to_string(multiple_select(:search, :key, ~w(foo bar))) == - ~s() - - assert safe_to_string(multiple_select(:search, :key, [{"foo", 1}, {"bar", 2}])) == - ~s() - - assert safe_to_string(multiple_select(:search, :key, ~w(foo bar), value: ["foo"])) =~ - ~s() - - assert safe_to_string( - multiple_select(:search, :key, [{"foo", "1"}, {"bar", "2"}], value: [1]) - ) =~ ~s() - - assert safe_to_string(multiple_select(:search, :key, [{"foo", 1}, {"bar", 2}], selected: [1])) =~ - ~s() - - assert safe_to_string( - multiple_select(:search, :key, %{"foo" => [{"One", 1}, {"Two", 2}], "bar" => ~w(3 4)}) - ) == - ~s() - end - - test "multiple_select/4 with form" do - assert conn_form( - &multiple_select(&1, :key, [{"foo", 1}, {"bar", 2}], value: [1], selected: [2]) - ) == - ~s() - - assert conn_form(&multiple_select(&1, :other, [{"foo", 1}, {"bar", 2}], selected: [2])) == - ~s() - - assert conn_form(&multiple_select(&1, :key, [{"foo", 1}, {"bar", 2}], value: [2])) == - ~s() - - assert conn_form(&multiple_select(&1, :key, ~w(value novalue), value: ["novalue"])) == - ~s() - - assert conn_form( - &multiple_select( - put_in(&1.params["key"], ["3"]), - :key, - [{"foo", 1}, {"bar", 2}, {"goo", 3}], - selected: [2] - ) - ) == - ~s() - end - - test "multiple_select/4 with unnamed form" do - assert conn_form( - &multiple_select(&1, :key, [{"foo", 1}, {"bar", 2}], value: [1], selected: [2]), - [] - ) == - ~s() - - assert conn_form(&multiple_select(&1, :other, [{"foo", 1}, {"bar", 2}], selected: [2]), []) == - ~s() - - assert conn_form(&multiple_select(&1, :key, [{"foo", 1}, {"bar", 2}], value: [2]), []) == - ~s() - - assert conn_form(&multiple_select(&1, :key, ~w(value novalue), value: ["novalue"]), []) == - ~s() - - assert conn_form( - &multiple_select( - put_in(&1.params["key"], ["3"]), - :key, - [{"foo", 1}, {"bar", 2}, {"goo", 3}], - selected: [2] - ), - [] - ) == - ~s() - end - - # options_for_select/2 - - test "options_for_select/2" do - assert options_for_select(~w(value novalue), "novalue") |> safe_to_string() == - ~s() <> - ~s() - - assert options_for_select(~w(value novalue), "novalue") |> safe_to_string() == - ~s() <> - ~s() - - assert options_for_select( - [ - [value: "value", key: "Value", disabled: true], - [value: "novalue", key: "No Value"] - ], - "novalue" - ) - |> safe_to_string() == - ~s() <> - ~s() - - assert options_for_select(~w(value novalue), ["value", "novalue"]) |> safe_to_string() == - ~s() <> - ~s() - end - - test "options_for_select/2 with groups" do - assert options_for_select([{"foo", ~w(bar baz)}, {"qux", ~w(qux quz)}], "qux") - |> safe_to_string() == - ~s() <> - ~s() <> - ~s() <> - ~s() <> - ~s() <> - ~s() <> - ~s() <> ~s() - - assert options_for_select([{"foo", ~w(bar baz)}, {"qux", ~w(qux quz)}], ["baz", "qux"]) - |> safe_to_string() == - ~s() <> - ~s() <> - ~s() <> - ~s() <> - ~s() <> - ~s() <> - ~s() <> ~s() - end - - # date_select/4 - - test "date_select/4" do - content = safe_to_string(date_select(:search, :datetime)) - assert content =~ ~s() - assert content =~ ~s() <> - ~s() - - assert content =~ - ~s() <> - ~s() - end - - test "date_select/4 with form" do - content = conn_form(&date_select(&1, :datetime, default: {2020, 10, 13})) - assert content =~ ~s() - assert content =~ ~s() - assert content =~ ~s() - - content = safe_to_string(time_select(:search, :datetime, second: [])) - assert content =~ ~s() - assert content =~ ~s() <> - ~s() - - assert content =~ - ~s() <> - ~s() - end - - test "time_select/4 with form" do - content = conn_form(&time_select(&1, :datetime, default: {1, 2, 3}, second: [])) - assert content =~ ~s() - assert content =~ ~s() - assert content =~ ~s() - assert content =~ ~s() - refute content =~ ~s() - assert content =~ ~s() - assert content =~ ~s() - assert content =~ ~s() - assert content =~ ~s() - assert content =~ ~s() - assert content =~ ~s() - assert content =~ ~s() - - assert content =~ ~s() - assert content =~ ~s() - assert content =~ ~s(Month: ) - assert content =~ ~s(Hour: ) - assert content =~ ~s(Sec: ) - end - - test "one: inputs_for/4 does not generate index" do - safe_inputs_for(:unknown, fn f -> - refute f.index - "ok" - end) - end - - test "one: inputs_for/4 without default and field is present" do - contents = - safe_inputs_for(:date, fn f -> - text_input(f, :year) - end) - - assert contents == - ~s() - end - - test "one: inputs_for/4 with default and field is not present" do - contents = - safe_inputs_for(:unknown, [default: %{year: 2015}], fn f -> - text_input(f, :year) - end) - - assert contents == - ~s() - end - - test "one: inputs_for/4 with default and field is present" do - contents = - safe_inputs_for(:date, [default: %{year: 2015}], fn f -> - text_input(f, :year) - end) - - assert contents == - ~s() - end - - test "one: inputs_for/4 with custom name and id" do - contents = - safe_inputs_for(:date, [as: :foo, id: :bar], fn f -> - text_input(f, :year) - end) - - assert contents == ~s() - end - - ## Cardinality many - - test "many: inputs_for/4 with file field generates file input" do - contents = - safe_inputs_for(:unknown, [default: [%{}, %{}], multipart: true], fn f -> - assert f.index in [0, 1] - file_input(f, :file) - end) - - assert contents == - ~s() <> - ~s() - end - - test "many: inputs_for/4 with default and field is not present" do - contents = - safe_inputs_for(:unknown, [default: [%{year: 2012}, %{year: 2018}]], fn f -> - assert f.index in [0, 1] - text_input(f, :year) - end) - - assert contents == - ~s() <> - ~s() - end - - test "many: inputs_for/4 generates indexes" do - safe_inputs_for(:unknown, [default: [%{year: 2012}]], fn f -> - assert f.index == 0 - "ok" - end) - - safe_inputs_for(:unknown, [default: [%{year: 2012}, %{year: 2018}]], fn f -> - assert f.index in [0, 1] - "ok" - end) - end - - test "many: inputs_for/4 with default and field is present" do - contents = - safe_inputs_for(:dates, [default: [%{year: 2012}, %{year: 2018}]], fn f -> - text_input(f, :year) - end) - - assert contents == - ~s() <> - ~s() - end - - test "many: inputs_for/4 with name and id" do - contents = - safe_inputs_for( - :dates, - [default: [%{year: 2012}, %{year: 2018}], as: :foo, id: :bar], - fn f -> - text_input(f, :year) - end - ) - - assert contents == - ~s() <> - ~s() - end - - @prepend_append [ - prepend: [%{year: 2008}], - append: [%{year: 2022}], - default: [%{year: 2012}, %{year: 2018}] - ] - - test "many: inputs_for/4 with prepend/append and field is not present" do - contents = - safe_inputs_for(:unknown, @prepend_append, fn f -> - text_input(f, :year) - end) - - assert contents == - ~s() <> - ~s() <> - ~s() <> - ~s() - end - - test "many: inputs_for/4 with prepend/append and field is present" do - contents = - safe_inputs_for(:dates, @prepend_append, fn f -> - text_input(f, :year) - end) - - assert contents == - ~s() <> - ~s() - end -end diff --git a/test/phoenix_html/link_test.exs b/test/phoenix_html/link_test.exs deleted file mode 100644 index 481e12b..0000000 --- a/test/phoenix_html/link_test.exs +++ /dev/null @@ -1,149 +0,0 @@ -defmodule Phoenix.HTML.LinkTest do - use ExUnit.Case, async: true - - import Phoenix.HTML - import Phoenix.HTML.Link - - test "link with post" do - csrf_token = Plug.CSRFProtection.get_csrf_token() - - assert safe_to_string(link("hello", to: "/world", method: :post)) == - ~s[hello] - end - - test "link with %URI{}" do - url = "https://elixir-lang.org/" - - assert safe_to_string(link("elixir", to: url)) == - safe_to_string(link("elixir", to: URI.parse(url))) - - path = "/elixir" - - assert safe_to_string(link("elixir", to: path)) == - safe_to_string(link("elixir", to: URI.parse(path))) - end - - test "link with put/delete" do - csrf_token = Plug.CSRFProtection.get_csrf_token() - - assert safe_to_string(link("hello", to: "/world", method: :put)) == - ~s[hello] - end - - test "link with put/delete without csrf_token" do - assert safe_to_string(link("hello", to: "/world", method: :put, csrf_token: false)) == - ~s[hello] - end - - test "link with :do contents" do - assert ~s[

world

] == - safe_to_string( - link to: "/hello" do - Phoenix.HTML.Tag.content_tag(:p, "world") - end - ) - - assert safe_to_string( - link(to: "/hello") do - "world" - end - ) == ~s[world] - end - - test "link with scheme" do - assert safe_to_string(link("foo", to: "/javascript:alert(<1>)")) == - ~s[foo] - - assert safe_to_string(link("foo", to: {:safe, "/javascript:alert(<1>)"})) == - ~s[foo] - - assert safe_to_string(link("foo", to: {:javascript, "alert(<1>)"})) == - ~s[foo] - - assert safe_to_string(link("foo", to: {:javascript, ~c"alert(<1>)"})) == - ~s[foo] - - assert safe_to_string(link("foo", to: {:javascript, {:safe, "alert(<1>)"}})) == - ~s[foo] - - assert safe_to_string(link("foo", to: {:javascript, {:safe, ~c"alert(<1>)"}})) == - ~s[foo] - end - - test "link with invalid args" do - msg = "expected non-nil value for :to in link/2" - - assert_raise ArgumentError, msg, fn -> - link("foo", bar: "baz") - end - - msg = "link/2 requires a keyword list as second argument" - - assert_raise ArgumentError, msg, fn -> - link("foo", "/login") - end - - assert_raise ArgumentError, ~r"unsupported scheme given as link", fn -> - link("foo", to: "javascript:alert(1)") - end - - assert_raise ArgumentError, ~r"unsupported scheme given as link", fn -> - link("foo", to: {:safe, "javascript:alert(1)"}) - end - - assert_raise ArgumentError, ~r"unsupported scheme given as link", fn -> - link("foo", to: {:safe, ~c"javascript:alert(1)"}) - end - end - - test "button with post (default)" do - csrf_token = Plug.CSRFProtection.get_csrf_token() - - assert safe_to_string(button("hello", to: "/world")) == - ~s[] - end - - test "button with %URI{}" do - url = "https://elixir-lang.org/" - - assert safe_to_string(button("elixir", to: url, csrf_token: false)) == - safe_to_string(button("elixir", to: URI.parse(url), csrf_token: false)) - end - - test "button with post without csrf_token" do - assert safe_to_string(button("hello", to: "/world", csrf_token: false)) == - ~s[] - end - - test "button with get does not generate CSRF" do - assert safe_to_string(button("hello", to: "/world", method: :get)) == - ~s[] - end - - test "button with do" do - csrf_token = Plug.CSRFProtection.get_csrf_token() - - output = - safe_to_string( - button to: "/world", class: "small" do - raw("Hi") - end - ) - - assert output == - ~s[] - end - - test "button with class overrides default" do - csrf_token = Plug.CSRFProtection.get_csrf_token() - - assert safe_to_string(button("hello", to: "/world", class: "btn rounded", id: "btn")) == - ~s[] - end - - test "button with invalid args" do - assert_raise ArgumentError, ~r/unsupported scheme given as link/, fn -> - button("foo", to: "javascript:alert(1)", method: :get) - end - end -end diff --git a/test/phoenix_html/tag_test.exs b/test/phoenix_html/tag_test.exs deleted file mode 100644 index 7b4a623..0000000 --- a/test/phoenix_html/tag_test.exs +++ /dev/null @@ -1,209 +0,0 @@ -defmodule Phoenix.HTML.TagTest do - use ExUnit.Case, async: true - - import Phoenix.HTML - import Phoenix.HTML.Tag, except: [attributes_escape: 1] - doctest Phoenix.HTML.Tag - - test "tag" do - assert tag(:br) |> safe_to_string() == ~s(
) - - assert tag(:input, name: ~s("<3")) |> safe_to_string() == ~s() - assert tag(:input, name: raw("<3")) |> safe_to_string() == ~s() - assert tag(:input, name: ["foo", raw("b safe_to_string() == ~s() - assert tag(:input, name: :hello) |> safe_to_string() == ~s() - - assert tag(:input, type: "text", name: "user_id") |> safe_to_string() == - ~s() - - assert tag(:input, data: [toggle: "dropdown"]) |> safe_to_string() == - ~s() - - assert tag(:input, my_attr: "blah") |> safe_to_string() == ~s() - - assert tag(:input, [{"my_<_attr", "blah"}]) |> safe_to_string() == - ~s() - - assert tag(:input, [{{:safe, "my_<_attr"}, "blah"}]) |> safe_to_string() == - ~s() - - assert tag(:input, data: [my_attr: "blah"]) |> safe_to_string() == - ~s() - - assert tag(:input, data: [toggle: [attr: "blah", target: "#parent"]]) |> safe_to_string() == - ~s() - - assert tag(:audio, autoplay: "autoplay") |> safe_to_string() == - ~s(