├── .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 | [](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 `