├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib ├── phoenix_html.ex └── phoenix_html │ ├── engine.ex │ ├── form.ex │ ├── form_data.ex │ ├── form_field.ex │ └── safe.ex ├── mix.exs ├── mix.lock ├── package.json ├── priv └── static │ └── phoenix_html.js └── test ├── phoenix_html ├── engine_test.exs ├── form_test.exs └── safe_test.exs ├── phoenix_html_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.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.12 17 | otp: 24.3 18 | - elixir: 1.18 19 | otp: 27.2 20 | lint: lint 21 | 22 | runs-on: ubuntu-22.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@v4 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 | /_build 2 | /cover 3 | /node_modules 4 | /deps 5 | /doc 6 | erl_crash.dump 7 | *.ez 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.2.1 (2025-02-21) 4 | 5 | * Enhancements 6 | * Add type to `Phoenix.HTML.FormField` 7 | * Allow keyword lists in options to use nil as key/value 8 | 9 | ## 4.2.0 (2024-12-28) 10 | 11 | * Enhancements 12 | * Add `Phoenix.HTML.css_escape/1` to escape strings for use inside CSS selectors 13 | * Add the ability to pass `:hr` to `options_for_select/2` to render a horizontal rule 14 | 15 | * Bug fixes 16 | * Pass form action through in FormData implementation 17 | 18 | ## v4.1.1 (2024-03-01) 19 | * Fix dependency resolution error 20 | 21 | ## v4.1.0 (2024-02-29) 22 | 23 | * Enhancements 24 | * Introduce form `:action` and consider input as changed if action changes to support better change tracking 25 | 26 | ## v4.0.0 (2023-12-19) 27 | 28 | This version removes deprecated functionality and moved all HTML helpers to a separate library. HTML Helpers are no longer used in new apps from Phoenix v1.7, instead it relies on function components from `Phoenix.LiveView`. Older applications who wish to maintain compatibility, add `{:phoenix_html_helpers, "~> 1.0"}` to your `mix.exs` and then replace `use Phoenix.HTML` in your applications by: 29 | 30 | ```elixir 31 | import Phoenix.HTML 32 | import Phoenix.HTML.Form 33 | use PhoenixHTMLHelpers 34 | ``` 35 | 36 | ## v3.3.3 (2023-10-09) 37 | 38 | * Enhancements 39 | * Allow string fields on `input_changed?` 40 | 41 | ## v3.3.2 (2023-08-10) 42 | 43 | * Enhancements 44 | * Address deprecations in Elixir v1.16+ 45 | 46 | * Deprecations 47 | * Deprecate `inputs_for/2` and `inputs_for/3` (without anonymous functions) 48 | 49 | ## v3.3.1 (2023-02-27) 50 | 51 | * Bug fix 52 | * Set display to none on generated forms 53 | * Warn for maps with atom keys 54 | 55 | ## v3.3.0 (2023-02-10) 56 | 57 | * Enhancements 58 | * Support deeply nested class lists 59 | * Implement Phoenix.HTML.Safe for URI 60 | * Implement Phoenix.HTML.FormData for Map 61 | 62 | * Bug fix 63 | * Generate unique IDs for checkboxes based on the value 64 | * Use artificial button click instead of `form.submit` in JavaScript to trigger all relevant events 65 | * Fix a bug where nil/false/true attributes in `aria`/`data`/`phx` would emit empty or literal values, such as `"true"` and `"false"`. This release aligns them with all other attributes so both `nil` and `false` emit nothing. `true` emits the attribute with no value. 66 | 67 | * Deprecations 68 | * `Phoenix.HTML.Tag.attributes_escape/1` is deprecated in favor of `Phoenix.HTML.attributes_escape/1` 69 | 70 | ## v3.2.0 (2021-12-18) 71 | 72 | * Enhancements 73 | * Raise if the `id` attribute is set to a number. This is actually an invalid value according to the HTML spec and it can lead to problematic client behaviour, especially in LiveView and other client frameworks. 74 | * Allow `phx` attributes to be nested, similar to `aria` and `data` attributes 75 | * Allow hidden fields in forms to be a list of values 76 | 77 | ## v3.1.0 (2021-10-23) 78 | 79 | * Bug fix 80 | * Do not submit data-method links if default has been prevented 81 | * Deprecations 82 | * Deprecate `~E` and `Phoenix.HTML.Tag.attributes_escape/1` 83 | * Remove deprecated `Phoenix.HTML.Link.link/1` 84 | 85 | ## v3.0.4 (2021-09-23) 86 | 87 | * Bug fix 88 | * Ensure `class={@class}` in HEEx templates and `:class` attribute in `content_tag` are properly escaped against XSS 89 | 90 | ## v3.0.3 (2021-09-04) 91 | 92 | * Bug fix 93 | * Fix sorting of attributes in `tag`/`content_tag` 94 | 95 | ## v3.0.2 (2021-08-19) 96 | 97 | * Enhancements 98 | * Support maps on `Phoenix.HTML.Tag.attributes_escape/1` 99 | 100 | ## v3.0.1 (2021-08-14) 101 | 102 | * Enhancements 103 | * Add `Phoenix.HTML.Tag.csrf_input_tag/2` 104 | 105 | ## v3.0.0 (2021-08-06) 106 | 107 | * Enhancements 108 | * Allow extra html attributes on the `:prompt` option in `select` 109 | * Make `Plug` an optional dependency 110 | * Prefix form id on inputs when it is given to `form_for/3` 111 | * Allow `%URI{}` to be passed to `link/2` and `button/2` as `:to` 112 | * Expose `Phoenix.HTML.Tag.csrf_token_value/1` 113 | * Add `Phoenix.HTML.Tag.attributes_escape/1` 114 | 115 | * Bug fixes 116 | * Honor the `form` attribute when creating hidden checkbox input 117 | * Use `to_iso8601` as the standard implementation for safe dates and times 118 | 119 | * Deprecations 120 | * `form_for` without an anonymous function has been deprecated. v3.0 has deprecated the usage, v3.1 will emit warnings, and v3.2 will fully remove the functionality 121 | 122 | * Backwards incompatible changes 123 | * Strings given as attributes keys in `tag` and `content_tag` are now emitted as is (without being dasherized) and are also HTML escaped 124 | * Prefix form id on inputs when it is given to `form_for/3` 125 | * By default dates and times will format to the `to_iso8601` functions provided by their implementation 126 | * Do not include `csrf-param` and `method-param` in generated `csrf_meta_tag` 127 | * Remove deprecated `escape_javascript` in favor of `javascript_escape` 128 | * Remove deprecated `field_value` in favor of `input_value` 129 | * Remove deprecated `field_name` in favor of `input_name` 130 | * Remove deprecated `field_id` in favor of `input_id` 131 | 132 | ## v2.14.3 (2020-12-12) 133 | 134 | * Bug fixes 135 | * Fix warnings on Elixir v1.12 136 | 137 | ## v2.14.2 (2020-04-30) 138 | 139 | * Deprecations 140 | * Deprecate `Phoenix`-specific assigns `:view_module` and `:view_template` 141 | 142 | ## v2.14.1 (2020-03-20) 143 | 144 | * Enhancements 145 | * Add `Phoenix.HTML.Form.options_for_select/2` 146 | * Add `Phoenix.HTML.Form.inputs_for/3` 147 | 148 | * Bug fixes 149 | * Disable hidden input for disabled checkboxes 150 | 151 | ## v2.14.0 (2020-01-28) 152 | 153 | * Enhancements 154 | * Remove enforce_utf8 workaround on forms as it is no longer required by browser 155 | * Remove support tuple-based date/time with microseconds calendar types 156 | * Allow strings as first element in `content_tag` 157 | * Add `:srcset` support to `img_tag` 158 | * Allow `inputs_for` to skip hidden fields 159 | 160 | ## v2.13.4 (2020-01-28) 161 | 162 | * Bug fixes 163 | * Fix invalid :line in Elixir v1.10.0 164 | 165 | ## v2.13.3 (2019-05-31) 166 | 167 | * Enhancements 168 | * Add atom support to FormData 169 | 170 | * Bug fixes 171 | * Keep proper line numbers on .eex templates for proper coverage 172 | 173 | ## v2.13.2 (2019-03-29) 174 | 175 | * Bug fixes 176 | * Stop event propagation when confirm dialog is canceled 177 | 178 | ## v2.13.1 (2019-01-05) 179 | 180 | * Enhancements 181 | * Allow safe content to be given to label 182 | * Also escale template literals in `javascript_escape/1` 183 | 184 | * Bug fixes 185 | * Fix deprecation warnings to point to the correct alternative 186 | 187 | ## v2.13.0 (2018-12-09) 188 | 189 | * Enhancements 190 | * Require Elixir v1.5+ for more efficient template compilation/rendering 191 | * Add `Phoenix.HTML.Engine.encode_to_iodata!/1` 192 | * Add `Phoenix.HTML.Form.form_for/3` that works without an anonymous function 193 | 194 | * Deprecations 195 | * Deprecate `Phoenix.HTML.escape_javascript/1` in favor of `Phoenix.HTML.javascript_escape/1` for consistency 196 | 197 | ## v2.12.0 (2018-08-06) 198 | 199 | * Enhancements 200 | * Configurable and extendable data-confirm behaviour 201 | * Allow data-confirm with submit buttons 202 | * Support ISO 8601 formatted strings for date and time values 203 | 204 | * Bug fixes 205 | * Provide a default id of the field name for `@conn` based forms 206 | 207 | ## v2.11.2 (2018-04-13) 208 | 209 | * Enhancements 210 | * Support custom precision on time input 211 | 212 | * Bug fixes 213 | * Do not raise when `:` is part of a path on link/button attributes 214 | 215 | ## v2.11.1 (2018-03-20) 216 | 217 | * Enhancements 218 | * Add `label/1` 219 | * Copy the target attribute of the link in the generated JS form 220 | 221 | * Bug fixes 222 | * Support any value that is html escapable in `radio_button` 223 | 224 | ## v2.11.0 (2018-03-09) 225 | 226 | * Enhancements 227 | * Add date, datetime-local and time input types 228 | * Enable string keys to be usable with forms 229 | * Support carriage return in `text_to_html` 230 | * Add support for HTML5 boolean attributes to `content_tag` and `tag` 231 | * Improve performance by relying on `html_safe_to_iodata/1` 232 | * Protect against CSRF tokens leaking across hosts when the POST URL is dynamic 233 | * Require `to` attribute in links and buttons to explicitly pass protocols as a separate option for safety reasons 234 | 235 | * Bug fixes 236 | * Guarantee `input_name/2` always returns strings 237 | * Improve handling of uncommon whitespace and null in `escape_javascript` 238 | * Escape value attribute so it is never treated as a boolean 239 | 240 | * Backwards incompatible changes 241 | * The :csrf_token_generator configuration in the Phoenix.HTML app no longer works due to the improved security mechanisms 242 | 243 | ## v2.10.5 (2017-11-08) 244 | 245 | * Enhancements 246 | * Do not require the :as option in form_for 247 | 248 | ## v2.10.4 (2017-08-15) 249 | 250 | * Bug fixes 251 | * Fix formatting of days in datetime_builder 252 | 253 | ## v2.10.3 (2017-07-30) 254 | 255 | * Enhancements 256 | * Allow specifying a custom CSRF token generator 257 | 258 | * Bug fixes 259 | * Do not submit `method: :get` in buttons as "post" 260 | 261 | ## v2.10.2 (2017-07-24) 262 | 263 | * Bug fixes 264 | * Traverse DOM elements up when handling data-method 265 | 266 | ## v2.10.1 (2017-07-22) 267 | 268 | * Bug fixes 269 | * Only generate CSRF token if necessary 270 | 271 | ## v2.10.0 (2017-07-21) 272 | 273 | * Enhancements 274 | * Support custom attributes in options in select 275 | 276 | * Bug fixes 277 | * Accept non-binary values in textarea's content 278 | * Allow nested forms on the javascript side. This means `link` and `button` no longer generate a child form such as the `:form` option has no effect and "data-submit=parent" is no longer supported. Instead "data-to" and "data-method" are set on the entities and the form is generated on the javascript side of things 279 | 280 | ## v2.9.3 (2016-12-24) 281 | 282 | * Bug fixes 283 | * Once again support any name for atom forms 284 | 285 | ## v2.9.2 (2016-12-24) 286 | 287 | * Bug fixes 288 | * Always read from `form.params` and then from `:selected` in `select` and `multiple_select` before falling back to `input_value/2` 289 | 290 | ## v2.9.1 (2016-12-20) 291 | 292 | * Bug fixes 293 | * Implement proper `input_value/3` callback 294 | 295 | ## v2.9.0 (2016-12-19) 296 | 297 | * Enhancements 298 | * Add `img_tag/2` helper to `Phoenix.HTML.Tag` 299 | * Submit nearest form even if not direct descendent 300 | * Use more iodata for `tag/2` and `content_tag/3` 301 | * Add `input_value/3`, `input_id/2` and `input_name/2` as a unified API around the input (alongside `input_type/3` and `input_validations/2`) 302 | 303 | ## v2.8.0 (2016-11-15) 304 | 305 | * Enhancements 306 | * Add `csrf_meta_tag/0` helper to `Phoenix.HTML.Tag` 307 | * Allow passing a `do:` option to `Phoenix.HTML.Link.button/2` 308 | 309 | ## v2.7.0 (2016-09-21) 310 | 311 | * Enhancements 312 | * Render button tags for form submits and in the `button/2` function 313 | * Allow `submit/2` and `button/2` to receive `do` blocks 314 | * Support the `:multiple` option in `file_input/3` 315 | * Remove previously deprecated and unused `model` field 316 | 317 | ## v2.6.1 (2016-07-08) 318 | 319 | * Enhancements 320 | * Remove warnings on v1.4 321 | 322 | * Bug fixes 323 | * Ensure some contents are properly escaped as an integer 324 | * Ensure JavaScript data-submit events bubble up until it finds the proper parent 325 | 326 | ## v2.6.0 (2016-06-16) 327 | 328 | * Enhancements 329 | * Raise helpful error when using invalid iodata 330 | * Inline date/time API with Elixir v1.3 Calendar types 331 | * Add `:insert_brs` option to `text_to_html/2` 332 | * Run on Erlang 19 without warnings 333 | 334 | * Client-side changes 335 | * Use event delegation in `phoenix_html.js` 336 | * Drop IE8 support on `phoenix_html.js` 337 | 338 | * Backwards incompatible changes 339 | * `:min`, `:sec` option in `Phoenix.HTML.Form` (`datetime_select/3` and `time_select/3`) are no longer supported. Use `:minute` or `:second` instead. 340 | 341 | ## v2.5.1 (2016-03-12) 342 | 343 | * Bug fixes 344 | * Ensure multipart files work with inputs_for 345 | 346 | ## v2.5.0 (2016-01-28) 347 | 348 | * Enhancements 349 | * Introduce `form.data` field instead of `form.model`. Currently those values are kept in sync then the form is built but `form.model` will be deprecated in the long term 350 | 351 | ## v2.4.0 (2016-01-21) 352 | 353 | * Enhancements 354 | * Add `rel=nofollow` auto generation for non-get links 355 | * Introduce `:selected` option for `select` and `multiple_select` 356 | 357 | * Bug fixes 358 | * Fix safe engine incorrectly marking safe code as unsafe when last expression is `<% ... %>` 359 | 360 | ## v2.3.0 (2015-12-16) 361 | 362 | * Enhancements 363 | * Add `escape_javascript/1` 364 | * Add helpful error message when using unknown `@inner` assign 365 | * Add `Phoenix.HTML.Format.text_to_html/2` 366 | 367 | ## v2.2.0 (2015-09-01) 368 | 369 | * Bug fix 370 | * Allow the `:name` to be given in forms. For this, using `:name` to configure the underlying input name prefix has been deprecated in favor of `:as` 371 | 372 | ## v2.1.2 (2015-08-22) 373 | 374 | * Bug fix 375 | * Do not include values in `password_input/3` 376 | 377 | ## v2.1.1 (2015-08-15) 378 | 379 | * Enhancements 380 | * Allow nil in `raw/1` 381 | * Allow block options in `label/3` 382 | * Introduce `:skip_deleted` in `inputs_for/4` 383 | 384 | ## v2.1.0 (2015-08-06) 385 | 386 | * Enhancements 387 | * Add an index field to forms to be used by `inputs_for/4` collections 388 | 389 | ## v2.0.1 (2015-07-31) 390 | 391 | * Bug fix 392 | * Include web directory in Hex package 393 | 394 | ## v2.0.0 (2015-07-30) 395 | 396 | * Enhancements 397 | * No longer generate onclick attributes. 398 | 399 | The main motivation for this is to provide support 400 | for Content Security Policy, which recommends 401 | disabling all inline scripts in a page. 402 | 403 | We took the opportunity to also add support for 404 | data-confirm in `link/2`. 405 | 406 | ## v1.4.0 (2015-07-26) 407 | 408 | * Enhancements 409 | * Support `input_type/2` and `input_validations/2` as reflection mechanisms 410 | 411 | ## v1.3.0 (2015-07-23) 412 | 413 | * Enhancements 414 | * Add `Phoenix.HTML.Form.inputs_for/4` support 415 | * Add multiple select support 416 | * Add reset input 417 | * Infer default text context for labels 418 | 419 | ## v1.2.1 (2015-06-02) 420 | 421 | * Bug fix 422 | * Ensure nil parameters are not discarded when rendering input 423 | 424 | ## v1.2.0 (2015-05-30) 425 | 426 | * Enhancements 427 | * Add `label/3` for generating a label tag within a form 428 | 429 | ## v1.1.0 (2015-05-20) 430 | 431 | * Enhancements 432 | * Allow do/end syntax with `link/2` 433 | * Raise on missing assigns 434 | 435 | ## v1.0.1 436 | 437 | * Bug fixes 438 | * Avoid variable clash in Phoenix.HTML engine buffers 439 | 440 | ## v1.0.0 441 | 442 | * Enhancements 443 | * Provides an EEx engine with HTML safe rendering 444 | * Provides a `Phoenix.HTML.Safe` protocol 445 | * Provides a `Phoenix.HTML.FormData` protocol 446 | * Provides functions for generating tags, links and form builders in a safe way 447 | -------------------------------------------------------------------------------- /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 | # Phoenix.HTML 2 | 3 | [![Build Status](https://github.com/phoenixframework/phoenix_html/workflows/Tests/badge.svg)](https://github.com/phoenixframework/phoenix_html/actions?query=workflow%3ATests) 4 | 5 | Building blocks for working with HTML in Phoenix. 6 | 7 | This library provides three main functionalities: 8 | 9 | * HTML safety 10 | * Form abstractions 11 | * A tiny JavaScript library to enhance applications 12 | 13 | See the [docs](https://hexdocs.pm/phoenix_html/) for more information. 14 | 15 | ## License 16 | 17 | Copyright (c) 2014 Chris McCord 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining 20 | a copy of this software and associated documentation files (the 21 | "Software"), to deal in the Software without restriction, including 22 | without limitation the rights to use, copy, modify, merge, publish, 23 | distribute, sublicense, and/or sell copies of the Software, and to 24 | permit persons to whom the Software is furnished to do so, subject to 25 | the following conditions: 26 | 27 | The above copyright notice and this permission notice shall be 28 | included in all copies or substantial portions of the Software. 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 31 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 32 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 33 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 34 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 35 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 36 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 37 | -------------------------------------------------------------------------------- /lib/phoenix_html.ex: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.HTML do 2 | @moduledoc """ 3 | Building blocks for working with HTML in Phoenix. 4 | 5 | This library provides three main functionalities: 6 | 7 | * HTML safety 8 | * Form abstractions 9 | * A tiny JavaScript library to enhance applications 10 | 11 | ## HTML safety 12 | 13 | One of the main responsibilities of this package is to 14 | provide convenience functions for escaping and marking 15 | HTML code as safe. 16 | 17 | By default, data output in templates is not considered 18 | safe: 19 | 20 | ```heex 21 | <%= "" %> 22 | ``` 23 | 24 | will be shown as: 25 | 26 | ```html 27 | <hello> 28 | ``` 29 | 30 | User data or data coming from the database is almost never 31 | considered safe. However, in some cases, you may want to tag 32 | it as safe and show its "raw" contents: 33 | 34 | ```heex 35 | <%= raw "" %> 36 | ``` 37 | 38 | ## Form handling 39 | 40 | See `Phoenix.HTML.Form`. 41 | 42 | ## JavaScript library 43 | 44 | This project ships with a tiny bit of JavaScript that listens 45 | to all click events to: 46 | 47 | * Support `data-confirm="message"` attributes, which shows 48 | a confirmation modal with the given message 49 | 50 | * Support `data-method="patch|post|put|delete"` attributes, 51 | which sends the current click as a PATCH/POST/PUT/DELETE 52 | HTTP request. You will need to add `data-to` with the URL 53 | and `data-csrf` with the CSRF token value 54 | 55 | * Dispatch a "phoenix.link.click" event. You can listen to this 56 | event to customize the behaviour above. Returning false from 57 | this event will disable `data-method`. Stopping propagation 58 | will disable `data-confirm` 59 | 60 | To use the functionality above, you must load `priv/static/phoenix_html.js` 61 | into your build tool. 62 | 63 | ### Overriding the default confirmation behaviour 64 | 65 | You can override the default implementation by hooking 66 | into `phoenix.link.click`. Here is an example: 67 | 68 | ```javascript 69 | window.addEventListener('phoenix.link.click', function (e) { 70 | // Introduce custom behaviour 71 | var message = e.target.getAttribute("data-prompt"); 72 | var answer = e.target.getAttribute("data-prompt-answer"); 73 | if(message && answer && (answer != window.prompt(message))) { 74 | e.preventDefault(); 75 | } 76 | }, false); 77 | ``` 78 | 79 | """ 80 | 81 | @doc false 82 | defmacro __using__(_) do 83 | raise """ 84 | use Phoenix.HTML is no longer supported in v4.0. 85 | 86 | To keep compatibility with previous versions, \ 87 | add {:phoenix_html_helpers, "~> 1.0"} to your mix.exs deps 88 | and then, instead of "use Phoenix.HTML", you might: 89 | 90 | import Phoenix.HTML 91 | import Phoenix.HTML.Form 92 | use PhoenixHTMLHelpers 93 | 94 | """ 95 | end 96 | 97 | @typedoc "Guaranteed to be safe" 98 | @type safe :: {:safe, iodata} 99 | 100 | @typedoc "May be safe or unsafe (i.e. it needs to be converted)" 101 | @type unsafe :: Phoenix.HTML.Safe.t() 102 | 103 | @doc """ 104 | Marks the given content as raw. 105 | 106 | This means any HTML code inside the given 107 | string won't be escaped. 108 | 109 | iex> raw("") 110 | {:safe, ""} 111 | iex> raw({:safe, ""}) 112 | {:safe, ""} 113 | iex> raw(nil) 114 | {:safe, ""} 115 | 116 | """ 117 | @spec raw(iodata | safe | nil) :: safe 118 | def raw({:safe, value}), do: {:safe, value} 119 | def raw(nil), do: {:safe, ""} 120 | def raw(value) when is_binary(value) or is_list(value), do: {:safe, value} 121 | 122 | @doc """ 123 | Escapes the HTML entities in the given term, returning safe iodata. 124 | 125 | iex> html_escape("") 126 | {:safe, [[[] | "<"], "hello" | ">"]} 127 | 128 | iex> html_escape(~c"") 129 | {:safe, ["<", 104, 101, 108, 108, 111, ">"]} 130 | 131 | iex> html_escape(1) 132 | {:safe, "1"} 133 | 134 | iex> html_escape({:safe, ""}) 135 | {:safe, ""} 136 | 137 | """ 138 | @spec html_escape(unsafe) :: safe 139 | def html_escape({:safe, _} = safe), do: safe 140 | def html_escape(other), do: {:safe, Phoenix.HTML.Engine.encode_to_iodata!(other)} 141 | 142 | @doc """ 143 | Converts a safe result into a string. 144 | 145 | Fails if the result is not safe. In such cases, you can 146 | invoke `html_escape/1` or `raw/1` accordingly before. 147 | 148 | You can combine `html_escape/1` and `safe_to_string/1` 149 | to convert a data structure to a escaped string: 150 | 151 | data |> html_escape() |> safe_to_string() 152 | """ 153 | @spec safe_to_string(safe) :: String.t() 154 | def safe_to_string({:safe, iodata}) do 155 | IO.iodata_to_binary(iodata) 156 | end 157 | 158 | @doc ~S""" 159 | Escapes an enumerable of attributes, returning iodata. 160 | 161 | The attributes are rendered in the given order. Note if 162 | a map is given, the key ordering is not guaranteed. 163 | 164 | The keys and values can be of any shape, as long as they 165 | implement the `Phoenix.HTML.Safe` protocol. In addition, 166 | if the key is an atom, it will be "dasherized". In other 167 | words, `:phx_value_id` will be converted to `phx-value-id`. 168 | 169 | Furthermore, the following attributes provide behaviour: 170 | 171 | * `:aria`, `:data`, and `:phx` - they accept a keyword list as 172 | value. `data: [confirm: "are you sure?"]` is converted to 173 | `data-confirm="are you sure?"`. 174 | 175 | * `:class` - it accepts a list of classes as argument. Each 176 | element in the list is separated by space. `nil` and `false` 177 | elements are discarded. `class: ["foo", nil, "bar"]` then 178 | becomes `class="foo bar"`. 179 | 180 | * `:id` - it is validated raise if a number is given as ID, 181 | which is not allowed by the HTML spec and leads to unpredictable 182 | behaviour. 183 | 184 | ## Examples 185 | 186 | iex> safe_to_string attributes_escape(title: "the title", id: "the id", selected: true) 187 | " title=\"the title\" id=\"the id\" selected" 188 | 189 | iex> safe_to_string attributes_escape(%{data: [confirm: "Are you sure?"]}) 190 | " data-confirm=\"Are you sure?\"" 191 | 192 | iex> safe_to_string attributes_escape(%{phx: [value: [foo: "bar"]]}) 193 | " phx-value-foo=\"bar\"" 194 | 195 | """ 196 | def attributes_escape(attrs) when is_list(attrs) do 197 | {:safe, build_attrs(attrs)} 198 | end 199 | 200 | def attributes_escape(attrs) do 201 | {:safe, attrs |> Enum.to_list() |> build_attrs()} 202 | end 203 | 204 | defp build_attrs([{k, true} | t]), 205 | do: [?\s, key_escape(k) | build_attrs(t)] 206 | 207 | defp build_attrs([{_, false} | t]), 208 | do: build_attrs(t) 209 | 210 | defp build_attrs([{_, nil} | t]), 211 | do: build_attrs(t) 212 | 213 | defp build_attrs([{:id, v} | t]), 214 | do: [" id=\"", id_value(v), ?" | build_attrs(t)] 215 | 216 | defp build_attrs([{:class, v} | t]), 217 | do: [" class=\"", class_value(v), ?" | build_attrs(t)] 218 | 219 | defp build_attrs([{:aria, v} | t]) when is_list(v), 220 | do: nested_attrs(v, " aria", t) 221 | 222 | defp build_attrs([{:data, v} | t]) when is_list(v), 223 | do: nested_attrs(v, " data", t) 224 | 225 | defp build_attrs([{:phx, v} | t]) when is_list(v), 226 | do: nested_attrs(v, " phx", t) 227 | 228 | defp build_attrs([{"id", v} | t]), 229 | do: [" id=\"", id_value(v), ?" | build_attrs(t)] 230 | 231 | defp build_attrs([{"class", v} | t]), 232 | do: [" class=\"", class_value(v), ?" | build_attrs(t)] 233 | 234 | defp build_attrs([{"aria", v} | t]) when is_list(v), 235 | do: nested_attrs(v, " aria", t) 236 | 237 | defp build_attrs([{"data", v} | t]) when is_list(v), 238 | do: nested_attrs(v, " data", t) 239 | 240 | defp build_attrs([{"phx", v} | t]) when is_list(v), 241 | do: nested_attrs(v, " phx", t) 242 | 243 | defp build_attrs([{k, v} | t]), 244 | do: [?\s, key_escape(k), ?=, ?", attr_escape(v), ?" | build_attrs(t)] 245 | 246 | defp build_attrs([]), do: [] 247 | 248 | defp nested_attrs([{k, true} | kv], attr, t), 249 | do: [attr, ?-, key_escape(k) | nested_attrs(kv, attr, t)] 250 | 251 | defp nested_attrs([{_, falsy} | kv], attr, t) when falsy in [false, nil], 252 | do: nested_attrs(kv, attr, t) 253 | 254 | defp nested_attrs([{k, v} | kv], attr, t) when is_list(v), 255 | do: [nested_attrs(v, "#{attr}-#{key_escape(k)}", []) | nested_attrs(kv, attr, t)] 256 | 257 | defp nested_attrs([{k, v} | kv], attr, t), 258 | do: [attr, ?-, key_escape(k), ?=, ?", attr_escape(v), ?" | nested_attrs(kv, attr, t)] 259 | 260 | defp nested_attrs([], _attr, t), 261 | do: build_attrs(t) 262 | 263 | defp id_value(value) when is_number(value) do 264 | raise ArgumentError, 265 | "attempting to set id attribute to #{value}, " <> 266 | "but setting the DOM ID to a number can lead to unpredictable behaviour. " <> 267 | "Instead consider prefixing the id with a string, such as \"user-#{value}\" or similar" 268 | end 269 | 270 | defp id_value(value) do 271 | attr_escape(value) 272 | end 273 | 274 | defp class_value(value) when is_list(value) do 275 | value 276 | |> list_class_value() 277 | |> attr_escape() 278 | end 279 | 280 | defp class_value(value) do 281 | attr_escape(value) 282 | end 283 | 284 | defp list_class_value(value) do 285 | value 286 | |> Enum.flat_map(fn 287 | nil -> [] 288 | false -> [] 289 | inner when is_list(inner) -> [list_class_value(inner)] 290 | other -> [other] 291 | end) 292 | |> Enum.join(" ") 293 | end 294 | 295 | defp key_escape(value) when is_atom(value), do: String.replace(Atom.to_string(value), "_", "-") 296 | defp key_escape(value), do: attr_escape(value) 297 | 298 | defp attr_escape({:safe, data}), do: data 299 | defp attr_escape(nil), do: [] 300 | defp attr_escape(other) when is_binary(other), do: Phoenix.HTML.Engine.html_escape(other) 301 | defp attr_escape(other), do: Phoenix.HTML.Safe.to_iodata(other) 302 | 303 | @doc """ 304 | Escapes HTML content to be inserted into a JavaScript string. 305 | 306 | This function is useful in JavaScript responses when there is a need 307 | to escape HTML rendered from other templates, like in the following: 308 | 309 | $("#container").append("<%= javascript_escape(render("post.html", post: @post)) %>"); 310 | 311 | It escapes quotes (double and single), double backslashes and others. 312 | """ 313 | @spec javascript_escape(binary) :: binary 314 | @spec javascript_escape(safe) :: safe 315 | def javascript_escape({:safe, data}), 316 | do: {:safe, data |> IO.iodata_to_binary() |> javascript_escape("")} 317 | 318 | def javascript_escape(data) when is_binary(data), 319 | do: javascript_escape(data, "") 320 | 321 | defp javascript_escape(<<0x2028::utf8, t::binary>>, acc), 322 | do: javascript_escape(t, <>) 323 | 324 | defp javascript_escape(<<0x2029::utf8, t::binary>>, acc), 325 | do: javascript_escape(t, <>) 326 | 327 | defp javascript_escape(<<0::utf8, t::binary>>, acc), 328 | do: javascript_escape(t, <>) 329 | 330 | defp javascript_escape(<<">, acc), 331 | do: javascript_escape(t, <>) 332 | 333 | defp javascript_escape(<<"\r\n", t::binary>>, acc), 334 | do: javascript_escape(t, <>) 335 | 336 | defp javascript_escape(<>, acc) when h in [?", ?', ?\\, ?`], 337 | do: javascript_escape(t, <>) 338 | 339 | defp javascript_escape(<>, acc) when h in [?\r, ?\n], 340 | do: javascript_escape(t, <>) 341 | 342 | defp javascript_escape(<>, acc), 343 | do: javascript_escape(t, <>) 344 | 345 | defp javascript_escape(<<>>, acc), do: acc 346 | 347 | @doc """ 348 | Escapes a string for use as a CSS identifier. 349 | 350 | ## Examples 351 | 352 | iex> css_escape("hello world") 353 | "hello\\\\ world" 354 | 355 | iex> css_escape("-123") 356 | "-\\\\31 23" 357 | 358 | """ 359 | @spec css_escape(String.t()) :: String.t() 360 | def css_escape(value) when is_binary(value) do 361 | # This is a direct translation of 362 | # https://github.com/mathiasbynens/CSS.escape/blob/master/css.escape.js 363 | # into Elixir. 364 | value 365 | |> String.to_charlist() 366 | |> escape_css_chars() 367 | |> IO.iodata_to_binary() 368 | end 369 | 370 | defp escape_css_chars(chars) do 371 | case chars do 372 | # If the character is the first character and is a `-` (U+002D), and 373 | # there is no second character, […] 374 | [?- | []] -> ["\\-"] 375 | _ -> escape_css_chars(chars, 0, []) 376 | end 377 | end 378 | 379 | defp escape_css_chars([], _, acc), do: Enum.reverse(acc) 380 | 381 | defp escape_css_chars([char | rest], index, acc) do 382 | escaped = 383 | cond do 384 | # If the character is NULL (U+0000), then the REPLACEMENT CHARACTER 385 | # (U+FFFD). 386 | char == 0 -> 387 | <<0xFFFD::utf8>> 388 | 389 | # If the character is in the range [\1-\1F] (U+0001 to U+001F) or is 390 | # U+007F, 391 | # if the character is the first character and is in the range [0-9] 392 | # (U+0030 to U+0039), 393 | # if the character is the second character and is in the range [0-9] 394 | # (U+0030 to U+0039) and the first character is a `-` (U+002D), 395 | char in 0x0001..0x001F or char == 0x007F or 396 | (index == 0 and char in ?0..?9) or 397 | (index == 1 and char in ?0..?9 and hd(acc) == "-") -> 398 | # https://drafts.csswg.org/cssom/#escape-a-character-as-code-point 399 | ["\\", Integer.to_string(char, 16), " "] 400 | 401 | # If the character is not handled by one of the above rules and is 402 | # greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or 403 | # is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to 404 | # U+005A), or [a-z] (U+0061 to U+007A), […] 405 | char >= 0x0080 or char in [?-, ?_] or char in ?0..?9 or char in ?A..?Z or char in ?a..?z -> 406 | # the character itself 407 | <> 408 | 409 | true -> 410 | # Otherwise, the escaped character. 411 | # https://drafts.csswg.org/cssom/#escape-a-character 412 | ["\\", <>] 413 | end 414 | 415 | escape_css_chars(rest, index + 1, [escaped | acc]) 416 | end 417 | end 418 | -------------------------------------------------------------------------------- /lib/phoenix_html/engine.ex: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.HTML.Engine do 2 | @moduledoc """ 3 | An EEx.Engine that guarantees templates are HTML Safe. 4 | """ 5 | 6 | @behaviour EEx.Engine 7 | 8 | @anno (if :erlang.system_info(:otp_release) >= ~c"19" do 9 | [generated: true] 10 | else 11 | [line: -1] 12 | end) 13 | 14 | @doc """ 15 | Encodes the HTML templates to iodata. 16 | """ 17 | def encode_to_iodata!({:safe, body}), do: body 18 | def encode_to_iodata!(nil), do: "" 19 | def encode_to_iodata!(""), do: "" 20 | def encode_to_iodata!(bin) when is_binary(bin), do: html_escape(bin) 21 | def encode_to_iodata!(list) when is_list(list), do: Phoenix.HTML.Safe.List.to_iodata(list) 22 | def encode_to_iodata!(other), do: Phoenix.HTML.Safe.to_iodata(other) 23 | 24 | @doc false 25 | def html_escape(bin) when is_binary(bin) do 26 | html_escape(bin, 0, bin, []) 27 | end 28 | 29 | escapes = [ 30 | {?<, "<"}, 31 | {?>, ">"}, 32 | {?&, "&"}, 33 | {?", """}, 34 | {?', "'"} 35 | ] 36 | 37 | for {match, insert} <- escapes do 38 | defp html_escape(<>, skip, original, acc) do 39 | html_escape(rest, skip + 1, original, [acc | unquote(insert)]) 40 | end 41 | end 42 | 43 | defp html_escape(<<_char, rest::bits>>, skip, original, acc) do 44 | html_escape(rest, skip, original, acc, 1) 45 | end 46 | 47 | defp html_escape(<<>>, _skip, _original, acc) do 48 | acc 49 | end 50 | 51 | for {match, insert} <- escapes do 52 | defp html_escape(<>, skip, original, acc, len) do 53 | part = binary_part(original, skip, len) 54 | html_escape(rest, skip + len + 1, original, [acc, part | unquote(insert)]) 55 | end 56 | end 57 | 58 | defp html_escape(<<_char, rest::bits>>, skip, original, acc, len) do 59 | html_escape(rest, skip, original, acc, len + 1) 60 | end 61 | 62 | defp html_escape(<<>>, 0, original, _acc, _len) do 63 | original 64 | end 65 | 66 | defp html_escape(<<>>, skip, original, acc, len) do 67 | [acc | binary_part(original, skip, len)] 68 | end 69 | 70 | @doc false 71 | def init(_opts) do 72 | %{ 73 | iodata: [], 74 | dynamic: [], 75 | vars_count: 0 76 | } 77 | end 78 | 79 | @doc false 80 | def handle_begin(state) do 81 | %{state | iodata: [], dynamic: []} 82 | end 83 | 84 | @doc false 85 | def handle_end(quoted) do 86 | handle_body(quoted) 87 | end 88 | 89 | @doc false 90 | def handle_body(state) do 91 | %{iodata: iodata, dynamic: dynamic} = state 92 | safe = {:safe, Enum.reverse(iodata)} 93 | {:__block__, [], Enum.reverse([safe | dynamic])} 94 | end 95 | 96 | @doc false 97 | def handle_text(state, text) do 98 | handle_text(state, [], text) 99 | end 100 | 101 | @doc false 102 | def handle_text(state, _meta, text) do 103 | %{iodata: iodata} = state 104 | %{state | iodata: [text | iodata]} 105 | end 106 | 107 | @doc false 108 | def handle_expr(state, "=", ast) do 109 | ast = traverse(ast) 110 | %{iodata: iodata, dynamic: dynamic, vars_count: vars_count} = state 111 | var = Macro.var(:"arg#{vars_count}", __MODULE__) 112 | ast = quote do: unquote(var) = unquote(to_safe(ast)) 113 | %{state | dynamic: [ast | dynamic], iodata: [var | iodata], vars_count: vars_count + 1} 114 | end 115 | 116 | def handle_expr(state, "", ast) do 117 | ast = traverse(ast) 118 | %{dynamic: dynamic} = state 119 | %{state | dynamic: [ast | dynamic]} 120 | end 121 | 122 | def handle_expr(state, marker, ast) do 123 | EEx.Engine.handle_expr(state, marker, ast) 124 | end 125 | 126 | ## Safe conversion 127 | 128 | defp to_safe(ast), do: to_safe(ast, line_from_expr(ast)) 129 | 130 | defp line_from_expr({_, meta, _}) when is_list(meta), do: Keyword.get(meta, :line, 0) 131 | defp line_from_expr(_), do: 0 132 | 133 | # We can do the work at compile time 134 | defp to_safe(literal, _line) 135 | when is_binary(literal) or is_atom(literal) or is_number(literal) do 136 | literal 137 | |> Phoenix.HTML.Safe.to_iodata() 138 | |> IO.iodata_to_binary() 139 | end 140 | 141 | # We can do the work at runtime 142 | defp to_safe(literal, line) when is_list(literal) do 143 | quote line: line, do: Phoenix.HTML.Safe.List.to_iodata(unquote(literal)) 144 | end 145 | 146 | # We need to check at runtime and we do so by optimizing common cases. 147 | defp to_safe(expr, line) do 148 | # Keep stacktraces for protocol dispatch and coverage 149 | safe_return = quote line: line, do: data 150 | bin_return = quote line: line, do: Phoenix.HTML.Engine.html_escape(bin) 151 | other_return = quote line: line, do: Phoenix.HTML.Safe.to_iodata(other) 152 | 153 | # However ignore them for the generated clauses to avoid warnings 154 | quote @anno do 155 | case unquote(expr) do 156 | {:safe, data} -> unquote(safe_return) 157 | bin when is_binary(bin) -> unquote(bin_return) 158 | other -> unquote(other_return) 159 | end 160 | end 161 | end 162 | 163 | ## Traversal 164 | 165 | defp traverse(expr) do 166 | Macro.prewalk(expr, &handle_assign/1) 167 | end 168 | 169 | defp handle_assign({:@, meta, [{name, _, atom}]}) when is_atom(name) and is_atom(atom) do 170 | quote line: meta[:line] || 0 do 171 | Phoenix.HTML.Engine.fetch_assign!(var!(assigns), unquote(name)) 172 | end 173 | end 174 | 175 | defp handle_assign(arg), do: arg 176 | 177 | @doc false 178 | def fetch_assign!(assigns, key) do 179 | case Access.fetch(assigns, key) do 180 | {:ok, val} -> 181 | val 182 | 183 | :error -> 184 | raise ArgumentError, """ 185 | assign @#{key} not available in template. 186 | 187 | Please make sure all proper assigns have been set. If this 188 | is a child template, ensure assigns are given explicitly by 189 | the parent template as they are not automatically forwarded. 190 | 191 | Available assigns: #{inspect(Enum.map(assigns, &elem(&1, 0)))} 192 | """ 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /lib/phoenix_html/form.ex: -------------------------------------------------------------------------------- 1 | defmodule Phoenix.HTML.Form do 2 | @moduledoc ~S""" 3 | Define a `Phoenix.HTML.Form` struct and functions to interact with it. 4 | 5 | For building actual forms in your Phoenix application, see 6 | [the `Phoenix.Component.form/1` component](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#form/1). 7 | 8 | ## Access behaviour 9 | 10 | The `Phoenix.HTML.Form` struct implements the `Access` behaviour. 11 | When you do `form[field]`, it returns a `Phoenix.HTML.FormField` 12 | struct with the `id`, `name`, `value`, and `errors` prefilled. 13 | 14 | The field name can be either an atom or a string. If it is an atom, 15 | it assumes the form keeps both data and errors as atoms. If it is a 16 | string, it considers that data and errors are stored as strings for said 17 | field. Forms backed by an `Ecto.Changeset` only support atom field names. 18 | 19 | It is possible to "access" fields which do not exist in the source data 20 | structure. A `Phoenix.HTML.FormField` struct will be dynamically created 21 | with some attributes such as `name` and `id` populated. 22 | 23 | ## Custom implementations 24 | 25 | There is a protocol named `Phoenix.HTML.FormData` which can be implemented 26 | by any data structure that wants to be cast to the `Phoenix.HTML.Form` struct. 27 | """ 28 | 29 | alias Phoenix.HTML.Form 30 | import Phoenix.HTML 31 | 32 | @doc """ 33 | Defines the Phoenix.HTML.Form struct. 34 | 35 | Its fields are: 36 | 37 | * `:source` - the data structure that implements the form data protocol 38 | 39 | * `:action` - The action that was taken against the form. This value can be 40 | used to distinguish between different operations such as the user typing 41 | into a form for validation, or submitting a form for a database insert. 42 | 43 | * `:impl` - the module with the form data protocol implementation. 44 | This is used to avoid multiple protocol dispatches. 45 | 46 | * `:id` - the id to be used when generating input fields 47 | 48 | * `:index` - the index of the struct in the form 49 | 50 | * `:name` - the name to be used when generating input fields 51 | 52 | * `:data` - the field used to store lookup data 53 | 54 | * `:params` - the parameters associated with this form 55 | 56 | * `:hidden` - a keyword list of fields that are required to 57 | submit the form behind the scenes as hidden inputs 58 | 59 | * `:options` - a copy of the options given when creating the 60 | form without any form data specific key 61 | 62 | * `:errors` - a keyword list of errors that are associated with 63 | the form 64 | """ 65 | defstruct source: nil, 66 | impl: nil, 67 | id: nil, 68 | name: nil, 69 | data: nil, 70 | action: nil, 71 | hidden: [], 72 | params: %{}, 73 | errors: [], 74 | options: [], 75 | index: nil 76 | 77 | @type t :: %Form{ 78 | source: Phoenix.HTML.FormData.t(), 79 | name: String.t(), 80 | data: %{field => term}, 81 | action: atom(), 82 | params: %{binary => term}, 83 | hidden: Keyword.t(), 84 | options: Keyword.t(), 85 | errors: [{field, term}], 86 | impl: module, 87 | id: String.t(), 88 | index: nil | non_neg_integer 89 | } 90 | 91 | @type field :: atom | String.t() 92 | 93 | @doc false 94 | def fetch(%Form{} = form, field) when is_atom(field) do 95 | fetch(form, field, Atom.to_string(field)) 96 | end 97 | 98 | def fetch(%Form{} = form, field) when is_binary(field) do 99 | fetch(form, field, field) 100 | end 101 | 102 | def fetch(%Form{}, field) do 103 | raise ArgumentError, 104 | "accessing a form with form[field] requires the field to be an atom or a string, got: #{inspect(field)}" 105 | end 106 | 107 | defp fetch(%{errors: errors} = form, field, field_as_string) do 108 | {:ok, 109 | %Phoenix.HTML.FormField{ 110 | errors: field_errors(errors, field), 111 | field: field, 112 | form: form, 113 | id: input_id(form, field_as_string), 114 | name: input_name(form, field_as_string), 115 | value: input_value(form, field) 116 | }} 117 | end 118 | 119 | @doc """ 120 | Returns a value of a corresponding form field. 121 | 122 | The `form` should either be a `Phoenix.HTML.Form` or an atom. 123 | The field is either a string or an atom. If the field is given 124 | as an atom, it will attempt to look data with atom keys. If 125 | a string, it will look data with string keys. 126 | 127 | When a form is given, it will look for changes, then 128 | fallback to parameters, and finally fallback to the default 129 | struct/map value. 130 | 131 | Since the function looks up parameter values too, there is 132 | no guarantee that the value will have a certain type. For 133 | example, a boolean field will be sent as "false" as a 134 | parameter, and this function will return it as is. If you 135 | need to normalize the result of `input_value`, see 136 | `normalize_value/2`. 137 | """ 138 | @spec input_value(t | atom, field) :: term 139 | def input_value(%{source: source, impl: impl} = form, field) 140 | when is_atom(field) or is_binary(field) do 141 | impl.input_value(source, form, field) 142 | end 143 | 144 | def input_value(name, _field) when is_atom(name), do: nil 145 | 146 | @doc """ 147 | Returns an id of a corresponding form field. 148 | 149 | The form should either be a `Phoenix.HTML.Form` or an atom. 150 | """ 151 | @spec input_id(t | atom, field) :: String.t() 152 | def input_id(%{id: nil}, field), do: "#{field}" 153 | 154 | def input_id(%{id: id}, field) when is_atom(field) or is_binary(field) do 155 | "#{id}_#{field}" 156 | end 157 | 158 | def input_id(name, field) when (is_atom(name) and is_atom(field)) or is_binary(field) do 159 | "#{name}_#{field}" 160 | end 161 | 162 | @doc """ 163 | Returns an id of a corresponding form field and value attached to it. 164 | 165 | Useful for radio buttons and inputs like multiselect checkboxes. 166 | """ 167 | @spec input_id(t | atom, field, Phoenix.HTML.Safe.t()) :: String.t() 168 | def input_id(name, field, value) do 169 | {:safe, value} = html_escape(value) 170 | value_id = value |> IO.iodata_to_binary() |> String.replace(~r/\W/u, "_") 171 | input_id(name, field) <> "_" <> value_id 172 | end 173 | 174 | @doc """ 175 | Returns a name of a corresponding form field. 176 | 177 | The first argument should either be a `Phoenix.HTML.Form` or an atom. 178 | 179 | ## Examples 180 | 181 | iex> Phoenix.HTML.Form.input_name(:user, :first_name) 182 | "user[first_name]" 183 | """ 184 | @spec input_name(t | atom, field) :: String.t() 185 | def input_name(form_or_name, field) 186 | 187 | def input_name(%{name: nil}, field), do: to_string(field) 188 | 189 | def input_name(%{name: name}, field) when is_atom(field) or is_binary(field), 190 | do: "#{name}[#{field}]" 191 | 192 | def input_name(name, field) when (is_atom(name) and is_atom(field)) or is_binary(field), 193 | do: "#{name}[#{field}]" 194 | 195 | @doc """ 196 | Receives two forms structs and checks if the given field changed. 197 | 198 | The field will have changed if either its associated value, errors, 199 | action, or implementation changed. This is mostly used for optimization 200 | engines as an extension of the `Access` behaviour. 201 | """ 202 | @spec input_changed?(t, t, field()) :: boolean() 203 | def input_changed?( 204 | %Form{ 205 | impl: impl1, 206 | id: id1, 207 | name: name1, 208 | errors: errors1, 209 | source: source1, 210 | action: action1 211 | } = form1, 212 | %Form{ 213 | impl: impl2, 214 | id: id2, 215 | name: name2, 216 | errors: errors2, 217 | source: source2, 218 | action: action2 219 | } = form2, 220 | field 221 | ) 222 | when is_atom(field) or is_binary(field) do 223 | impl1 != impl2 or id1 != id2 or name1 != name2 or action1 != action2 or 224 | field_errors(errors1, field) != field_errors(errors2, field) or 225 | impl1.input_value(source1, form1, field) != impl2.input_value(source2, form2, field) 226 | end 227 | 228 | @doc """ 229 | Returns the HTML validations that would apply to 230 | the given field. 231 | """ 232 | @spec input_validations(t, field) :: Keyword.t() 233 | def input_validations(%{source: source, impl: impl} = form, field) 234 | when is_atom(field) or is_binary(field) do 235 | impl.input_validations(source, form, field) 236 | end 237 | 238 | @doc """ 239 | Normalizes an input `value` according to its input `type`. 240 | 241 | Certain HTML input values must be cast, or they will have idiosyncracies 242 | when they are rendered. The goal of this function is to encapsulate 243 | this logic. In particular: 244 | 245 | * For "datetime-local" types, it converts `DateTime` and 246 | `NaiveDateTime` to strings without the second precision 247 | 248 | * For "checkbox" types, it returns a boolean depending on 249 | whether the input is "true" or not 250 | 251 | * For "textarea", it prefixes a newline to ensure newlines 252 | won't be ignored on submission. This requires however 253 | that the textarea is rendered with no spaces after its 254 | content 255 | """ 256 | def normalize_value("datetime-local", %struct{} = value) 257 | when struct in [NaiveDateTime, DateTime] do 258 | <> = struct.to_string(value) 259 | {:safe, [date, ?T, hour_minute]} 260 | end 261 | 262 | def normalize_value("textarea", value) do 263 | {:safe, value} = html_escape(value || "") 264 | {:safe, [?\n | value]} 265 | end 266 | 267 | def normalize_value("checkbox", value) do 268 | html_escape(value) == {:safe, "true"} 269 | end 270 | 271 | def normalize_value(_type, value) do 272 | value 273 | end 274 | 275 | @doc """ 276 | Returns options to be used inside a select element. 277 | 278 | `options` is expected to be an enumerable which will be used to 279 | generate each `option` element. The function supports different data 280 | for the individual elements: 281 | 282 | * keyword lists - each keyword list is expected to have the keys 283 | `:key` and `:value`. Additional keys such as `:disabled` may 284 | be given to customize the option. 285 | * two-item tuples - where the first element is an atom, string or 286 | integer to be used as the option label and the second element is 287 | an atom, string or integer to be used as the option value 288 | * simple atom, string or integer - which will be used as both label and value 289 | for the generated select 290 | 291 | ## Option groups 292 | 293 | If `options` is map or keyword list where the first element is a string, 294 | atom or integer and the second element is a list or a map, it is assumed 295 | the key will be wrapped in an `` and the value will be used to 296 | generate `` nested under the group. 297 | 298 | ## Examples 299 | 300 | options_for_select(["Admin": "admin", "User": "user"], "admin") 301 | #=> 302 | #=> 303 | 304 | Multiple selected values: 305 | 306 | options_for_select(["Admin": "admin", "User": "user", "Moderator": "moderator"], 307 | ["admin", "moderator"]) 308 | #=> 309 | #=> 310 | #=> 311 | 312 | Groups: 313 | 314 | options_for_select(["Europe": ["UK", "Sweden", "France"], ...], nil) 315 | #=> 316 | #=> 317 | #=> 318 | #=> 319 | #=> 320 | 321 | Horizontal separators can be added: 322 | 323 | options_for_select(["Admin", "User", :hr, "New"], nil) 324 | #=> 325 | #=> 326 | #=>
327 | #=> 328 | 329 | options_for_select(["Admin": "admin", "User": "user", hr: nil, "New": "new"], nil) 330 | #=> 331 | #=> 332 | #=>
333 | #=> 334 | 335 | 336 | """ 337 | def options_for_select(options, selected_values) do 338 | {:safe, 339 | escaped_options_for_select( 340 | options, 341 | selected_values |> List.wrap() |> Enum.map(&html_escape/1) 342 | )} 343 | end 344 | 345 | defp escaped_options_for_select(options, selected_values) do 346 | Enum.reduce(options, [], fn 347 | {:hr, nil}, acc -> 348 | [acc | hr_tag()] 349 | 350 | {option_key, option_value}, acc -> 351 | [acc | option(option_key, option_value, [], selected_values)] 352 | 353 | options, acc when is_list(options) -> 354 | {option_key, options} = 355 | case List.keytake(options, :key, 0) do 356 | nil -> 357 | raise ArgumentError, 358 | "expected :key key when building