├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── phoenix_html_helpers.ex └── phoenix_html_helpers │ ├── form.ex │ ├── form_data.ex │ ├── format.ex │ ├── link.ex │ └── tag.ex ├── mix.exs ├── mix.lock └── test ├── phoenix_html_helpers ├── csrf_test.exs ├── form_test.exs ├── format_test.exs ├── inputs_for_test.exs ├── link_test.exs └── tag_test.exs ├── phoenix_html_helpers_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | tests: 11 | name: Run tests (Elixir ${{matrix.elixir}}, OTP ${{matrix.otp}}) 12 | 13 | strategy: 14 | matrix: 15 | include: 16 | - elixir: 1.7 17 | otp: 21.3 18 | - elixir: 1.14 19 | otp: 25.3 20 | lint: lint 21 | 22 | runs-on: ubuntu-20.04 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v2 27 | 28 | - name: Set up Elixir 29 | uses: erlef/setup-elixir@v1 30 | with: 31 | elixir-version: ${{ matrix.elixir }} 32 | otp-version: ${{ matrix.otp }} 33 | 34 | - name: Restore deps and _build cache 35 | uses: actions/cache@v2 36 | with: 37 | path: | 38 | deps 39 | _build 40 | key: deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} 41 | restore-keys: | 42 | deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }} 43 | 44 | - name: Install dependencies 45 | run: mix deps.get --only test 46 | 47 | - name: Check source code format 48 | run: mix format --check-formatted 49 | if: ${{ matrix.lint }} 50 | 51 | - name: Remove compiled application files 52 | run: mix clean 53 | 54 | - name: Compile dependencies 55 | run: mix compile 56 | if: ${{ !matrix.lint }} 57 | env: 58 | MIX_ENV: test 59 | 60 | - name: Compile & lint dependencies 61 | run: mix compile --warnings-as-errors 62 | if: ${{ matrix.lint }} 63 | env: 64 | MIX_ENV: test 65 | 66 | - name: Run tests 67 | run: mix test 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | phoenix_html_helpers-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Chris McCord 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhoenixHTMLHelpers 2 | 3 | [![Build Status](https://github.com/phoenixframework/phoenix_html_helpers/workflows/Tests/badge.svg)](https://github.com/phoenixframework/phoenix_html_helpers/actions?query=workflow%3ATests) 4 | 5 | Collection of helpers to generate and manipulate HTML contents. 6 | These helpers were used in Phoenix v1.6 and earlier versions, 7 | before the introduction of `Phoenix.Component`. 8 | 9 | To maintain compatibility, replace `use Phoenix.HTML` in your applications by: 10 | 11 | ```elixir 12 | import Phoenix.HTML 13 | import Phoenix.HTML.Form 14 | use PhoenixHTMLHelpers 15 | ``` 16 | 17 | See the [docs](https://hexdocs.pm/phoenix_html_helpers/) for more information. 18 | 19 | This library is maintained for compatibility, but does not accept new features. 20 | 21 | ## License 22 | 23 | Copyright (c) 2014 Chris McCord 24 | 25 | Permission is hereby granted, free of charge, to any person obtaining 26 | a copy of this software and associated documentation files (the 27 | "Software"), to deal in the Software without restriction, including 28 | without limitation the rights to use, copy, modify, merge, publish, 29 | distribute, sublicense, and/or sell copies of the Software, and to 30 | permit persons to whom the Software is furnished to do so, subject to 31 | the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be 34 | included in all copies or substantial portions of the Software. 35 | 36 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 37 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 38 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 39 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 40 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 41 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 42 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 43 | -------------------------------------------------------------------------------- /lib/phoenix_html_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHTMLHelpers do 2 | @moduledoc """ 3 | Collection of helpers to generate and manipulate HTML contents. 4 | 5 | These helpers were used in Phoenix v1.6 and earlier versions, 6 | before the introduction of `Phoenix.Component`. 7 | 8 | Replace `use Phoenix.HTML` in your applications by: 9 | 10 | ```elixir 11 | import Phoenix.HTML 12 | import Phoenix.HTML.Form 13 | use PhoenixHTMLHelpers 14 | ``` 15 | 16 | To preserve backwards compatibility. 17 | """ 18 | 19 | @doc false 20 | defmacro __using__(_) do 21 | quote do 22 | import PhoenixHTMLHelpers.Form 23 | import PhoenixHTMLHelpers.Link 24 | import PhoenixHTMLHelpers.Tag 25 | import PhoenixHTMLHelpers.Format 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/phoenix_html_helpers/form.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHTMLHelpers.Form do 2 | @moduledoc ~S""" 3 | Functions for generating forms and inputs HTML. 4 | """ 5 | 6 | alias Phoenix.HTML.Form 7 | import Phoenix.HTML 8 | import Phoenix.HTML.Form 9 | import PhoenixHTMLHelpers.Tag 10 | 11 | @doc """ 12 | Converts an attribute/form field into its humanize version. 13 | 14 | iex> humanize(:username) 15 | "Username" 16 | iex> humanize(:created_at) 17 | "Created at" 18 | iex> humanize("user_id") 19 | "User" 20 | 21 | """ 22 | def humanize(atom) when is_atom(atom), do: humanize(Atom.to_string(atom)) 23 | 24 | def humanize(bin) when is_binary(bin) do 25 | bin = 26 | if String.ends_with?(bin, "_id") do 27 | binary_part(bin, 0, byte_size(bin) - 3) 28 | else 29 | bin 30 | end 31 | 32 | bin |> String.replace("_", " ") |> :string.titlecase() 33 | end 34 | 35 | @doc """ 36 | Generates a form tag with a form builder and an anonymous function. 37 | 38 | <%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %> 39 | Name: <%= text_input f, :name %> 40 | <% end %> 41 | 42 | Forms may be used in two distinct scenarios: 43 | 44 | * with changeset data - when information to populate 45 | the form comes from an `Ecto.Changeset` changeset. 46 | The changeset holds rich information, which helps 47 | provide conveniences 48 | 49 | * with map data - a simple map of parameters (such as 50 | `Plug.Conn.params` can be given as data to the form) 51 | 52 | We will explore all them below. 53 | 54 | Note that if you are using HEEx templates, `form_for/4` is no longer 55 | the preferred way to generate a form tag, and you should use 56 | [`Phoenix.Component.form/1`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#form/1) 57 | instead. 58 | 59 | ## With changeset data 60 | 61 | The entry point for defining forms in Phoenix is with 62 | the `form_for/4` function. For this example, we will 63 | use `Ecto.Changeset`, which integrates nicely with Phoenix 64 | forms via the `phoenix_ecto` package. 65 | 66 | Imagine you have the following action in your controller: 67 | 68 | def new(conn, _params) do 69 | changeset = User.changeset(%User{}) 70 | render conn, "new.html", changeset: changeset 71 | end 72 | 73 | where `User.changeset/2` is defined as follows: 74 | 75 | def changeset(user, params \\ %{}) do 76 | Ecto.Changeset.cast(user, params, [:name, :age]) 77 | end 78 | 79 | Now a `@changeset` assign is available in views which we 80 | can pass to the form: 81 | 82 | <%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %> 83 | 86 | 87 | 90 | 91 | <%= submit "Submit" %> 92 | <% end %> 93 | 94 | `form_for/4` receives the `Ecto.Changeset` and converts it 95 | to a form, which is passed to the function as the argument 96 | `f`. All the remaining functions in this module receive 97 | the form and automatically generate the input fields, often 98 | by extracting information from the given changeset. For example, 99 | if the user had a default value for age set, it will 100 | automatically show up as selected in the form. 101 | 102 | ### A note on `:errors` 103 | 104 | Even if `changeset.errors` is non-empty, errors will not be displayed in a 105 | form if [the changeset 106 | `:action`](https://hexdocs.pm/ecto/Ecto.Changeset.html#module-changeset-actions) 107 | is `nil` or `:ignore`. 108 | 109 | This is useful for things like validation hints on form fields, e.g. an empty 110 | changeset for a new form. That changeset isn't valid, but we don't want to 111 | show errors until an actual user action has been performed. 112 | 113 | For example, if the user submits and a `Repo.insert/1` is called and fails on 114 | changeset validation, the action will be set to `:insert` to show that an 115 | insert was attempted, and the presence of that action will cause errors to be 116 | displayed. The same is true for Repo.update/delete. 117 | 118 | If you want to show errors manually you can also set the action yourself, 119 | either directly on the `Ecto.Changeset` struct field or by using 120 | `Ecto.Changeset.apply_action/2`. Since the action can be arbitrary, you can 121 | set it to `:validate` or anything else to avoid giving the impression that a 122 | database operation has actually been attempted. 123 | 124 | ## With map data 125 | 126 | `form_for/4` expects as first argument any data structure that 127 | implements the `Phoenix.HTML.FormData` protocol. By default, 128 | Phoenix.HTML implements this protocol for `Map`. 129 | 130 | This is useful when you are creating forms that are not backed 131 | by any kind of data layer. Let's assume that we're submitting a 132 | form to the `:new` action in the `FooController`: 133 | 134 | <%= form_for @conn.params, Routes.foo_path(@conn, :new), fn f -> %> 135 | <%= text_input f, :contents %> 136 | <%= submit "Search" %> 137 | <% end %> 138 | 139 | Once the form is submitted, the form contents will be set directly 140 | as the parameters root, such as `conn.params["contents"]`. If you 141 | prefer, you can pass the `:as` option to configure them to be nested: 142 | 143 | <%= form_for @conn.params["search"] || %{}, Routes.foo_path(@conn, :new), [as: :search], fn f -> %> 144 | <%= text_input f, :contents %> 145 | <%= submit "Search" %> 146 | <% end %> 147 | 148 | In the example above, all form contents are now set inside `conn.params["search"]` 149 | thanks to the `[as: :search]` option. 150 | 151 | ## Nested inputs 152 | 153 | If your data layer supports embedding or nested associations, 154 | you can use `inputs_for` to attach nested data to the form. 155 | 156 | Imagine the following Ecto schemas: 157 | 158 | defmodule User do 159 | use Ecto.Schema 160 | 161 | schema "users" do 162 | field :name 163 | embeds_one :permalink, Permalink 164 | end 165 | 166 | def changeset(user \\ %User{}, params) do 167 | user 168 | |> Ecto.Changeset.cast(params, [:name]) 169 | |> Ecto.Changeset.cast_embed(:permalink) 170 | end 171 | end 172 | 173 | defmodule Permalink do 174 | use Ecto.Schema 175 | 176 | embedded_schema do 177 | field :url 178 | end 179 | end 180 | 181 | In the form, you can now do this: 182 | 183 | <%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %> 184 | <%= text_input f, :name %> 185 | 186 | <%= inputs_for f, :permalink, fn fp -> %> 187 | <%= text_input fp, :url %> 188 | <% end %> 189 | <% end %> 190 | 191 | The default option can be given to populate the fields if none 192 | is given: 193 | 194 | <%= inputs_for f, :permalink, [default: %Permalink{title: "default"}], fn fp -> %> 195 | <%= text_input fp, :url %> 196 | <% end %> 197 | 198 | `inputs_for/4` can be used to work with single entities or 199 | collections. When working with collections, `:prepend` and 200 | `:append` can be used to add entries to the collection 201 | stored in the changeset. 202 | 203 | ## CSRF protection 204 | 205 | CSRF protection is a mechanism to ensure that the user who rendered 206 | the form is the one actually submitting it. This module generates a 207 | CSRF token by default. Your application should check this token on 208 | the server to prevent attackers from making requests on your server on 209 | behalf of other users. Phoenix checks this token by default. 210 | 211 | When posting a form with a host in its address, such as "//host.com/path" 212 | instead of only "/path", Phoenix will include the host signature in the 213 | token, and will only validate the token if the accessed host is the same as 214 | the host in the token. This is to avoid tokens from leaking to third-party 215 | applications. If this behaviour is problematic, you can generate a 216 | non-host-specific token with `Plug.CSRFProtection.get_csrf_token/0` and 217 | pass it to the form generator via the `:csrf_token` option. 218 | 219 | ## Options 220 | 221 | * `:as` - the server side parameter in which all params for this 222 | form will be collected (i.e. `as: :user_params` would mean all fields 223 | for this form will be accessed as `conn.params.user_params` server 224 | side). Automatically inflected when a changeset is given. 225 | 226 | * `:method` - the HTTP method. If the method is not "get" nor "post", 227 | an input tag with name `_method` is generated along-side the form tag. 228 | Defaults to "post". 229 | 230 | * `:multipart` - when true, sets enctype to "multipart/form-data". 231 | Required when uploading files. 232 | 233 | * `:csrf_token` - for "post" requests, the form tag will automatically 234 | include an input tag with name `_csrf_token`. When set to false, this 235 | is disabled. 236 | 237 | * `:errors` - use this to manually pass a keyword list of errors to the form 238 | (for example from `conn.assigns[:errors]`). This option is only used when a 239 | connection is used as the form source and it will make the errors available 240 | under `f.errors`. 241 | 242 | * `:id` - the ID of the form attribute. If an ID is given, all form inputs 243 | will also be prefixed by the given ID. 244 | 245 | All other options will be passed as HTML attributes, such as `class: "foo"`. 246 | """ 247 | @spec form_for(Phoenix.HTML.FormData.t(), String.t(), (Form.t() -> Phoenix.HTML.unsafe())) :: 248 | Phoenix.HTML.safe() 249 | @spec form_for( 250 | Phoenix.HTML.FormData.t(), 251 | String.t(), 252 | Keyword.t(), 253 | (Form.t() -> Phoenix.HTML.unsafe()) 254 | ) :: 255 | Phoenix.HTML.safe() 256 | def form_for(form_data, action, options \\ [], fun) when is_function(fun, 1) do 257 | form = Phoenix.HTML.FormData.to_form(form_data, options) 258 | html_escape([form_tag(action, form.options), fun.(form), raw("")]) 259 | end 260 | 261 | @doc """ 262 | Generate a new form builder for the given parameter in form. 263 | 264 | See `form_for/4` for examples of using this function. 265 | 266 | ## Options 267 | 268 | * `:id` - the id to be used in the form, defaults to the 269 | concatenation of the given `field` to the parent form id 270 | 271 | * `:as` - the name to be used in the form, defaults to the 272 | concatenation of the given `field` to the parent form name 273 | 274 | * `:default` - the value to use if none is available 275 | 276 | * `:prepend` - the values to prepend when rendering. This only 277 | applies if the field value is a list and no parameters were 278 | sent through the form. 279 | 280 | * `:append` - the values to append when rendering. This only 281 | applies if the field value is a list and no parameters were 282 | sent through the form. 283 | 284 | * `:skip_hidden` - skip the automatic rendering of hidden 285 | fields to allow for more tight control over the generated 286 | markup. You can access `form.hidden` to generate them manually 287 | within the supplied callback. 288 | 289 | """ 290 | @spec inputs_for(Form.t(), Form.field(), (Form.t() -> Phoenix.HTML.unsafe())) :: 291 | Phoenix.HTML.safe() 292 | @spec inputs_for(Form.t(), Form.field(), Keyword.t(), (Form.t() -> Phoenix.HTML.unsafe())) :: 293 | Phoenix.HTML.safe() 294 | def inputs_for(%{impl: impl} = form, field, options \\ [], fun) 295 | when is_atom(field) or is_binary(field) do 296 | {skip, options} = Keyword.pop(options, :skip_hidden, false) 297 | 298 | options = 299 | form.options 300 | |> Keyword.take([:multipart]) 301 | |> Keyword.merge(options) 302 | 303 | forms = impl.to_form(form.source, form, field, options) 304 | 305 | html_escape( 306 | Enum.map(forms, fn form -> 307 | if skip do 308 | fun.(form) 309 | else 310 | [hidden_inputs_for(form), fun.(form)] 311 | end 312 | end) 313 | ) 314 | end 315 | 316 | @mapping %{ 317 | "url" => :url_input, 318 | "email" => :email_input, 319 | "search" => :search_input, 320 | "password" => :password_input 321 | } 322 | 323 | @doc """ 324 | Gets the input type for a given field. 325 | 326 | If the underlying input type is a `:text_field`, 327 | a mapping could be given to further inflect 328 | the input type based solely on the field name. 329 | The default mapping is: 330 | 331 | %{"url" => :url_input, 332 | "email" => :email_input, 333 | "search" => :search_input, 334 | "password" => :password_input} 335 | 336 | """ 337 | @spec input_type(Form.t(), Form.field()) :: atom 338 | def input_type(%{impl: impl, source: source} = form, field, mapping \\ @mapping) 339 | when is_atom(field) or is_binary(field) do 340 | type = 341 | if function_exported?(impl, :input_type, 3) do 342 | impl.input_type(source, form, field) 343 | else 344 | :text_input 345 | end 346 | 347 | if type == :text_input do 348 | field = field_to_string(field) 349 | 350 | Enum.find_value(mapping, type, fn {k, v} -> 351 | String.contains?(field, k) && v 352 | end) 353 | else 354 | type 355 | end 356 | end 357 | 358 | @doc """ 359 | Generates a text input. 360 | 361 | The form should either be a `Phoenix.HTML.Form` emitted 362 | by `form_for` or an atom. 363 | 364 | All given options are forwarded to the underlying input, 365 | default values are provided for id, name and value if 366 | possible. 367 | 368 | ## Examples 369 | 370 | # Assuming form contains a User schema 371 | text_input(form, :name) 372 | #=> 373 | 374 | text_input(:user, :name) 375 | #=> 376 | 377 | """ 378 | def text_input(form, field, opts \\ []) do 379 | generic_input(:text, form, field, opts) 380 | end 381 | 382 | @doc """ 383 | Generates a hidden input. 384 | 385 | See `text_input/3` for example and docs. 386 | """ 387 | def hidden_input(form, field, opts \\ []) do 388 | generic_input(:hidden, form, field, opts) 389 | end 390 | 391 | @doc """ 392 | Generates hidden inputs for the given form inputs. 393 | 394 | See `inputs_for/2` and `inputs_for/3`. 395 | """ 396 | @spec hidden_inputs_for(Form.t()) :: list(Phoenix.HTML.safe()) 397 | def hidden_inputs_for(form) do 398 | Enum.flat_map(form.hidden, fn {k, v} -> 399 | hidden_inputs_for(form, k, v) 400 | end) 401 | end 402 | 403 | defp hidden_inputs_for(form, k, values) when is_list(values) do 404 | id = input_id(form, k) 405 | name = input_name(form, k) 406 | 407 | for {v, index} <- Enum.with_index(values) do 408 | hidden_input(form, k, 409 | id: id <> "_" <> Integer.to_string(index), 410 | name: name <> "[]", 411 | value: v 412 | ) 413 | end 414 | end 415 | 416 | defp hidden_inputs_for(form, k, v) do 417 | [hidden_input(form, k, value: v)] 418 | end 419 | 420 | @doc """ 421 | Generates an email input. 422 | 423 | See `text_input/3` for example and docs. 424 | """ 425 | def email_input(form, field, opts \\ []) do 426 | generic_input(:email, form, field, opts) 427 | end 428 | 429 | @doc """ 430 | Generates a number input. 431 | 432 | See `text_input/3` for example and docs. 433 | """ 434 | def number_input(form, field, opts \\ []) do 435 | generic_input(:number, form, field, opts) 436 | end 437 | 438 | @doc """ 439 | Generates a password input. 440 | 441 | For security reasons, the form data and parameter values 442 | are never re-used in `password_input/3`. Pass the value 443 | explicitly if you would like to set one. 444 | 445 | See `text_input/3` for example and docs. 446 | """ 447 | def password_input(form, field, opts \\ []) do 448 | opts = 449 | opts 450 | |> Keyword.put_new(:type, "password") 451 | |> Keyword.put_new(:id, input_id(form, field)) 452 | |> Keyword.put_new(:name, input_name(form, field)) 453 | 454 | tag(:input, opts) 455 | end 456 | 457 | @doc """ 458 | Generates an url input. 459 | 460 | See `text_input/3` for example and docs. 461 | """ 462 | def url_input(form, field, opts \\ []) do 463 | generic_input(:url, form, field, opts) 464 | end 465 | 466 | @doc """ 467 | Generates a search input. 468 | 469 | See `text_input/3` for example and docs. 470 | """ 471 | def search_input(form, field, opts \\ []) do 472 | generic_input(:search, form, field, opts) 473 | end 474 | 475 | @doc """ 476 | Generates a telephone input. 477 | 478 | See `text_input/3` for example and docs. 479 | """ 480 | def telephone_input(form, field, opts \\ []) do 481 | generic_input(:tel, form, field, opts) 482 | end 483 | 484 | @doc """ 485 | Generates a color input. 486 | 487 | See `text_input/3` for example and docs. 488 | """ 489 | def color_input(form, field, opts \\ []) do 490 | generic_input(:color, form, field, opts) 491 | end 492 | 493 | @doc """ 494 | Generates a range input. 495 | 496 | See `text_input/3` for example and docs. 497 | """ 498 | def range_input(form, field, opts \\ []) do 499 | generic_input(:range, form, field, opts) 500 | end 501 | 502 | @doc """ 503 | Generates a date input. 504 | 505 | See `text_input/3` for example and docs. 506 | """ 507 | def date_input(form, field, opts \\ []) do 508 | generic_input(:date, form, field, opts) 509 | end 510 | 511 | @doc """ 512 | Generates a datetime-local input. 513 | 514 | See `text_input/3` for example and docs. 515 | """ 516 | def datetime_local_input(form, field, opts \\ []) do 517 | value = Keyword.get(opts, :value, input_value(form, field)) 518 | opts = Keyword.put(opts, :value, normalize_value("datetime-local", value)) 519 | 520 | generic_input(:"datetime-local", form, field, opts) 521 | end 522 | 523 | @doc """ 524 | Generates a time input. 525 | 526 | ## Options 527 | 528 | * `:precision` - Allowed values: `:minute`, `:second`, `:millisecond`. 529 | Defaults to `:minute`. 530 | 531 | All other options are forwarded. See `text_input/3` for example and docs. 532 | 533 | ## Examples 534 | 535 | time_input form, :time 536 | #=> 537 | 538 | time_input form, :time, precision: :second 539 | #=> 540 | 541 | time_input form, :time, precision: :millisecond 542 | #=> 543 | """ 544 | def time_input(form, field, opts \\ []) do 545 | {precision, opts} = Keyword.pop(opts, :precision, :minute) 546 | value = opts[:value] || input_value(form, field) 547 | opts = Keyword.put(opts, :value, truncate_time(value, precision)) 548 | 549 | generic_input(:time, form, field, opts) 550 | end 551 | 552 | defp truncate_time(%Time{} = time, :minute) do 553 | time 554 | |> Time.to_string() 555 | |> String.slice(0, 5) 556 | end 557 | 558 | defp truncate_time(%Time{} = time, precision) do 559 | time 560 | |> Time.truncate(precision) 561 | |> Time.to_string() 562 | end 563 | 564 | defp truncate_time(value, _), do: value 565 | 566 | defp generic_input(type, form, field, opts) 567 | when is_list(opts) and (is_atom(field) or is_binary(field)) do 568 | opts = 569 | opts 570 | |> Keyword.put_new(:type, type) 571 | |> Keyword.put_new(:id, input_id(form, field)) 572 | |> Keyword.put_new(:name, input_name(form, field)) 573 | |> Keyword.put_new(:value, input_value(form, field)) 574 | |> Keyword.update!(:value, &maybe_html_escape/1) 575 | 576 | tag(:input, opts) 577 | end 578 | 579 | defp maybe_html_escape(nil), do: nil 580 | defp maybe_html_escape(value), do: html_escape(value) 581 | 582 | @doc """ 583 | Generates a textarea input. 584 | 585 | All given options are forwarded to the underlying input, 586 | default values are provided for id, name and textarea 587 | content if possible. 588 | 589 | ## Examples 590 | 591 | # Assuming form contains a User schema 592 | textarea(form, :description) 593 | #=> 594 | 595 | ## New lines 596 | 597 | Notice the generated textarea includes a new line after 598 | the opening tag. This is because the HTML spec says new 599 | lines after tags must be ignored, and all major browser 600 | implementations do that. 601 | 602 | Therefore, in order to avoid new lines provided by the user 603 | from being ignored when the form is resubmitted, we 604 | automatically add a new line before the text area 605 | value. 606 | """ 607 | def textarea(form, field, opts \\ []) do 608 | opts = 609 | opts 610 | |> Keyword.put_new(:id, input_id(form, field)) 611 | |> Keyword.put_new(:name, input_name(form, field)) 612 | 613 | {value, opts} = Keyword.pop(opts, :value, input_value(form, field)) 614 | content_tag(:textarea, normalize_value("textarea", value), opts) 615 | end 616 | 617 | @doc """ 618 | Generates a file input. 619 | 620 | It requires the given form to be configured with `multipart: true` 621 | when invoking `form_for/4`, otherwise it fails with `ArgumentError`. 622 | 623 | See `text_input/3` for example and docs. 624 | """ 625 | def file_input(form, field, opts \\ []) do 626 | if match?(%Form{}, form) and !form.options[:multipart] do 627 | raise ArgumentError, 628 | "file_input/3 requires the enclosing form_for/4 " <> 629 | "to be configured with multipart: true" 630 | end 631 | 632 | opts = 633 | opts 634 | |> Keyword.put_new(:type, :file) 635 | |> Keyword.put_new(:id, input_id(form, field)) 636 | |> Keyword.put_new(:name, input_name(form, field)) 637 | 638 | opts = 639 | if opts[:multiple] do 640 | Keyword.update!(opts, :name, &"#{&1}[]") 641 | else 642 | opts 643 | end 644 | 645 | tag(:input, opts) 646 | end 647 | 648 | @doc """ 649 | Generates a submit button to send the form. 650 | 651 | ## Examples 652 | 653 | submit do: "Submit" 654 | #=> 655 | 656 | """ 657 | def submit([do: _] = block_option), do: submit([], block_option) 658 | 659 | @doc """ 660 | Generates a submit button to send the form. 661 | 662 | All options are forwarded to the underlying button tag. 663 | When called with a `do:` block, the button tag options 664 | come first. 665 | 666 | ## Examples 667 | 668 | submit "Submit" 669 | #=> 670 | 671 | submit "Submit", class: "btn" 672 | #=> 673 | 674 | submit [class: "btn"], do: "Submit" 675 | #=> 676 | 677 | """ 678 | def submit(value, opts \\ []) 679 | 680 | def submit(opts, [do: _] = block_option) do 681 | opts = Keyword.put_new(opts, :type, "submit") 682 | 683 | content_tag(:button, opts, block_option) 684 | end 685 | 686 | def submit(value, opts) do 687 | opts = Keyword.put_new(opts, :type, "submit") 688 | 689 | content_tag(:button, value, opts) 690 | end 691 | 692 | @doc """ 693 | Generates a reset input to reset all the form fields to 694 | their original state. 695 | 696 | All options are forwarded to the underlying input tag. 697 | 698 | ## Examples 699 | 700 | reset "Reset" 701 | #=> 702 | 703 | reset "Reset", class: "btn" 704 | #=> 705 | 706 | """ 707 | def reset(value, opts \\ []) do 708 | opts = 709 | opts 710 | |> Keyword.put_new(:type, "reset") 711 | |> Keyword.put_new(:value, value) 712 | 713 | tag(:input, opts) 714 | end 715 | 716 | @doc """ 717 | Generates a radio button. 718 | 719 | Invoke this function for each possible value you want 720 | to be sent to the server. 721 | 722 | ## Examples 723 | 724 | # Assuming form contains a User schema 725 | radio_button(form, :role, "admin") 726 | #=> 727 | 728 | ## Options 729 | 730 | All options are simply forwarded to the underlying HTML tag. 731 | """ 732 | def radio_button(form, field, value, opts \\ []) do 733 | escaped_value = html_escape(value) 734 | 735 | opts = 736 | opts 737 | |> Keyword.put_new(:type, "radio") 738 | |> Keyword.put_new(:id, input_id(form, field, escaped_value)) 739 | |> Keyword.put_new(:name, input_name(form, field)) 740 | 741 | opts = 742 | if escaped_value == html_escape(input_value(form, field)) do 743 | Keyword.put_new(opts, :checked, true) 744 | else 745 | opts 746 | end 747 | 748 | tag(:input, [value: escaped_value] ++ opts) 749 | end 750 | 751 | @doc """ 752 | Generates a checkbox. 753 | 754 | This function is useful for sending boolean values to the server. 755 | 756 | ## Examples 757 | 758 | # Assuming form contains a User schema 759 | checkbox(form, :famous) 760 | #=> 761 | #=> 762 | 763 | ## Options 764 | 765 | * `:checked_value` - the value to be sent when the checkbox is checked. 766 | Defaults to "true" 767 | 768 | * `:hidden_input` - controls if this function will generate a hidden input 769 | to submit the unchecked value or not. Defaults to "true" 770 | 771 | * `:unchecked_value` - the value to be sent when the checkbox is unchecked, 772 | Defaults to "false" 773 | 774 | * `:value` - the value used to check if a checkbox is checked or unchecked. 775 | The default value is extracted from the form data if available 776 | 777 | All other options are forwarded to the underlying HTML tag. 778 | 779 | ## Hidden fields 780 | 781 | Because an unchecked checkbox is not sent to the server, Phoenix 782 | automatically generates a hidden field with the unchecked_value 783 | *before* the checkbox field to ensure the `unchecked_value` is sent 784 | when the checkbox is not marked. Set `hidden_input` to false If you 785 | don't want to send values from unchecked checkbox to the server. 786 | """ 787 | def checkbox(form, field, opts \\ []) do 788 | opts = 789 | opts 790 | |> Keyword.put_new(:type, "checkbox") 791 | |> Keyword.put_new(:name, input_name(form, field)) 792 | 793 | {value, opts} = Keyword.pop(opts, :value, input_value(form, field)) 794 | {checked_value, opts} = Keyword.pop(opts, :checked_value, true) 795 | {unchecked_value, opts} = Keyword.pop(opts, :unchecked_value, false) 796 | {hidden_input, opts} = Keyword.pop(opts, :hidden_input, true) 797 | 798 | # We html escape all values to be sure we are comparing 799 | # apples to apples. After all, we may have true in the data 800 | # but "true" in the params and both need to match. 801 | checked_value = html_escape(checked_value) 802 | unchecked_value = html_escape(unchecked_value) 803 | 804 | opts = 805 | opts 806 | |> Keyword.put_new_lazy(:checked, fn -> 807 | value = html_escape(value) 808 | value == checked_value 809 | end) 810 | |> Keyword.put_new_lazy(:id, fn -> 811 | if String.ends_with?(opts[:name], "[]"), 812 | do: input_id(form, field, checked_value), 813 | else: input_id(form, field) 814 | end) 815 | 816 | if hidden_input do 817 | hidden_opts = [type: "hidden", value: unchecked_value] 818 | 819 | html_escape([ 820 | tag(:input, hidden_opts ++ Keyword.take(opts, [:name, :disabled, :form])), 821 | tag(:input, [value: checked_value] ++ opts) 822 | ]) 823 | else 824 | html_escape([ 825 | tag(:input, [value: checked_value] ++ opts) 826 | ]) 827 | end 828 | end 829 | 830 | @doc """ 831 | Generates a select tag with the given `options`. 832 | 833 | `options` are expected to be an enumerable which will be used to 834 | generate each respective `option`. The enumerable may have: 835 | 836 | * keyword lists - each keyword list is expected to have the keys 837 | `:key` and `:value`. Additional keys such as `:disabled` may 838 | be given to customize the option. 839 | 840 | * two-item tuples - where the first element is an atom, string or 841 | integer to be used as the option label and the second element is 842 | an atom, string or integer to be used as the option value 843 | 844 | * atom, string or integer - which will be used as both label and value 845 | for the generated select 846 | 847 | ## Optgroups 848 | 849 | If `options` is map or keyword list where the first element is a string, 850 | atom or integer and the second element is a list or a map, it is assumed 851 | the key will be wrapped in an `` and the value will be used to 852 | generate `` nested under the group. 853 | 854 | ## Examples 855 | 856 | # Assuming form contains a User schema 857 | select(form, :age, 0..120) 858 | #=> 863 | 864 | select(form, :role, ["Admin": "admin", "User": "user"]) 865 | #=> 869 | 870 | select(form, :role, [[key: "Admin", value: "admin", disabled: true], 871 | [key: "User", value: "user"]]) 872 | #=> 876 | 877 | You can also pass a prompt: 878 | 879 | select(form, :role, ["Admin": "admin", "User": "user"], prompt: "Choose your role") 880 | #=> 885 | 886 | And customize the prompt like any other entry: 887 | 888 | select(form, :role, ["Admin": "admin", "User": "user"], prompt: [key: "Choose your role", disabled: true]) 889 | #=> 894 | 895 | If you want to select an option that comes from the database, 896 | such as a manager for a given project, you may write: 897 | 898 | select(form, :manager_id, Enum.map(@managers, &{&1.name, &1.id})) 899 | #=> 903 | 904 | Finally, if the values are a list or a map, we use the keys for 905 | grouping: 906 | 907 | select(form, :country, ["Europe": ["UK", "Sweden", "France"]], ...) 908 | #=> 916 | 917 | ## Options 918 | 919 | * `:prompt` - an option to include at the top of the options. It may be 920 | a string or a keyword list of attributes and the `:key` 921 | 922 | * `:selected` - the default value to use when none was sent as parameter 923 | 924 | Be aware that a `:multiple` option will not generate a correctly 925 | functioning multiple select element. Use `multiple_select/4` instead. 926 | 927 | All other options are forwarded to the underlying HTML tag. 928 | """ 929 | def select(form, field, options, opts \\ []) when is_atom(field) or is_binary(field) do 930 | {selected, opts} = selected(form, field, opts) 931 | options_html = options_for_select(options, selected) 932 | 933 | {options_html, opts} = 934 | case Keyword.pop(opts, :prompt) do 935 | {nil, opts} -> {options_html, opts} 936 | {prompt, opts} -> {[prompt_option(prompt) | options_html], opts} 937 | end 938 | 939 | opts = 940 | opts 941 | |> Keyword.put_new(:id, input_id(form, field)) 942 | |> Keyword.put_new(:name, input_name(form, field)) 943 | 944 | content_tag(:select, options_html, opts) 945 | end 946 | 947 | defp prompt_option(prompt) when is_list(prompt) do 948 | {prompt_key, prompt_opts} = Keyword.pop(prompt, :key) 949 | 950 | prompt_key || 951 | raise ArgumentError, 952 | "expected :key key when building a prompt select option with a keyword list: " <> 953 | inspect(prompt) 954 | 955 | prompt_option(prompt_key, prompt_opts) 956 | end 957 | 958 | defp prompt_option(key) when is_binary(key), do: prompt_option(key, []) 959 | 960 | defp prompt_option(key, opts) when is_list(opts) do 961 | content_tag(:option, key, Keyword.put_new(opts, :value, "")) 962 | end 963 | 964 | @doc """ 965 | Generates a select tag with the given `options`. 966 | 967 | Values are expected to be an Enumerable containing two-item tuples 968 | (like maps and keyword lists) or any Enumerable where the element 969 | will be used both as key and value for the generated select. 970 | 971 | ## Examples 972 | 973 | # Assuming form contains a User schema 974 | multiple_select(form, :roles, ["Admin": 1, "Power User": 2]) 975 | #=> 979 | 980 | multiple_select(form, :roles, ["Admin": 1, "Power User": 2], selected: [1]) 981 | #=> 985 | 986 | When working with structs, associations, and embeds, you will need to tell 987 | Phoenix how to extract the value out of the collection. For example, 988 | imagine `user.roles` is a list of `%Role{}` structs. You must call it as: 989 | 990 | multiple_select(form, :roles, ["Admin": 1, "Power User": 2], 991 | selected: Enum.map(@user.roles, &(&1.id)) 992 | 993 | The `:selected` option will mark the given IDs as selected unless the form 994 | is being resubmitted. When resubmitted, it uses the form params as values. 995 | 996 | When used with Ecto, you will typically do a query to retrieve the IDs from 997 | the database: 998 | 999 | from r in Role, where: r.id in ^(params["roles"] || []) 1000 | 1001 | And then use `Ecto.Changeset.put_assoc/2` to insert the new roles into the user. 1002 | 1003 | ## Options 1004 | 1005 | * `:selected` - the default options to be marked as selected. The values 1006 | on this list are ignored in case ids have been set as parameters. 1007 | 1008 | All other options are forwarded to the underlying HTML tag. 1009 | """ 1010 | def multiple_select(form, field, options, opts \\ []) do 1011 | {selected, opts} = selected(form, field, opts) 1012 | 1013 | opts = 1014 | opts 1015 | |> Keyword.put_new(:id, input_id(form, field)) 1016 | |> Keyword.put_new(:name, input_name(form, field) <> "[]") 1017 | |> Keyword.put_new(:multiple, "") 1018 | 1019 | content_tag(:select, options_for_select(options, selected), opts) 1020 | end 1021 | 1022 | defp selected(form, field, opts) do 1023 | {value, opts} = Keyword.pop(opts, :value) 1024 | {selected, opts} = Keyword.pop(opts, :selected) 1025 | 1026 | if value != nil do 1027 | {value, opts} 1028 | else 1029 | param = field_to_string(field) 1030 | 1031 | case form do 1032 | %{params: %{^param => sent}} -> 1033 | {sent, opts} 1034 | 1035 | _ -> 1036 | {selected || input_value(form, field), opts} 1037 | end 1038 | end 1039 | end 1040 | 1041 | ## Datetime 1042 | 1043 | @doc ~S''' 1044 | Generates select tags for datetime. 1045 | 1046 | Warning: This functionality is best provided by browsers nowadays. 1047 | Consider using `datetime_local_input/3` instead. 1048 | 1049 | ## Examples 1050 | 1051 | # Assuming form contains a User schema 1052 | datetime_select form, :born_at 1053 | #=> / 1054 | #=> / 1055 | #=> — 1056 | #=> : 1057 | #=> 1058 | 1059 | If you want to include the seconds field (hidden by default), pass `second: []`: 1060 | 1061 | # Assuming form contains a User schema 1062 | datetime_select form, :born_at, second: [] 1063 | 1064 | If you want to configure the years range: 1065 | 1066 | # Assuming form contains a User schema 1067 | datetime_select form, :born_at, year: [options: 1900..2100] 1068 | 1069 | You are also able to configure `:month`, `:day`, `:hour`, `:minute` and 1070 | `:second`. All options given to those keys will be forwarded to the 1071 | underlying select. See `select/4` for more information. 1072 | 1073 | For example, if you are using Phoenix with Gettext and you want to localize 1074 | the list of months, you can pass `:options` to the `:month` key: 1075 | 1076 | # Assuming form contains a User schema 1077 | datetime_select form, :born_at, month: [ 1078 | options: [ 1079 | {gettext("January"), "1"}, 1080 | {gettext("February"), "2"}, 1081 | {gettext("March"), "3"}, 1082 | {gettext("April"), "4"}, 1083 | {gettext("May"), "5"}, 1084 | {gettext("June"), "6"}, 1085 | {gettext("July"), "7"}, 1086 | {gettext("August"), "8"}, 1087 | {gettext("September"), "9"}, 1088 | {gettext("October"), "10"}, 1089 | {gettext("November"), "11"}, 1090 | {gettext("December"), "12"}, 1091 | ] 1092 | ] 1093 | 1094 | You may even provide your own `localized_datetime_select/3` built on top of 1095 | `datetime_select/3`: 1096 | 1097 | defp localized_datetime_select(form, field, opts \\ []) do 1098 | opts = 1099 | Keyword.put(opts, :month, options: [ 1100 | {gettext("January"), "1"}, 1101 | {gettext("February"), "2"}, 1102 | {gettext("March"), "3"}, 1103 | {gettext("April"), "4"}, 1104 | {gettext("May"), "5"}, 1105 | {gettext("June"), "6"}, 1106 | {gettext("July"), "7"}, 1107 | {gettext("August"), "8"}, 1108 | {gettext("September"), "9"}, 1109 | {gettext("October"), "10"}, 1110 | {gettext("November"), "11"}, 1111 | {gettext("December"), "12"}, 1112 | ]) 1113 | 1114 | datetime_select(form, field, opts) 1115 | end 1116 | 1117 | ## Options 1118 | 1119 | * `:value` - the value used to select a given option. 1120 | The default value is extracted from the form data if available. 1121 | 1122 | * `:default` - the default value to use when none was given in 1123 | `:value` and none is available in the form data 1124 | 1125 | * `:year`, `:month`, `:day`, `:hour`, `:minute`, `:second` - options passed 1126 | to the underlying select. See `select/4` for more information. 1127 | The available values can be given in `:options`. 1128 | 1129 | * `:builder` - specify how the select can be build. It must be a function 1130 | that receives a builder that should be invoked with the select name 1131 | and a set of options. See builder below for more information. 1132 | 1133 | ## Builder 1134 | 1135 | The generated datetime_select can be customized at will by providing a 1136 | builder option. Here is an example from EEx: 1137 | 1138 | <%= datetime_select form, :born_at, builder: fn b -> %> 1139 | Date: <%= b.(:day, []) %> / <%= b.(:month, []) %> / <%= b.(:year, []) %> 1140 | Time: <%= b.(:hour, []) %> : <%= b.(:minute, []) %> 1141 | <% end %> 1142 | 1143 | Although we have passed empty lists as options (they are required), you 1144 | could pass any option there and it would be given to the underlying select 1145 | input. 1146 | 1147 | In practice, we recommend you to create your own helper with your default 1148 | builder: 1149 | 1150 | def my_datetime_select(form, field, opts \\ []) do 1151 | builder = fn b -> 1152 | assigns = %{b: b} 1153 | 1154 | ~H""" 1155 | Date: <%= @b.(:day, []) %> / <%= @b.(:month, []) %> / <%= @b.(:year, []) %> 1156 | Time: <%= @b.(:hour, []) %> : <%= @b.(:minute, []) %> 1157 | """ 1158 | end 1159 | 1160 | datetime_select(form, field, [builder: builder] ++ opts) 1161 | end 1162 | 1163 | Then you are able to use your own datetime_select throughout your whole 1164 | application. 1165 | 1166 | ## Supported date values 1167 | 1168 | The following values are supported as date: 1169 | 1170 | * a map containing the `year`, `month` and `day` keys (either as strings or atoms) 1171 | * a tuple with three elements: `{year, month, day}` 1172 | * a string in ISO 8601 format 1173 | * `nil` 1174 | 1175 | ## Supported time values 1176 | 1177 | The following values are supported as time: 1178 | 1179 | * a map containing the `hour` and `minute` keys and an optional `second` key (either as strings or atoms) 1180 | * a tuple with three elements: `{hour, min, sec}` 1181 | * a tuple with four elements: `{hour, min, sec, usec}` 1182 | * `nil` 1183 | 1184 | ''' 1185 | def datetime_select(form, field, opts \\ []) do 1186 | value = Keyword.get(opts, :value, input_value(form, field) || Keyword.get(opts, :default)) 1187 | 1188 | builder = 1189 | Keyword.get(opts, :builder) || 1190 | fn b -> 1191 | date = date_builder(b, opts) 1192 | time = time_builder(b, opts) 1193 | html_escape([date, raw(" — "), time]) 1194 | end 1195 | 1196 | builder.(datetime_builder(form, field, date_value(value), time_value(value), opts)) 1197 | end 1198 | 1199 | @doc """ 1200 | Generates select tags for date. 1201 | 1202 | Warning: This functionality is best provided by browsers nowadays. 1203 | Consider using `date_input/3` instead. 1204 | 1205 | Check `datetime_select/3` for more information on options and supported values. 1206 | """ 1207 | def date_select(form, field, opts \\ []) do 1208 | value = Keyword.get(opts, :value, input_value(form, field) || Keyword.get(opts, :default)) 1209 | builder = Keyword.get(opts, :builder) || (&date_builder(&1, opts)) 1210 | builder.(datetime_builder(form, field, date_value(value), nil, opts)) 1211 | end 1212 | 1213 | defp date_builder(b, _opts) do 1214 | html_escape([b.(:year, []), raw(" / "), b.(:month, []), raw(" / "), b.(:day, [])]) 1215 | end 1216 | 1217 | defp date_value(%{"year" => year, "month" => month, "day" => day}), 1218 | do: %{year: year, month: month, day: day} 1219 | 1220 | defp date_value(%{year: year, month: month, day: day}), 1221 | do: %{year: year, month: month, day: day} 1222 | 1223 | defp date_value({{year, month, day}, _}), do: %{year: year, month: month, day: day} 1224 | defp date_value({year, month, day}), do: %{year: year, month: month, day: day} 1225 | 1226 | defp date_value(nil), do: %{year: nil, month: nil, day: nil} 1227 | 1228 | defp date_value(string) when is_binary(string) do 1229 | string 1230 | |> Date.from_iso8601!() 1231 | |> date_value 1232 | end 1233 | 1234 | defp date_value(other), do: raise(ArgumentError, "unrecognized date #{inspect(other)}") 1235 | 1236 | @doc """ 1237 | Generates select tags for time. 1238 | 1239 | Warning: This functionality is best provided by browsers nowadays. 1240 | Consider using `time_input/3` instead. 1241 | 1242 | Check `datetime_select/3` for more information on options and supported values. 1243 | """ 1244 | def time_select(form, field, opts \\ []) do 1245 | value = Keyword.get(opts, :value, input_value(form, field) || Keyword.get(opts, :default)) 1246 | builder = Keyword.get(opts, :builder) || (&time_builder(&1, opts)) 1247 | builder.(datetime_builder(form, field, nil, time_value(value), opts)) 1248 | end 1249 | 1250 | defp time_builder(b, opts) do 1251 | time = html_escape([b.(:hour, []), raw(" : "), b.(:minute, [])]) 1252 | 1253 | if Keyword.get(opts, :second) do 1254 | html_escape([time, raw(" : "), b.(:second, [])]) 1255 | else 1256 | time 1257 | end 1258 | end 1259 | 1260 | defp time_value(%{"hour" => hour, "minute" => min} = map), 1261 | do: %{hour: hour, minute: min, second: Map.get(map, "second", 0)} 1262 | 1263 | defp time_value(%{hour: hour, minute: min} = map), 1264 | do: %{hour: hour, minute: min, second: Map.get(map, :second, 0)} 1265 | 1266 | defp time_value({_, {hour, min, sec}}), 1267 | do: %{hour: hour, minute: min, second: sec} 1268 | 1269 | defp time_value({hour, min, sec}), 1270 | do: %{hour: hour, minute: min, second: sec} 1271 | 1272 | defp time_value(nil), do: %{hour: nil, minute: nil, second: nil} 1273 | 1274 | defp time_value(string) when is_binary(string) do 1275 | string 1276 | |> Time.from_iso8601!() 1277 | |> time_value 1278 | end 1279 | 1280 | defp time_value(other), do: raise(ArgumentError, "unrecognized time #{inspect(other)}") 1281 | 1282 | @months [ 1283 | {"January", "1"}, 1284 | {"February", "2"}, 1285 | {"March", "3"}, 1286 | {"April", "4"}, 1287 | {"May", "5"}, 1288 | {"June", "6"}, 1289 | {"July", "7"}, 1290 | {"August", "8"}, 1291 | {"September", "9"}, 1292 | {"October", "10"}, 1293 | {"November", "11"}, 1294 | {"December", "12"} 1295 | ] 1296 | 1297 | map = 1298 | &Enum.map(&1, fn i -> 1299 | pre = if i < 10, do: "0" 1300 | {"#{pre}#{i}", i} 1301 | end) 1302 | 1303 | @days map.(1..31) 1304 | @hours map.(0..23) 1305 | @minsec map.(0..59) 1306 | 1307 | defp datetime_builder(form, field, date, time, parent) do 1308 | id = Keyword.get(parent, :id, input_id(form, field)) 1309 | name = Keyword.get(parent, :name, input_name(form, field)) 1310 | 1311 | fn 1312 | :year, opts when date != nil -> 1313 | {year, _, _} = :erlang.date() 1314 | 1315 | {value, opts} = 1316 | datetime_options(:year, (year - 5)..(year + 5), id, name, parent, date, opts) 1317 | 1318 | select(:datetime, :year, value, opts) 1319 | 1320 | :month, opts when date != nil -> 1321 | {value, opts} = datetime_options(:month, @months, id, name, parent, date, opts) 1322 | select(:datetime, :month, value, opts) 1323 | 1324 | :day, opts when date != nil -> 1325 | {value, opts} = datetime_options(:day, @days, id, name, parent, date, opts) 1326 | select(:datetime, :day, value, opts) 1327 | 1328 | :hour, opts when time != nil -> 1329 | {value, opts} = datetime_options(:hour, @hours, id, name, parent, time, opts) 1330 | select(:datetime, :hour, value, opts) 1331 | 1332 | :minute, opts when time != nil -> 1333 | {value, opts} = datetime_options(:minute, @minsec, id, name, parent, time, opts) 1334 | select(:datetime, :minute, value, opts) 1335 | 1336 | :second, opts when time != nil -> 1337 | {value, opts} = datetime_options(:second, @minsec, id, name, parent, time, opts) 1338 | select(:datetime, :second, value, opts) 1339 | end 1340 | end 1341 | 1342 | defp datetime_options(type, values, id, name, parent, datetime, opts) do 1343 | opts = Keyword.merge(Keyword.get(parent, type, []), opts) 1344 | suff = Atom.to_string(type) 1345 | 1346 | {value, opts} = Keyword.pop(opts, :options, values) 1347 | 1348 | {value, 1349 | opts 1350 | |> Keyword.put_new(:id, id <> "_" <> suff) 1351 | |> Keyword.put_new(:name, name <> "[" <> suff <> "]") 1352 | |> Keyword.put_new(:value, Map.get(datetime, type))} 1353 | end 1354 | 1355 | @doc """ 1356 | Generates a label tag. 1357 | 1358 | Useful when wrapping another input inside a label. 1359 | 1360 | ## Examples 1361 | 1362 | label do 1363 | radio_button :user, :choice, "Choice" 1364 | end 1365 | #=> 1366 | 1367 | label class: "control-label" do 1368 | radio_button :user, :choice, "Choice" 1369 | end 1370 | #=> 1371 | 1372 | """ 1373 | def label(do_block) 1374 | 1375 | def label(do: block) do 1376 | content_tag(:label, block, []) 1377 | end 1378 | 1379 | def label(opts, do: block) when is_list(opts) do 1380 | content_tag(:label, block, opts) 1381 | end 1382 | 1383 | @doc """ 1384 | Generates a label tag for the given field. 1385 | 1386 | The form should either be a `Phoenix.HTML.Form` emitted 1387 | by `form_for` or an atom. 1388 | 1389 | All given options are forwarded to the underlying tag. 1390 | A default value is provided for `for` attribute but can 1391 | be overridden if you pass a value to the `for` option. 1392 | Text content would be inferred from `field` if not specified 1393 | as either a function argument or string value in a block. 1394 | 1395 | To wrap a label around an input, see `label/1`. 1396 | 1397 | ## Examples 1398 | 1399 | # Assuming form contains a User schema 1400 | label(form, :name, "Name") 1401 | #=> 1402 | 1403 | label(:user, :email, "Email") 1404 | #=> 1405 | 1406 | label(:user, :email) 1407 | #=> 1408 | 1409 | label(:user, :email, class: "control-label") 1410 | #=> 1411 | 1412 | label :user, :email do 1413 | "E-mail Address" 1414 | end 1415 | #=> 1416 | 1417 | label :user, :email, "E-mail Address", class: "control-label" 1418 | #=> 1419 | 1420 | label :user, :email, class: "control-label" do 1421 | "E-mail Address" 1422 | end 1423 | #=> 1424 | 1425 | """ 1426 | def label(form, field) when is_atom(field) or is_binary(field) do 1427 | label(form, field, humanize(field), []) 1428 | end 1429 | 1430 | @doc """ 1431 | See `label/2`. 1432 | """ 1433 | def label(form, field, text_or_do_block_or_attributes) 1434 | 1435 | def label(form, field, do: block) do 1436 | label(form, field, [], do: block) 1437 | end 1438 | 1439 | def label(form, field, opts) when is_list(opts) do 1440 | label(form, field, humanize(field), opts) 1441 | end 1442 | 1443 | def label(form, field, text) do 1444 | label(form, field, text, []) 1445 | end 1446 | 1447 | @doc """ 1448 | See `label/2`. 1449 | """ 1450 | def label(form, field, text, do_block_or_attributes) 1451 | 1452 | def label(form, field, opts, do: block) when is_list(opts) do 1453 | opts = Keyword.put_new(opts, :for, input_id(form, field)) 1454 | content_tag(:label, block, opts) 1455 | end 1456 | 1457 | def label(form, field, text, opts) when is_list(opts) do 1458 | opts = Keyword.put_new(opts, :for, input_id(form, field)) 1459 | content_tag(:label, text, opts) 1460 | end 1461 | 1462 | # Normalize field name to string version 1463 | defp field_to_string(field) when is_atom(field), do: Atom.to_string(field) 1464 | defp field_to_string(field) when is_binary(field), do: field 1465 | end 1466 | -------------------------------------------------------------------------------- /lib/phoenix_html_helpers/form_data.ex: -------------------------------------------------------------------------------- 1 | defimpl Phoenix.HTML.FormData, for: [Plug.Conn, Atom] do 2 | def to_form(conn_or_atom_or_map, opts) do 3 | {name, params, opts} = name_params_and_opts(conn_or_atom_or_map, opts) 4 | {errors, opts} = Keyword.pop(opts, :errors, []) 5 | id = Keyword.get(opts, :id) || name 6 | 7 | unless is_binary(id) or is_nil(id) do 8 | raise ArgumentError, ":id option in form_for must be a binary/string, got: #{inspect(id)}" 9 | end 10 | 11 | %Phoenix.HTML.Form{ 12 | source: conn_or_atom_or_map, 13 | impl: __MODULE__, 14 | id: id, 15 | name: name, 16 | params: params, 17 | data: %{}, 18 | errors: errors, 19 | options: opts 20 | } 21 | end 22 | 23 | case @for do 24 | Atom -> 25 | defp name_params_and_opts(atom, opts) do 26 | {params, opts} = Keyword.pop(opts, :params, %{}) 27 | {Atom.to_string(atom), params, opts} 28 | end 29 | 30 | Plug.Conn -> 31 | defp name_params_and_opts(conn, opts) do 32 | case Keyword.pop(opts, :as) do 33 | {nil, opts} -> 34 | {nil, conn.params, opts} 35 | 36 | {name, opts} -> 37 | name = to_string(name) 38 | {name, Map.get(conn.params, name) || %{}, opts} 39 | end 40 | end 41 | end 42 | 43 | def to_form(conn_or_atom_or_map, form, field, opts) when is_atom(field) or is_binary(field) do 44 | {default, opts} = Keyword.pop(opts, :default, %{}) 45 | {prepend, opts} = Keyword.pop(opts, :prepend, []) 46 | {append, opts} = Keyword.pop(opts, :append, []) 47 | {name, opts} = Keyword.pop(opts, :as) 48 | {id, opts} = Keyword.pop(opts, :id) 49 | {hidden, opts} = Keyword.pop(opts, :hidden, []) 50 | 51 | id = to_string(id || form.id <> "_#{field}") 52 | name = to_string(name || form.name <> "[#{field}]") 53 | params = Map.get(form.params, field_to_string(field)) 54 | 55 | cond do 56 | # cardinality: one 57 | is_map(default) -> 58 | [ 59 | %Phoenix.HTML.Form{ 60 | source: conn_or_atom_or_map, 61 | impl: __MODULE__, 62 | id: id, 63 | name: name, 64 | data: default, 65 | params: params || %{}, 66 | hidden: hidden, 67 | options: opts 68 | } 69 | ] 70 | 71 | # cardinality: many 72 | is_list(default) -> 73 | entries = 74 | if params do 75 | params 76 | |> Enum.sort_by(&elem(&1, 0)) 77 | |> Enum.map(&{nil, elem(&1, 1)}) 78 | else 79 | Enum.map(prepend ++ default ++ append, &{&1, %{}}) 80 | end 81 | 82 | for {{data, params}, index} <- Enum.with_index(entries) do 83 | index_string = Integer.to_string(index) 84 | 85 | %Phoenix.HTML.Form{ 86 | source: conn_or_atom_or_map, 87 | impl: __MODULE__, 88 | index: index, 89 | id: id <> "_" <> index_string, 90 | name: name <> "[" <> index_string <> "]", 91 | data: data, 92 | params: params, 93 | hidden: hidden, 94 | options: opts 95 | } 96 | end 97 | end 98 | end 99 | 100 | def input_value(_conn_or_atom_or_map, %{data: data, params: params}, field) 101 | when is_atom(field) or is_binary(field) do 102 | key = field_to_string(field) 103 | 104 | case params do 105 | %{^key => value} -> value 106 | %{} -> Map.get(data, field) 107 | end 108 | end 109 | 110 | def input_validations(_conn_or_atom_or_map, _form, _field), do: [] 111 | 112 | # Normalize field name to string version 113 | defp field_to_string(field) when is_atom(field), do: Atom.to_string(field) 114 | defp field_to_string(field) when is_binary(field), do: field 115 | end 116 | -------------------------------------------------------------------------------- /lib/phoenix_html_helpers/format.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHTMLHelpers.Format do 2 | @moduledoc """ 3 | Formatting functions. 4 | """ 5 | 6 | @doc ~S""" 7 | Returns text transformed into HTML using simple formatting rules. 8 | 9 | Two or more consecutive newlines `\n\n` or `\r\n\r\n` are considered as a 10 | paragraph and text between them is wrapped in `

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

Hello

\n

World

\n" 17 | 18 | iex> text_to_html("Hello\nWorld") |> safe_to_string 19 | "

Hello
\nWorld

\n" 20 | 21 | iex> opts = [wrapper_tag: :div, attributes: [class: "p"]] 22 | ...> text_to_html("Hello\n\nWorld", opts) |> safe_to_string 23 | "
Hello
\n
World
\n" 24 | 25 | ## Options 26 | 27 | * `:escape` - if `false` does not html escape input (default: `true`) 28 | * `:wrapper_tag` - tag to wrap each paragraph (default: `:p`) 29 | * `:attributes` - html attributes of the wrapper tag (default: `[]`) 30 | * `:insert_brs` - if `true` insert `
` for single line breaks (default: `true`) 31 | 32 | """ 33 | @spec text_to_html(Phoenix.HTML.unsafe(), Keyword.t()) :: Phoenix.HTML.safe() 34 | def text_to_html(string, opts \\ []) do 35 | escape? = Keyword.get(opts, :escape, true) 36 | wrapper_tag = Keyword.get(opts, :wrapper_tag, :p) 37 | attributes = Keyword.get(opts, :attributes, []) 38 | insert_brs? = Keyword.get(opts, :insert_brs, true) 39 | 40 | string 41 | |> maybe_html_escape(escape?) 42 | |> String.split(["\n\n", "\r\n\r\n"], trim: true) 43 | |> Enum.filter(¬_blank?/1) 44 | |> Enum.map(&wrap_paragraph(&1, wrapper_tag, attributes, insert_brs?)) 45 | |> Phoenix.HTML.html_escape() 46 | end 47 | 48 | defp maybe_html_escape(string, true), 49 | do: string |> Phoenix.HTML.Engine.html_escape() |> IO.iodata_to_binary() 50 | 51 | defp maybe_html_escape(string, false), 52 | do: string 53 | 54 | defp not_blank?("\r\n" <> rest), do: not_blank?(rest) 55 | defp not_blank?("\n" <> rest), do: not_blank?(rest) 56 | defp not_blank?(" " <> rest), do: not_blank?(rest) 57 | defp not_blank?(""), do: false 58 | defp not_blank?(_), do: true 59 | 60 | defp wrap_paragraph(text, tag, attributes, insert_brs?) do 61 | [PhoenixHTMLHelpers.Tag.content_tag(tag, insert_brs(text, insert_brs?), attributes), ?\n] 62 | end 63 | 64 | defp insert_brs(text, false) do 65 | text 66 | |> split_lines() 67 | |> Enum.intersperse(?\s) 68 | |> Phoenix.HTML.raw() 69 | end 70 | 71 | defp insert_brs(text, true) do 72 | text 73 | |> split_lines() 74 | |> Enum.map(&Phoenix.HTML.raw/1) 75 | |> Enum.intersperse([PhoenixHTMLHelpers.Tag.tag(:br), ?\n]) 76 | end 77 | 78 | defp split_lines(text) do 79 | String.split(text, ["\n", "\r\n"], trim: true) 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/phoenix_html_helpers/link.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHTMLHelpers.Link do 2 | @moduledoc """ 3 | Conveniences for working with links and URLs in HTML. 4 | """ 5 | 6 | import PhoenixHTMLHelpers.Tag 7 | 8 | @doc """ 9 | Generates a link to the given URL. 10 | 11 | ## Examples 12 | 13 | link("hello", to: "/world") 14 | #=> hello 15 | 16 | link("hello", to: URI.parse("https://elixir-lang.org")) 17 | #=> hello 18 | 19 | link("", to: "/world") 20 | #=> <hello> 21 | 22 | link("", to: "/world", class: "btn") 23 | #=> <hello> 24 | 25 | link("delete", to: "/the_world", data: [confirm: "Really?"]) 26 | #=> delete 27 | 28 | # If you supply a method other than `:get`: 29 | link("delete", to: "/everything", method: :delete) 30 | #=> delete 31 | 32 | # You can use a `do ... end` block too: 33 | link to: "/hello" do 34 | "world" 35 | end 36 | #=> world 37 | 38 | ## Options 39 | 40 | * `:to` - the page to link to. This option is required 41 | 42 | * `:method` - the method to use with the link. In case the 43 | method is not `:get`, the link is generated inside the form 44 | which sets the proper information. In order to submit the 45 | form, JavaScript must be enabled 46 | 47 | * `:csrf_token` - a custom token to use for links with a method 48 | other than `:get`. 49 | 50 | All other options are forwarded to the underlying `` tag. 51 | 52 | ## Data attributes 53 | 54 | Data attributes are added as a keyword list passed to the `data` key. 55 | The following data attributes are supported: 56 | 57 | * `data-confirm` - shows a confirmation prompt before 58 | generating and submitting the form when `:method` 59 | is not `:get`. 60 | 61 | ## CSRF Protection 62 | 63 | By default, CSRF tokens are generated through `Plug.CSRFProtection`. 64 | """ 65 | def link(text, opts) 66 | 67 | def link(opts, do: contents) when is_list(opts) do 68 | link(contents, opts) 69 | end 70 | 71 | def link(_text, opts) when not is_list(opts) do 72 | raise ArgumentError, "link/2 requires a keyword list as second argument" 73 | end 74 | 75 | def link(text, opts) do 76 | {to, opts} = pop_required_option!(opts, :to, "expected non-nil value for :to in link/2") 77 | {method, opts} = Keyword.pop(opts, :method, :get) 78 | 79 | if method == :get do 80 | # Call link attributes to validate `to` 81 | [data: data] = link_attributes(to, []) 82 | content_tag(:a, text, [href: data[:to]] ++ Keyword.delete(opts, :csrf_token)) 83 | else 84 | {csrf_token, opts} = Keyword.pop(opts, :csrf_token, true) 85 | opts = Keyword.put_new(opts, :rel, "nofollow") 86 | [data: data] = link_attributes(to, method: method, csrf_token: csrf_token) 87 | content_tag(:a, text, [data: data, href: data[:to]] ++ opts) 88 | end 89 | end 90 | 91 | @doc """ 92 | Generates a button tag that uses the Javascript function handleClick() 93 | (see phoenix_html.js) to submit the form data. 94 | 95 | Useful to ensure that links that change data are not triggered by 96 | search engines and other spidering software. 97 | 98 | ## Examples 99 | 100 | button("hello", to: "/world") 101 | #=> 102 | 103 | button("hello", to: "/world", method: :get, class: "btn") 104 | #=> 105 | 106 | ## Options 107 | 108 | * `:to` - the page to link to. This option is required 109 | 110 | * `:method` - the method to use with the button. Defaults to :post. 111 | 112 | All other options are forwarded to the underlying button input. 113 | 114 | When the `:method` is set to `:get` and the `:to` URL contains query 115 | parameters the generated form element will strip the parameters in accordance 116 | with the [W3C](https://www.w3.org/TR/html401/interact/forms.html#h-17.13.3.4) 117 | form specification. 118 | 119 | ## Data attributes 120 | 121 | Data attributes are added as a keyword list passed to the 122 | `data` key. The following data attributes are supported: 123 | 124 | * `data-confirm` - shows a confirmation prompt before generating and 125 | submitting the form. 126 | """ 127 | def button(opts, do: contents) do 128 | button(contents, opts) 129 | end 130 | 131 | def button(text, opts) do 132 | {to, opts} = pop_required_option!(opts, :to, "option :to is required in button/2") 133 | 134 | {link_opts, opts} = 135 | opts 136 | |> Keyword.put_new(:method, :post) 137 | |> Keyword.split([:method, :csrf_token]) 138 | 139 | content_tag(:button, text, link_attributes(to, link_opts) ++ opts) 140 | end 141 | 142 | defp pop_required_option!(opts, key, error_message) do 143 | {value, opts} = Keyword.pop(opts, key) 144 | 145 | unless value do 146 | raise ArgumentError, error_message 147 | end 148 | 149 | {value, opts} 150 | end 151 | 152 | defp link_attributes(to, opts) do 153 | to = valid_destination!(to) 154 | method = Keyword.get(opts, :method, :get) 155 | data = [method: method, to: to] 156 | 157 | data = 158 | if method == :get do 159 | data 160 | else 161 | case Keyword.get(opts, :csrf_token, true) do 162 | true -> [csrf: PhoenixHTMLHelpers.Tag.csrf_token_value(to)] ++ data 163 | false -> data 164 | csrf when is_binary(csrf) -> [csrf: csrf] ++ data 165 | end 166 | end 167 | 168 | [data: data] 169 | end 170 | 171 | defp valid_destination!(%URI{} = uri) do 172 | valid_destination!(URI.to_string(uri)) 173 | end 174 | 175 | defp valid_destination!({:safe, to}) do 176 | {:safe, valid_string_destination!(IO.iodata_to_binary(to))} 177 | end 178 | 179 | defp valid_destination!({other, to}) when is_atom(other) do 180 | [Atom.to_string(other), ?:, to] 181 | end 182 | 183 | defp valid_destination!(to) do 184 | valid_string_destination!(IO.iodata_to_binary(to)) 185 | end 186 | 187 | @valid_uri_schemes ~w(http: https: ftp: ftps: mailto: news: irc: gopher:) ++ 188 | ~w(nntp: feed: telnet: mms: rtsp: svn: tel: fax: xmpp:) 189 | 190 | for scheme <- @valid_uri_schemes do 191 | defp valid_string_destination!(unquote(scheme) <> _ = string), do: string 192 | end 193 | 194 | defp valid_string_destination!(to) do 195 | if not match?("/" <> _, to) and String.contains?(to, ":") do 196 | raise ArgumentError, """ 197 | unsupported scheme given as link. In case you want to link to an 198 | unknown or unsafe scheme, such as javascript, use a tuple: {:javascript, rest}\ 199 | """ 200 | else 201 | to 202 | end 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /lib/phoenix_html_helpers/tag.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHTMLHelpers.Tag do 2 | @moduledoc ~S""" 3 | Helpers related to producing HTML tags within templates. 4 | 5 | > Note: with the addition of the HEEx template engine to 6 | > Phoenix applications, the functions in this module have 7 | > lost a bit of relevance and must only be used in special 8 | > circumstances. 9 | > 10 | > Whenever possible, prefer to use the HEEx template engine 11 | > instead of the functions here. For example, instead of: 12 | > 13 | > <%= content_tag :div, class: @class do %> 14 | > Hello 15 | > <% end %> 16 | > 17 | > Do: 18 | > 19 | >
20 | > Hello 21 | >
22 | 23 | > Note: the examples in this module use `safe_to_string/1` 24 | > imported from `Phoenix.HTML` for readability. 25 | """ 26 | 27 | import Phoenix.HTML 28 | 29 | @csrf_param "_csrf_token" 30 | 31 | @doc ~S""" 32 | Creates an HTML tag with the given name and options. 33 | 34 | iex> safe_to_string tag(:br) 35 | "
" 36 | iex> safe_to_string tag(:input, type: "text", name: "user_id") 37 | "" 38 | 39 | ## Data attributes 40 | 41 | In order to add custom data attributes you need to pass 42 | a tuple containing :data atom and a keyword list 43 | with data attributes' names and values as the first element 44 | in the tag's attributes keyword list: 45 | 46 | iex> safe_to_string tag(:input, [data: [foo: "bar"], id: "some_id"]) 47 | "" 48 | 49 | ## Boolean values 50 | 51 | In case an attribute contains a boolean value, its key 52 | is repeated when it is true, as expected in HTML, or 53 | the attribute is completely removed if it is false: 54 | 55 | iex> safe_to_string tag(:audio, autoplay: "autoplay") 56 | "
hello) 11 | end 12 | 13 | test "link with put/delete using a custom csrf token" do 14 | assert safe_to_string(link("hello", to: "/world", method: :put)) =~ 15 | ~r(hello) 16 | end 17 | 18 | test "button with post using a custom csrf token" do 19 | assert safe_to_string(button("hello", to: "/world")) =~ 20 | ~r() 21 | end 22 | 23 | test "form_tag for post using a custom csrf token" do 24 | assert safe_to_string(form_tag("/")) =~ ~r( 25 | 26 | 27 | )mx 28 | end 29 | 30 | test "form_tag for other method using a custom csrf token" do 31 | assert safe_to_string(form_tag("/", method: :put)) =~ ~r( 32 | 33 | 34 | 35 | )mx 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/phoenix_html_helpers/form_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHTMLHelpers.FormTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Phoenix.HTML 5 | import PhoenixHTMLHelpers.Form 6 | doctest PhoenixHTMLHelpers.Form 7 | 8 | defp search_params do 9 | %{ 10 | "key" => "value", 11 | "time" => ~T[01:02:03.004005], 12 | "datetime" => %{ 13 | "year" => "2020", 14 | "month" => "4", 15 | "day" => "17", 16 | "hour" => "2", 17 | "minute" => "11", 18 | "second" => "13" 19 | }, 20 | "naive_datetime" => ~N[2000-01-01 10:00:42] 21 | } 22 | end 23 | 24 | describe "form_for/4 with map" do 25 | test "with :as" do 26 | search_params = search_params() 27 | 28 | form = 29 | safe_to_string( 30 | form_for(search_params, "/", [as: :search], fn f -> 31 | assert f.impl == Phoenix.HTML.FormData.Map 32 | assert f.name == "search" 33 | assert f.source == search_params 34 | assert f.params["key"] == "value" 35 | "" 36 | end) 37 | ) 38 | 39 | assert form =~ ~s(
) 40 | end 41 | 42 | test "without :as" do 43 | form = 44 | safe_to_string( 45 | form_for(search_params(), "/", fn f -> 46 | text_input(f, :key) 47 | end) 48 | ) 49 | 50 | assert form =~ ~s() 51 | end 52 | 53 | test "with custom options" do 54 | form = 55 | safe_to_string( 56 | form_for(search_params(), "/", [as: :search, method: :put, multipart: true], fn f -> 57 | refute f.options[:name] 58 | assert f.options[:multipart] == true 59 | assert f.options[:method] == :put 60 | "" 61 | end) 62 | ) 63 | 64 | assert form =~ 65 | ~s() 66 | 67 | assert form =~ ~s() 68 | end 69 | 70 | test "is html safe" do 71 | form = safe_to_string(form_for(search_params(), "/", [as: :search], fn _ -> "<>" end)) 72 | assert form =~ ~s(<>
) 73 | end 74 | 75 | test "with type and validations" do 76 | form = 77 | safe_to_string( 78 | form_for(search_params(), "/", [as: :search], fn f -> 79 | assert input_type(f, :hello) == :text_input 80 | assert input_type(f, :email) == :email_input 81 | assert input_type(f, :search) == :search_input 82 | assert input_type(f, :password) == :password_input 83 | assert input_type(f, :special_url) == :url_input 84 | assert input_type(f, :number, %{"number" => :number_input}) == :number_input 85 | "" 86 | end) 87 | ) 88 | 89 | assert form =~ " 98 | for {field, {message, _}} <- f.errors do 99 | PhoenixHTMLHelpers.Tag.content_tag(:span, humanize(field) <> " " <> message, 100 | class: "errors" 101 | ) 102 | end 103 | end) 104 | ) 105 | 106 | assert form =~ ~s(Field error message!) 107 | end 108 | end 109 | 110 | defp conn do 111 | Plug.Test.conn(:get, "/foo", %{"search" => search_params()}) 112 | end 113 | 114 | def conn_form(fun, opts \\ [as: :search]) do 115 | mark = "--PLACEHOLDER--" 116 | 117 | contents = 118 | safe_to_string( 119 | form_for(conn(), "/", opts, fn f -> 120 | html_escape([mark, fun.(f), mark]) 121 | end) 122 | ) 123 | 124 | [_, inner, _] = String.split(contents, mark) 125 | inner 126 | end 127 | 128 | describe "form_for/4 with connection" do 129 | test "with :as" do 130 | conn = conn() 131 | 132 | form = 133 | safe_to_string( 134 | form_for(conn, "/", [as: :search], fn f -> 135 | assert f.impl == Phoenix.HTML.FormData.Plug.Conn 136 | assert f.name == "search" 137 | assert f.source == conn 138 | assert f.params["key"] == "value" 139 | "" 140 | end) 141 | ) 142 | 143 | assert form =~ ~s(
) 144 | end 145 | 146 | test "without :as" do 147 | form = 148 | safe_to_string( 149 | form_for(conn(), "/", fn f -> 150 | text_input(f, :key) 151 | end) 152 | ) 153 | 154 | assert form =~ ~s() 155 | end 156 | 157 | test "with custom options" do 158 | form = 159 | safe_to_string( 160 | form_for(conn(), "/", [as: :search, method: :put, multipart: true], fn f -> 161 | refute f.options[:name] 162 | assert f.options[:multipart] == true 163 | assert f.options[:method] == :put 164 | "" 165 | end) 166 | ) 167 | 168 | assert form =~ 169 | ~s() 170 | 171 | assert form =~ ~s() 172 | end 173 | 174 | test "is html safe" do 175 | form = safe_to_string(form_for(conn(), "/", [as: :search], fn _ -> "<>" end)) 176 | assert form =~ ~s(<>
) 177 | end 178 | 179 | test "with type and validations" do 180 | form = 181 | safe_to_string( 182 | form_for(conn(), "/", [as: :search], fn f -> 183 | assert input_type(f, :hello) == :text_input 184 | assert input_type(f, :email) == :email_input 185 | assert input_type(f, :search) == :search_input 186 | assert input_type(f, :password) == :password_input 187 | assert input_type(f, :special_url) == :url_input 188 | assert input_type(f, :number, %{"number" => :number_input}) == :number_input 189 | "" 190 | end) 191 | ) 192 | 193 | assert form =~ " 202 | for {field, {message, _}} <- f.errors do 203 | PhoenixHTMLHelpers.Tag.content_tag(:span, humanize(field) <> " " <> message, 204 | class: "errors" 205 | ) 206 | end 207 | end) 208 | ) 209 | 210 | assert form =~ ~s(Field error message!) 211 | end 212 | end 213 | 214 | describe "form_for/4 with atom" do 215 | test "without params" do 216 | form = 217 | safe_to_string( 218 | form_for(:search, "/", fn f -> 219 | assert f.impl == Phoenix.HTML.FormData.Atom 220 | assert f.name == "search" 221 | assert f.source == :search 222 | assert f.params == %{} 223 | "" 224 | end) 225 | ) 226 | 227 | assert form =~ ~s(
) 228 | end 229 | 230 | test "with params" do 231 | form = 232 | safe_to_string( 233 | form_for(:search, "/", [params: search_params()], fn f -> 234 | text_input(f, :key) 235 | end) 236 | ) 237 | 238 | assert form =~ ~s() 239 | end 240 | 241 | test "with custom options" do 242 | form = 243 | safe_to_string( 244 | form_for(:search, "/", [method: :put, multipart: true], fn f -> 245 | refute f.options[:name] 246 | assert f.options[:multipart] == true 247 | assert f.options[:method] == :put 248 | "" 249 | end) 250 | ) 251 | 252 | assert form =~ 253 | ~s() 254 | 255 | assert form =~ ~s() 256 | end 257 | 258 | test "is html safe" do 259 | form = safe_to_string(form_for(conn(), "/", [as: :search], fn _ -> "<>" end)) 260 | assert form =~ ~s(<>
) 261 | end 262 | 263 | test "with type and validations" do 264 | form = 265 | safe_to_string( 266 | form_for(:search, "/", [], fn f -> 267 | assert input_type(f, :hello) == :text_input 268 | assert input_type(f, :email) == :email_input 269 | assert input_type(f, :search) == :search_input 270 | assert input_type(f, :password) == :password_input 271 | assert input_type(f, :special_url) == :url_input 272 | assert input_type(f, :number, %{"number" => :number_input}) == :number_input 273 | "" 274 | end) 275 | ) 276 | 277 | assert form =~ " 286 | for {field, {message, _}} <- f.errors do 287 | PhoenixHTMLHelpers.Tag.content_tag(:span, humanize(field) <> " " <> message, 288 | class: "errors" 289 | ) 290 | end 291 | end) 292 | ) 293 | 294 | assert form =~ ~s(Field error message!) 295 | end 296 | 297 | test "with id prefix the form id in the input id" do 298 | form = 299 | safe_to_string( 300 | form_for(:search, "/", [params: search_params(), id: "form_id"], fn f -> 301 | text_input(f, :key) 302 | end) 303 | ) 304 | 305 | assert form =~ 306 | ~s() 307 | end 308 | end 309 | 310 | describe "inputs_for/4" do 311 | test "generate a new form builder for the given parameter" do 312 | form = 313 | form_for(%{}, "/", [as: :user], fn form -> 314 | inputs_for(form, :company, fn company_form -> 315 | text_input(company_form, :name) 316 | end) 317 | end) 318 | |> safe_to_string() 319 | 320 | assert form =~ ~s() 321 | end 322 | 323 | test "generate a new form builder with hidden inputs when they are present" do 324 | form = 325 | form_for(%{}, "/", [as: :user], fn form -> 326 | inputs_for(form, :company, [hidden: [id: 1]], fn company_form -> 327 | text_input(company_form, :name) 328 | end) 329 | end) 330 | |> safe_to_string() 331 | 332 | assert form =~ 333 | ~s(input id="user_company_id" name="user[company][id]" type="hidden" value="1">) 334 | 335 | assert form =~ ~s() 336 | end 337 | 338 | test "skip hidden inputs" do 339 | form = 340 | form_for(%{}, "/", [as: :user], fn form -> 341 | inputs_for(form, :company, [skip_hidden: true, hidden: [id: 1]], fn company_form -> 342 | text_input(company_form, :name) 343 | end) 344 | end) 345 | |> safe_to_string() 346 | 347 | refute form =~ 348 | ~s(input id="user_company_id" name="user[company][id]" type="hidden" value="1">) 349 | 350 | assert form =~ ~s() 351 | end 352 | end 353 | 354 | ## text_input/3 355 | 356 | test "text_input/3" do 357 | assert safe_to_string(text_input(:search, :key)) == 358 | ~s() 359 | 360 | assert safe_to_string( 361 | text_input(:search, :key, value: "foo", id: "key", name: "search[key][]") 362 | ) == ~s() 363 | end 364 | 365 | test "text_input/3 with form" do 366 | assert conn_form(&text_input(&1, :key)) == 367 | ~s() 368 | 369 | assert conn_form(&text_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == 370 | ~s() 371 | end 372 | 373 | test "text_input/3 with form and data" do 374 | assert conn_form(&text_input(put_in(&1.data[:key], "original"), :key)) == 375 | ~s() 376 | 377 | assert conn_form(&text_input(put_in(&1.data[:no_key], "original"), :no_key)) == 378 | ~s() 379 | end 380 | 381 | ## textarea/3 382 | 383 | test "textarea/3" do 384 | assert safe_to_string(textarea(:search, :key)) == 385 | ~s() 386 | 387 | assert safe_to_string(textarea(:search, :key)) == 388 | ~s() 389 | 390 | assert safe_to_string(textarea(:search, :key, id: "key", name: "search[key][]")) == 391 | ~s() 392 | end 393 | 394 | test "textarea/3 with form" do 395 | assert conn_form(&textarea(&1, :key)) == 396 | ~s() 397 | 398 | assert conn_form(&textarea(&1, :key, value: "foo", id: "key", name: "search[key][]")) == 399 | ~s() 400 | end 401 | 402 | test "textarea/3 with non-binary type" do 403 | assert conn_form(&textarea(&1, :key, value: :atom_value)) == 404 | ~s() 405 | end 406 | 407 | ## number_input/3 408 | 409 | test "number_input/3" do 410 | assert safe_to_string(number_input(:search, :key)) == 411 | ~s() 412 | 413 | assert safe_to_string( 414 | number_input(:search, :key, value: "foo", id: "key", name: "search[key][]") 415 | ) == ~s() 416 | end 417 | 418 | test "number_input/3 with form" do 419 | assert conn_form(&number_input(&1, :key)) == 420 | ~s() 421 | 422 | assert conn_form(&number_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == 423 | ~s() 424 | end 425 | 426 | ## hidden_input/3 427 | 428 | test "hidden_input/3" do 429 | assert safe_to_string(hidden_input(:search, :key)) == 430 | ~s() 431 | 432 | assert safe_to_string( 433 | hidden_input(:search, :key, value: "foo", id: "key", name: "search[key][]") 434 | ) == ~s() 435 | 436 | assert safe_to_string( 437 | hidden_input(:search, :key, value: true, id: "key", name: "search[key][]") 438 | ) == ~s() 439 | 440 | assert safe_to_string( 441 | hidden_input(:search, :key, value: false, id: "key", name: "search[key][]") 442 | ) == ~s() 443 | end 444 | 445 | test "hidden_input/3 with form" do 446 | assert conn_form(&hidden_input(&1, :key)) == 447 | ~s() 448 | 449 | assert conn_form(&hidden_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == 450 | ~s() 451 | end 452 | 453 | describe "hidden_inputs_for/1" do 454 | test "generates hidden fields from the given form" do 455 | form = %{Phoenix.HTML.FormData.to_form(conn(), []) | hidden: [id: 1]} 456 | 457 | assert hidden_inputs_for(form) == [hidden_input(form, :id, value: 1)] 458 | end 459 | 460 | test "generates hidden fields for lists from the given form" do 461 | form = %{Phoenix.HTML.FormData.to_form(conn(), []) | hidden: [field: ["a", "b", "c"]]} 462 | 463 | assert hidden_inputs_for(form) == 464 | [ 465 | hidden_input(form, :field, name: "field[]", id: "field_0", value: "a"), 466 | hidden_input(form, :field, name: "field[]", id: "field_1", value: "b"), 467 | hidden_input(form, :field, name: "field[]", id: "field_2", value: "c") 468 | ] 469 | end 470 | end 471 | 472 | ## email_input/3 473 | 474 | test "email_input/3" do 475 | assert safe_to_string(email_input(:search, :key)) == 476 | ~s() 477 | 478 | assert safe_to_string( 479 | email_input(:search, :key, value: "foo", id: "key", name: "search[key][]") 480 | ) == ~s() 481 | end 482 | 483 | test "email_input/3 with form" do 484 | assert conn_form(&email_input(&1, :key)) == 485 | ~s() 486 | 487 | assert conn_form(&email_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == 488 | ~s() 489 | end 490 | 491 | ## password_input/3 492 | 493 | test "password_input/3" do 494 | assert safe_to_string(password_input(:search, :key)) == 495 | ~s() 496 | 497 | assert safe_to_string( 498 | password_input(:search, :key, value: "foo", id: "key", name: "search[key][]") 499 | ) == ~s() 500 | end 501 | 502 | test "password_input/3 with form" do 503 | assert conn_form(&password_input(&1, :key)) == 504 | ~s() 505 | 506 | assert conn_form(&password_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == 507 | ~s() 508 | end 509 | 510 | ## file_input/3 511 | 512 | test "file_input/3" do 513 | assert safe_to_string(file_input(:search, :key)) == 514 | ~s() 515 | 516 | assert safe_to_string(file_input(:search, :key, id: "key", name: "search[key][]")) == 517 | ~s() 518 | 519 | assert safe_to_string(file_input(:search, :key, multiple: true)) == 520 | ~s() 521 | end 522 | 523 | test "file_input/3 with form" do 524 | assert_raise ArgumentError, fn -> 525 | conn_form(&file_input(&1, :key)) 526 | end 527 | 528 | assert conn_form(&file_input(&1, :key), multipart: true, as: :search) == 529 | ~s() 530 | end 531 | 532 | ## url_input/3 533 | 534 | test "url_input/3" do 535 | assert safe_to_string(url_input(:search, :key)) == 536 | ~s() 537 | 538 | assert safe_to_string( 539 | url_input(:search, :key, value: "foo", id: "key", name: "search[key][]") 540 | ) == ~s() 541 | end 542 | 543 | test "url_input/3 with form" do 544 | assert conn_form(&url_input(&1, :key)) == 545 | ~s() 546 | 547 | assert conn_form(&url_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == 548 | ~s() 549 | end 550 | 551 | ## search_input/3 552 | 553 | test "search_input/3" do 554 | assert safe_to_string(search_input(:search, :key)) == 555 | ~s() 556 | 557 | assert safe_to_string( 558 | search_input(:search, :key, value: "foo", id: "key", name: "search[key][]") 559 | ) == ~s() 560 | end 561 | 562 | test "search_input/3 with form" do 563 | assert conn_form(&search_input(&1, :key)) == 564 | ~s() 565 | 566 | assert conn_form(&search_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == 567 | ~s() 568 | end 569 | 570 | ## color_input/3 571 | 572 | test "color_input/3" do 573 | assert safe_to_string(color_input(:search, :key)) == 574 | ~s() 575 | 576 | assert safe_to_string( 577 | color_input(:search, :key, value: "#123456", id: "key", name: "search[key][]") 578 | ) == ~s() 579 | end 580 | 581 | test "color_input/3 with form" do 582 | assert conn_form(&color_input(&1, :key)) == 583 | ~s() 584 | 585 | assert conn_form(&color_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == 586 | ~s() 587 | end 588 | 589 | ## telephone_input/3 590 | 591 | test "telephone_input/3" do 592 | assert safe_to_string(telephone_input(:search, :key)) == 593 | ~s() 594 | 595 | assert safe_to_string( 596 | telephone_input(:search, :key, value: "foo", id: "key", name: "search[key][]") 597 | ) == ~s() 598 | end 599 | 600 | test "telephone_input/3 with form" do 601 | assert conn_form(&telephone_input(&1, :key)) == 602 | ~s() 603 | 604 | assert conn_form(&telephone_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == 605 | ~s() 606 | end 607 | 608 | ## range_input/3 609 | 610 | test "range_input/3" do 611 | assert safe_to_string(range_input(:search, :key)) == 612 | ~s() 613 | 614 | assert safe_to_string( 615 | range_input(:search, :key, value: "foo", id: "key", name: "search[key][]") 616 | ) == ~s() 617 | end 618 | 619 | test "range_input/3 with form" do 620 | assert conn_form(&range_input(&1, :key)) == 621 | ~s() 622 | 623 | assert conn_form(&range_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == 624 | ~s() 625 | end 626 | 627 | ## date_input/3 628 | 629 | test "date_input/3" do 630 | assert safe_to_string(date_input(:search, :key)) == 631 | ~s() 632 | 633 | assert safe_to_string( 634 | date_input(:search, :key, value: "foo", id: "key", name: "search[key][]") 635 | ) == ~s() 636 | 637 | assert safe_to_string( 638 | date_input(:search, :key, value: ~D[2017-09-21], id: "key", name: "search[key][]") 639 | ) == ~s() 640 | end 641 | 642 | test "date_input/3 with form" do 643 | assert conn_form(&date_input(&1, :key)) == 644 | ~s() 645 | 646 | assert conn_form(&date_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == 647 | ~s() 648 | 649 | assert conn_form( 650 | &date_input(&1, :key, value: ~D[2017-09-21], id: "key", name: "search[key][]") 651 | ) == ~s() 652 | end 653 | 654 | ## datetime_input/3 655 | 656 | test "datetime_local_input/3" do 657 | assert safe_to_string(datetime_local_input(:search, :key)) == 658 | ~s() 659 | 660 | assert conn_form(&datetime_local_input(&1, :naive_datetime)) == 661 | ~s() 662 | 663 | assert safe_to_string( 664 | datetime_local_input(:search, :key, value: "foo", id: "key", name: "search[key][]") 665 | ) == ~s() 666 | 667 | assert safe_to_string( 668 | datetime_local_input( 669 | :search, 670 | :key, 671 | value: ~N[2017-09-21 20:21:53], 672 | id: "key", 673 | name: "search[key][]" 674 | ) 675 | ) == 676 | ~s() 677 | end 678 | 679 | test "datetime_local_input/3 with %DateTime{}" do 680 | assert safe_to_string( 681 | datetime_local_input( 682 | :search, 683 | :key, 684 | value: DateTime.from_naive!(~N[2021-05-13 04:20:20.836851], "Etc/UTC"), 685 | id: "key", 686 | name: "search[key][]" 687 | ) 688 | ) == 689 | ~s() 690 | end 691 | 692 | test "datetime_local_input/3 with form" do 693 | assert conn_form(&datetime_local_input(&1, :key)) == 694 | ~s() 695 | 696 | assert conn_form( 697 | &datetime_local_input(&1, :key, value: "foo", id: "key", name: "search[key][]") 698 | ) == ~s() 699 | 700 | assert conn_form( 701 | &datetime_local_input( 702 | &1, 703 | :key, 704 | value: ~N[2017-09-21 20:21:53], 705 | id: "key", 706 | name: "search[key][]" 707 | ) 708 | ) == 709 | ~s() 710 | end 711 | 712 | ## time_input/3 713 | 714 | test "time_input/3" do 715 | assert safe_to_string(time_input(:search, :key)) == 716 | ~s() 717 | 718 | assert safe_to_string( 719 | time_input(:search, :key, value: "foo", id: "key", name: "search[key][]") 720 | ) == ~s() 721 | 722 | assert safe_to_string( 723 | time_input(:search, :key, value: ~T[23:00:07.001], id: "key", name: "search[key][]") 724 | ) == ~s() 725 | end 726 | 727 | test "time_input/3 with form" do 728 | assert conn_form(&time_input(&1, :key)) == 729 | ~s() 730 | 731 | assert conn_form(&time_input(&1, :time)) == 732 | ~s() 733 | 734 | assert conn_form(&time_input(&1, :time, precision: :second)) == 735 | ~s() 736 | 737 | assert conn_form(&time_input(&1, :time, precision: :millisecond)) == 738 | ~s() 739 | 740 | assert conn_form(&time_input(&1, :key, value: "foo", id: "key", name: "search[key][]")) == 741 | ~s() 742 | 743 | assert conn_form( 744 | &time_input(&1, :key, value: ~T[23:00:07.001], id: "key", name: "search[key][]") 745 | ) == ~s() 746 | end 747 | 748 | ## submit/2 749 | 750 | test "submit/2" do 751 | assert safe_to_string(submit("Submit")) == ~s() 752 | 753 | assert safe_to_string(submit("Submit", class: "btn")) == 754 | ~s() 755 | 756 | assert safe_to_string(submit([class: "btn"], do: "Submit")) == 757 | ~s() 758 | 759 | assert safe_to_string(submit(do: "Submit")) == ~s() 760 | 761 | assert safe_to_string(submit("")) == ~s() 762 | 763 | assert safe_to_string(submit("", class: "btn")) == 764 | ~s() 765 | 766 | assert safe_to_string(submit([class: "btn"], do: "")) == 767 | ~s() 768 | 769 | assert safe_to_string(submit(do: "")) == 770 | ~s() 771 | end 772 | 773 | ## reset/2 774 | 775 | test "reset/2" do 776 | assert safe_to_string(reset("Reset")) == ~s() 777 | 778 | assert safe_to_string(reset("Reset", class: "btn")) == 779 | ~s() 780 | end 781 | 782 | ## radio_button/4 783 | 784 | test "radio_button/4" do 785 | assert safe_to_string(radio_button(:search, :key, "admin")) == 786 | ~s() 787 | 788 | assert safe_to_string(radio_button(:search, :key, "admin", checked: true)) == 789 | ~s() 790 | 791 | assert safe_to_string(radio_button(:search, :key, "value with spaces")) == 792 | ~s() 793 | 794 | assert safe_to_string(radio_button(:search, :key, "F✓o]o%b+a'R")) == 795 | ~s() 796 | end 797 | 798 | test "radio_button/4 with form" do 799 | assert conn_form(&radio_button(&1, :key, :admin)) == 800 | ~s() 801 | 802 | assert conn_form(&radio_button(&1, :key, :value)) == 803 | ~s() 804 | 805 | assert conn_form(&radio_button(&1, :key, :value, checked: false)) == 806 | ~s() 807 | end 808 | 809 | ## checkbox/3 810 | 811 | test "checkbox/3" do 812 | assert safe_to_string(checkbox(:search, :key)) == 813 | ~s() <> 814 | ~s() 815 | 816 | assert safe_to_string(checkbox(:search, :key, value: "true")) == 817 | ~s() <> 818 | ~s() 819 | 820 | assert safe_to_string(checkbox(:search, :key, checked: true)) == 821 | ~s() <> 822 | ~s() 823 | 824 | assert safe_to_string(checkbox(:search, :key, checked: true, disabled: true)) == 825 | ~s() <> 826 | ~s() 827 | 828 | assert safe_to_string(checkbox(:search, :key, value: "true", checked: false)) == 829 | ~s() <> 830 | ~s() 831 | 832 | assert safe_to_string(checkbox(:search, :key, value: 0, checked_value: 1, unchecked_value: 0)) == 833 | ~s() <> 834 | ~s() 835 | 836 | assert safe_to_string(checkbox(:search, :key, value: 1, checked_value: 1, unchecked_value: 0)) == 837 | ~s() <> 838 | ~s() 839 | 840 | assert safe_to_string(checkbox(:search, :key, value: 1, hidden_input: false)) == 841 | ~s() 842 | 843 | # Mimick a field of type {:array, Ecto.Enum}, for which `field_value` returns an array of atoms: 844 | assert safe_to_string( 845 | checkbox(:search, :key, 846 | name: "search[key][]", 847 | value: [:a, :b], 848 | checked_value: "c", 849 | checked: false, 850 | hidden_input: false 851 | ) 852 | ) == 853 | ~s() 854 | end 855 | 856 | test "checkbox/3 with form" do 857 | assert conn_form(&checkbox(&1, :key)) == 858 | ~s() <> 859 | ~s() 860 | 861 | assert conn_form(&checkbox(&1, :key, value: true)) == 862 | ~s() <> 863 | ~s() 864 | 865 | assert conn_form(&checkbox(&1, :key, checked_value: :value, unchecked_value: :novalue)) == 866 | ~s() <> 867 | ~s() 868 | end 869 | 870 | # select/4 871 | 872 | test "select/4" do 873 | assert safe_to_string(select(:search, :key, ~w(foo bar))) == 874 | ~s() 877 | 878 | assert safe_to_string(select(:search, :key, Foo: "foo", Bar: "bar")) == 879 | ~s() 882 | 883 | assert safe_to_string( 884 | select(:search, :key, [ 885 | [key: "Foo", value: "foo"], 886 | [key: "Bar", value: "bar", disabled: true] 887 | ]) 888 | ) == 889 | ~s() 892 | 893 | assert safe_to_string( 894 | select(:search, :key, [Foo: "foo", Bar: "bar"], prompt: "Choose your destiny") 895 | ) == 896 | ~s() 900 | 901 | assert safe_to_string( 902 | select(:search, :key, [Foo: "foo", Bar: "bar"], 903 | prompt: [key: "Choose your destiny", disabled: true] 904 | ) 905 | ) == 906 | ~s() 910 | 911 | assert_raise ArgumentError, fn -> 912 | select(:search, :key, [Foo: "foo", Bar: "bar"], prompt: []) 913 | end 914 | 915 | assert safe_to_string(select(:search, :key, ~w(foo bar), value: "foo")) =~ 916 | ~s() 917 | 918 | assert safe_to_string(select(:search, :key, ~w(foo bar), selected: "foo")) =~ 919 | ~s() 920 | end 921 | 922 | test "select/4 with form" do 923 | assert conn_form(&select(&1, :key, ~w(value novalue), selected: "novalue")) == 924 | ~s() 927 | 928 | assert conn_form(&select(&1, :other, ~w(value novalue), selected: "novalue")) == 929 | ~s() 932 | 933 | assert conn_form( 934 | &select( 935 | &1, 936 | :key, 937 | [ 938 | [value: "value", key: "Value", disabled: true], 939 | [value: "novalue", key: "No Value"] 940 | ], 941 | selected: "novalue" 942 | ) 943 | ) == 944 | ~s() 947 | 948 | assert conn_form( 949 | &select( 950 | put_in(&1.data[:other], "value"), 951 | :other, 952 | ~w(value novalue), 953 | selected: "novalue" 954 | ) 955 | ) == 956 | ~s() 959 | 960 | assert conn_form(&select(&1, :key, ~w(value novalue), value: "novalue")) == 961 | ~s() 964 | end 965 | 966 | test "select/4 with groups" do 967 | assert conn_form( 968 | &select(&1, :key, [{"foo", ~w(bar baz)}, {"qux", ~w(qux quz)}], value: "qux") 969 | ) == 970 | ~s() 978 | 979 | assert conn_form( 980 | &select( 981 | &1, 982 | :key, 983 | [foo: [{"1", "One"}, {"2", "Two"}], qux: ~w(qux quz)], 984 | value: "qux" 985 | ) 986 | ) == 987 | ~s() 995 | 996 | assert conn_form( 997 | &select( 998 | &1, 999 | :key, 1000 | %{"foo" => %{"1" => "One", "2" => "Two"}, "qux" => ~w(qux quz)}, 1001 | value: "qux" 1002 | ) 1003 | ) == 1004 | ~s() 1012 | 1013 | assert conn_form( 1014 | &select( 1015 | &1, 1016 | :key, 1017 | %{"foo" => [{"1", "One"}, {"2", "Two"}], "qux" => ~w(qux quz)}, 1018 | value: "qux" 1019 | ) 1020 | ) == 1021 | ~s() 1029 | end 1030 | 1031 | # multiple_select/4 1032 | 1033 | test "multiple_select/4" do 1034 | assert safe_to_string(multiple_select(:search, :key, ~w(foo bar))) == 1035 | ~s() 1038 | 1039 | assert safe_to_string(multiple_select(:search, :key, [{"foo", 1}, {"bar", 2}])) == 1040 | ~s() 1043 | 1044 | assert safe_to_string(multiple_select(:search, :key, ~w(foo bar), value: ["foo"])) =~ 1045 | ~s() 1046 | 1047 | assert safe_to_string( 1048 | multiple_select(:search, :key, [{"foo", "1"}, {"bar", "2"}], value: [1]) 1049 | ) =~ ~s() 1050 | 1051 | assert safe_to_string(multiple_select(:search, :key, [{"foo", 1}, {"bar", 2}], selected: [1])) =~ 1052 | ~s() 1053 | 1054 | assert safe_to_string( 1055 | multiple_select(:search, :key, %{"foo" => [{"One", 1}, {"Two", 2}], "bar" => ~w(3 4)}) 1056 | ) == 1057 | ~s() 1065 | end 1066 | 1067 | test "multiple_select/4 with form" do 1068 | assert conn_form( 1069 | &multiple_select(&1, :key, [{"foo", 1}, {"bar", 2}], value: [1], selected: [2]) 1070 | ) == 1071 | ~s() 1074 | 1075 | assert conn_form(&multiple_select(&1, :other, [{"foo", 1}, {"bar", 2}], selected: [2])) == 1076 | ~s() 1079 | 1080 | assert conn_form(&multiple_select(&1, :key, [{"foo", 1}, {"bar", 2}], value: [2])) == 1081 | ~s() 1084 | 1085 | assert conn_form(&multiple_select(&1, :key, ~w(value novalue), value: ["novalue"])) == 1086 | ~s() 1089 | 1090 | assert conn_form( 1091 | &multiple_select( 1092 | put_in(&1.params["key"], ["3"]), 1093 | :key, 1094 | [{"foo", 1}, {"bar", 2}, {"goo", 3}], 1095 | selected: [2] 1096 | ) 1097 | ) == 1098 | ~s() 1102 | end 1103 | 1104 | test "multiple_select/4 with unnamed form" do 1105 | assert conn_form( 1106 | &multiple_select(&1, :key, [{"foo", 1}, {"bar", 2}], value: [1], selected: [2]), 1107 | [] 1108 | ) == 1109 | ~s() 1112 | 1113 | assert conn_form(&multiple_select(&1, :other, [{"foo", 1}, {"bar", 2}], selected: [2]), []) == 1114 | ~s() 1117 | 1118 | assert conn_form(&multiple_select(&1, :key, [{"foo", 1}, {"bar", 2}], value: [2]), []) == 1119 | ~s() 1122 | 1123 | assert conn_form(&multiple_select(&1, :key, ~w(value novalue), value: ["novalue"]), []) == 1124 | ~s() 1127 | 1128 | assert conn_form( 1129 | &multiple_select( 1130 | put_in(&1.params["key"], ["3"]), 1131 | :key, 1132 | [{"foo", 1}, {"bar", 2}, {"goo", 3}], 1133 | selected: [2] 1134 | ), 1135 | [] 1136 | ) == 1137 | ~s() 1141 | end 1142 | 1143 | # date_select/4 1144 | 1145 | test "date_select/4" do 1146 | content = safe_to_string(date_select(:search, :datetime)) 1147 | assert content =~ ~s() 1149 | assert content =~ ~s() <> 1188 | ~s() 1189 | 1190 | assert content =~ 1191 | ~s() <> 1196 | ~s() 1197 | end 1198 | 1199 | test "date_select/4 with form" do 1200 | content = conn_form(&date_select(&1, :datetime, default: {2020, 10, 13})) 1201 | assert content =~ ~s() 1203 | assert content =~ ~s() 1230 | assert content =~ ~s() 1232 | 1233 | content = safe_to_string(time_select(:search, :datetime, second: [])) 1234 | assert content =~ ~s() 1236 | assert content =~ ~s() <> 1275 | ~s() 1276 | 1277 | assert content =~ 1278 | ~s() <> 1283 | ~s() 1284 | end 1285 | 1286 | test "time_select/4 with form" do 1287 | content = conn_form(&time_select(&1, :datetime, default: {1, 2, 3}, second: [])) 1288 | assert content =~ ~s() 1290 | assert content =~ ~s() 1312 | assert content =~ ~s() 1314 | assert content =~ ~s() 1316 | refute content =~ ~s() 1320 | assert content =~ ~s() 1322 | assert content =~ ~s() 1324 | assert content =~ ~s() 1356 | assert content =~ ~s() 1358 | assert content =~ ~s() 1359 | assert content =~ ~s() 1360 | assert content =~ ~s() 1361 | 1362 | assert content =~ ~s() 1364 | assert content =~ ~s() 1434 | assert content =~ ~s(Month: ) 1436 | assert content =~ ~s(Hour: ) 1438 | assert content =~ ~s(Sec: ) 48 | end 49 | 50 | test "one: inputs_for/4 does not generate index" do 51 | safe_inputs_for(:unknown, fn f -> 52 | refute f.index 53 | "ok" 54 | end) 55 | end 56 | 57 | test "one: inputs_for/4 without default and field is present" do 58 | contents = 59 | safe_inputs_for(:date, fn f -> 60 | text_input(f, :year) 61 | end) 62 | 63 | assert contents == 64 | ~s() 65 | end 66 | 67 | test "one: inputs_for/4 with default and field is not present" do 68 | contents = 69 | safe_inputs_for(:unknown, [default: %{year: 2015}], fn f -> 70 | text_input(f, :year) 71 | end) 72 | 73 | assert contents == 74 | ~s() 75 | end 76 | 77 | test "one: inputs_for/4 with default and field is present" do 78 | contents = 79 | safe_inputs_for(:date, [default: %{year: 2015}], fn f -> 80 | text_input(f, :year) 81 | end) 82 | 83 | assert contents == 84 | ~s() 85 | end 86 | 87 | test "one: inputs_for/4 with custom name and id" do 88 | contents = 89 | safe_inputs_for(:date, [as: :foo, id: :bar], fn f -> 90 | text_input(f, :year) 91 | end) 92 | 93 | assert contents == ~s() 94 | end 95 | 96 | ## Cardinality many 97 | 98 | test "many: inputs_for/4 with file field generates file input" do 99 | contents = 100 | safe_inputs_for(:unknown, [default: [%{}, %{}], multipart: true], fn f -> 101 | assert f.index in [0, 1] 102 | file_input(f, :file) 103 | end) 104 | 105 | assert contents == 106 | ~s() <> 107 | ~s() 108 | end 109 | 110 | test "many: inputs_for/4 with default and field is not present" do 111 | contents = 112 | safe_inputs_for(:unknown, [default: [%{year: 2012}, %{year: 2018}]], fn f -> 113 | assert f.index in [0, 1] 114 | text_input(f, :year) 115 | end) 116 | 117 | assert contents == 118 | ~s() <> 119 | ~s() 120 | end 121 | 122 | test "many: inputs_for/4 generates indexes" do 123 | safe_inputs_for(:unknown, [default: [%{year: 2012}]], fn f -> 124 | assert f.index == 0 125 | "ok" 126 | end) 127 | 128 | safe_inputs_for(:unknown, [default: [%{year: 2012}, %{year: 2018}]], fn f -> 129 | assert f.index in [0, 1] 130 | "ok" 131 | end) 132 | end 133 | 134 | test "many: inputs_for/4 with default and field is present" do 135 | contents = 136 | safe_inputs_for(:dates, [default: [%{year: 2012}, %{year: 2018}]], fn f -> 137 | text_input(f, :year) 138 | end) 139 | 140 | assert contents == 141 | ~s() <> 142 | ~s() 143 | end 144 | 145 | test "many: inputs_for/4 with name and id" do 146 | contents = 147 | safe_inputs_for( 148 | :dates, 149 | [default: [%{year: 2012}, %{year: 2018}], as: :foo, id: :bar], 150 | fn f -> 151 | text_input(f, :year) 152 | end 153 | ) 154 | 155 | assert contents == 156 | ~s() <> 157 | ~s() 158 | end 159 | 160 | @prepend_append [ 161 | prepend: [%{year: 2008}], 162 | append: [%{year: 2022}], 163 | default: [%{year: 2012}, %{year: 2018}] 164 | ] 165 | 166 | test "many: inputs_for/4 with prepend/append and field is not present" do 167 | contents = 168 | safe_inputs_for(:unknown, @prepend_append, fn f -> 169 | text_input(f, :year) 170 | end) 171 | 172 | assert contents == 173 | ~s() <> 174 | ~s() <> 175 | ~s() <> 176 | ~s() 177 | end 178 | 179 | test "many: inputs_for/4 with prepend/append and field is present" do 180 | contents = 181 | safe_inputs_for(:dates, @prepend_append, fn f -> 182 | text_input(f, :year) 183 | end) 184 | 185 | assert contents == 186 | ~s() <> 187 | ~s() 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /test/phoenix_html_helpers/link_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHTMLHelpers.LinkTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Phoenix.HTML 5 | import PhoenixHTMLHelpers.Link 6 | 7 | test "link with post" do 8 | csrf_token = Plug.CSRFProtection.get_csrf_token() 9 | 10 | assert safe_to_string(link("hello", to: "/world", method: :post)) == 11 | ~s[hello] 12 | end 13 | 14 | test "link with %URI{}" do 15 | url = "https://elixir-lang.org/" 16 | 17 | assert safe_to_string(link("elixir", to: url)) == 18 | safe_to_string(link("elixir", to: URI.parse(url))) 19 | 20 | path = "/elixir" 21 | 22 | assert safe_to_string(link("elixir", to: path)) == 23 | safe_to_string(link("elixir", to: URI.parse(path))) 24 | end 25 | 26 | test "link with put/delete" do 27 | csrf_token = Plug.CSRFProtection.get_csrf_token() 28 | 29 | assert safe_to_string(link("hello", to: "/world", method: :put)) == 30 | ~s[hello] 31 | end 32 | 33 | test "link with put/delete without csrf_token" do 34 | assert safe_to_string(link("hello", to: "/world", method: :put, csrf_token: false)) == 35 | ~s[hello] 36 | end 37 | 38 | test "link with :do contents" do 39 | assert ~s[

world

] == 40 | safe_to_string( 41 | link to: "/hello" do 42 | PhoenixHTMLHelpers.Tag.content_tag(:p, "world") 43 | end 44 | ) 45 | 46 | assert safe_to_string( 47 | link(to: "/hello") do 48 | "world" 49 | end 50 | ) == ~s[world] 51 | end 52 | 53 | test "link with scheme" do 54 | assert safe_to_string(link("foo", to: "/javascript:alert(<1>)")) == 55 | ~s[foo] 56 | 57 | assert safe_to_string(link("foo", to: {:safe, "/javascript:alert(<1>)"})) == 58 | ~s[foo] 59 | 60 | assert safe_to_string(link("foo", to: {:javascript, "alert(<1>)"})) == 61 | ~s[foo] 62 | 63 | assert safe_to_string(link("foo", to: {:javascript, ~c"alert(<1>)"})) == 64 | ~s[foo] 65 | 66 | assert safe_to_string(link("foo", to: {:javascript, {:safe, "alert(<1>)"}})) == 67 | ~s[foo] 68 | 69 | assert safe_to_string(link("foo", to: {:javascript, {:safe, ~c"alert(<1>)"}})) == 70 | ~s[foo] 71 | end 72 | 73 | test "link with invalid args" do 74 | msg = "expected non-nil value for :to in link/2" 75 | 76 | assert_raise ArgumentError, msg, fn -> 77 | link("foo", bar: "baz") 78 | end 79 | 80 | msg = "link/2 requires a keyword list as second argument" 81 | 82 | assert_raise ArgumentError, msg, fn -> 83 | link("foo", "/login") 84 | end 85 | 86 | assert_raise ArgumentError, ~r"unsupported scheme given as link", fn -> 87 | link("foo", to: "javascript:alert(1)") 88 | end 89 | 90 | assert_raise ArgumentError, ~r"unsupported scheme given as link", fn -> 91 | link("foo", to: {:safe, "javascript:alert(1)"}) 92 | end 93 | 94 | assert_raise ArgumentError, ~r"unsupported scheme given as link", fn -> 95 | link("foo", to: {:safe, ~c"javascript:alert(1)"}) 96 | end 97 | end 98 | 99 | test "button with post (default)" do 100 | csrf_token = Plug.CSRFProtection.get_csrf_token() 101 | 102 | assert safe_to_string(button("hello", to: "/world")) == 103 | ~s[] 104 | end 105 | 106 | test "button with %URI{}" do 107 | url = "https://elixir-lang.org/" 108 | 109 | assert safe_to_string(button("elixir", to: url, csrf_token: false)) == 110 | safe_to_string(button("elixir", to: URI.parse(url), csrf_token: false)) 111 | end 112 | 113 | test "button with post without csrf_token" do 114 | assert safe_to_string(button("hello", to: "/world", csrf_token: false)) == 115 | ~s[] 116 | end 117 | 118 | test "button with get does not generate CSRF" do 119 | assert safe_to_string(button("hello", to: "/world", method: :get)) == 120 | ~s[] 121 | end 122 | 123 | test "button with do" do 124 | csrf_token = Plug.CSRFProtection.get_csrf_token() 125 | 126 | output = 127 | safe_to_string( 128 | button to: "/world", class: "small" do 129 | raw("Hi") 130 | end 131 | ) 132 | 133 | assert output == 134 | ~s[] 135 | end 136 | 137 | test "button with class overrides default" do 138 | csrf_token = Plug.CSRFProtection.get_csrf_token() 139 | 140 | assert safe_to_string(button("hello", to: "/world", class: "btn rounded", id: "btn")) == 141 | ~s[] 142 | end 143 | 144 | test "button with invalid args" do 145 | assert_raise ArgumentError, ~r/unsupported scheme given as link/, fn -> 146 | button("foo", to: "javascript:alert(1)", method: :get) 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /test/phoenix_html_helpers/tag_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixHTMLHelpers.TagTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Phoenix.HTML 5 | import PhoenixHTMLHelpers.Tag 6 | doctest PhoenixHTMLHelpers.Tag 7 | 8 | test "tag" do 9 | assert tag(:br) |> safe_to_string() == ~s(
) 10 | 11 | assert tag(:input, name: ~s("<3")) |> safe_to_string() == ~s() 12 | assert tag(:input, name: raw("<3")) |> safe_to_string() == ~s() 13 | assert tag(:input, name: ["foo", raw("b safe_to_string() == ~s() 14 | assert tag(:input, name: :hello) |> safe_to_string() == ~s() 15 | 16 | assert tag(:input, type: "text", name: "user_id") |> safe_to_string() == 17 | ~s() 18 | 19 | assert tag(:input, data: [toggle: "dropdown"]) |> safe_to_string() == 20 | ~s() 21 | 22 | assert tag(:input, my_attr: "blah") |> safe_to_string() == ~s() 23 | 24 | assert tag(:input, [{"my_<_attr", "blah"}]) |> safe_to_string() == 25 | ~s() 26 | 27 | assert tag(:input, [{{:safe, "my_<_attr"}, "blah"}]) |> safe_to_string() == 28 | ~s() 29 | 30 | assert tag(:input, data: [my_attr: "blah"]) |> safe_to_string() == 31 | ~s() 32 | 33 | assert tag(:input, data: [toggle: [attr: "blah", target: "#parent"]]) |> safe_to_string() == 34 | ~s() 35 | 36 | assert tag(:audio, autoplay: "autoplay") |> safe_to_string() == 37 | ~s(