Building blocks for working with HTML in Phoenix - provides HTML safety mechanisms, form abstractions, and JavaScript enhancements
Phoenix.HTML provides comprehensive form abstractions that convert various data structures into form representations, handle field access and validation, and provide utilities for form rendering and data binding. The system is built around protocols that enable flexible data source integration.
Convert data structures into form representations with configurable options for naming, validation, and field handling.
The main form data structure that contains all information needed for form rendering and data binding.
defmodule Phoenix.HTML.Form do
defstruct [
:source, # Original data structure
:impl, # FormData protocol implementation module
:id, # Form ID for HTML attributes
:name, # Form name for input naming
:data, # Form data for field lookups
:action, # Current form action (:validate, :save, etc.)
:hidden, # Hidden fields required for submission
:params, # Current form parameters
:errors, # Form validation errors
:options, # Additional options
:index # Index for nested forms
]
@type t :: %__MODULE__{
source: Phoenix.HTML.FormData.t(),
impl: module,
id: String.t(),
name: String.t(),
data: %{field => term},
action: atom(),
hidden: Keyword.t(),
params: %{binary => term},
errors: [{field, term}],
options: Keyword.t(),
index: nil | non_neg_integer
}
@type field :: atom | String.t()
endAccess form fields and retrieve field-specific information like IDs, names, and values through the Access behaviour and utility functions.
Forms implement the Access behaviour, allowing bracket syntax for field access:
# Access pattern returns Phoenix.HTML.FormField
form[field_name] :: Phoenix.HTML.FormField.t()
# Direct fetch function (Access behavior)
def fetch(form, field) :: {:ok, Phoenix.HTML.FormField.t()} | :error
# Parameters:
# - form: Phoenix.HTML.Form.t() - Form struct
# - field: Phoenix.HTML.Form.field() - Field identifier (atom or string)
#
# Returns:
# - {:ok, Phoenix.HTML.FormField.t()} - Success tuple with FormField
# - :error - When field type is invalidUsage Examples:
# Create form from map data
form_data = %{name: "John", email: "john@example.com"}
form = Phoenix.HTML.FormData.to_form(form_data, as: :user)
# Access fields using bracket notation
name_field = form[:name]
# Returns: %Phoenix.HTML.FormField{
# field: :name,
# form: form,
# id: "user_name",
# name: "user[name]",
# value: "John",
# errors: []
# }
# Access with string keys
email_field = form["email"]
# Returns field struct for email fieldRetrieve the current value of form fields, considering parameters, changes, and default values.
def input_value(form, field) :: term
# Parameters:
# - form: Phoenix.HTML.Form.t() | atom - Form struct or form name
# - field: Phoenix.HTML.Form.field() - Field identifier (atom or string)
#
# Returns:
# - term - Current field value (may be any type)Value Resolution Order:
nil if neither availableUsage Examples:
# Basic value retrieval
form_data = %{name: "John", age: 30}
form = Phoenix.HTML.FormData.to_form(form_data)
name = Phoenix.HTML.Form.input_value(form, :name)
# Returns: "John"
# With form parameters (user input)
form_with_params = %Phoenix.HTML.Form{
data: %{name: "John"},
params: %{"name" => "Jane"} # User changed the name
}
current_name = Phoenix.HTML.Form.input_value(form_with_params, :name)
# Returns: "Jane" (parameters take precedence)
# Non-existent field
missing = Phoenix.HTML.Form.input_value(form, :nonexistent)
# Returns: nilGenerate HTML ID attributes for form fields with proper namespacing and collision prevention.
def input_id(form, field) :: String.t()
def input_id(form, field, value) :: String.t()
# Parameters:
# - form: Phoenix.HTML.Form.t() | atom - Form struct or form name
# - field: Phoenix.HTML.Form.field() - Field identifier
# - value: Phoenix.HTML.Safe.t() - Additional value for ID (optional)
#
# Returns:
# - String.t() - HTML ID attribute valueUsage Examples:
# Basic ID generation
form = Phoenix.HTML.FormData.to_form(%{}, as: :user)
id = Phoenix.HTML.Form.input_id(form, :name)
# Returns: "user_name"
# With form atom shorthand
id = Phoenix.HTML.Form.input_id(:user, :email)
# Returns: "user_email"
# With value suffix (for radio buttons, checkboxes)
id = Phoenix.HTML.Form.input_id(:user, :role, "admin")
# Returns: "user_role_admin"
# Special characters in value are escaped
id = Phoenix.HTML.Form.input_id(:user, :pref, "option-1 & 2")
# Returns: "user_pref_option_1___2" (non-word chars become underscores)Generate HTML name attributes for form fields with proper nested structure for parameter binding.
def input_name(form, field) :: String.t()
# Parameters:
# - form: Phoenix.HTML.Form.t() | atom - Form struct or form name
# - field: Phoenix.HTML.Form.field() - Field identifier
#
# Returns:
# - String.t() - HTML name attribute valueUsage Examples:
# Basic name generation
form = Phoenix.HTML.FormData.to_form(%{}, as: :user)
name = Phoenix.HTML.Form.input_name(form, :email)
# Returns: "user[email]"
# With form atom shorthand
name = Phoenix.HTML.Form.input_name(:post, :title)
# Returns: "post[title]"
# Nested forms create nested names
parent_form = Phoenix.HTML.FormData.to_form(%{}, as: :user)
nested_forms = Phoenix.HTML.FormData.to_form(%{}, parent_form, :addresses, as: :address)
nested_name = Phoenix.HTML.Form.input_name(List.first(nested_forms), :street)
# Returns: "user[addresses][0][street]"Retrieve HTML5 validation attributes and compare field changes between form states.
Extract validation rules from form data sources for HTML5 client-side validation.
def input_validations(form, field) :: Keyword.t()
# Parameters:
# - form: Phoenix.HTML.Form.t() - Form struct with source and impl
# - field: Phoenix.HTML.Form.field() - Field identifier (atom or string)
#
# Returns:
# - Keyword.t() - HTML5 validation attributes
#
# Delegates to the FormData protocol implementationCommon Validation Attributes:
:required - Field is required:minlength - Minimum string length:maxlength - Maximum string length:min - Minimum numeric value:max - Maximum numeric value:pattern - Regular expression patternUsage Examples:
# Basic usage (implementation-dependent)
form = Phoenix.HTML.FormData.to_form(%{}, as: :user)
validations = Phoenix.HTML.Form.input_validations(form, :email)
# Returns: [] (Map implementation returns empty list)
# With Ecto changeset (requires phoenix_ecto)
changeset = User.changeset(%User{}, %{})
ecto_form = Phoenix.HTML.FormData.to_form(changeset)
validations = Phoenix.HTML.Form.input_validations(ecto_form, :email)
# Returns: [required: true, type: "email"] (example)Compare field values and metadata between two form states to detect changes.
def input_changed?(form1, form2, field) :: boolean()
# Parameters:
# - form1: Phoenix.HTML.Form.t() - First form state
# - form2: Phoenix.HTML.Form.t() - Second form state
# - field: Phoenix.HTML.Form.field() - Field identifier (atom or string)
#
# Returns:
# - boolean() - True if field changed between forms
#
# Compares form implementation, id, name, action, field errors, and field valuesChange Detection Criteria:
Usage Examples:
# Compare form states
original_form = Phoenix.HTML.FormData.to_form(%{name: "John"})
updated_form = Phoenix.HTML.FormData.to_form(%{name: "Jane"})
changed = Phoenix.HTML.Form.input_changed?(original_form, updated_form, :name)
# Returns: true
# No change detected
same_form = Phoenix.HTML.FormData.to_form(%{name: "John"})
no_change = Phoenix.HTML.Form.input_changed?(original_form, same_form, :name)
# Returns: falseNormalize input values according to their HTML input types and handle type-specific formatting requirements.
Convert and format values based on HTML input type requirements.
def normalize_value(input_type, value) :: term
# Parameters:
# - input_type: String.t() - HTML input type
# - value: term - Value to normalize
#
# Returns:
# - term - Normalized value appropriate for input typeSupported Input Types:
"checkbox": Returns boolean based on "true" string value"datetime-local": Formats DateTime/NaiveDateTime to HTML datetime format"textarea": Prefixes newline to preserve formattingUsage Examples:
# Checkbox normalization
Phoenix.HTML.Form.normalize_value("checkbox", "true")
# Returns: true
Phoenix.HTML.Form.normalize_value("checkbox", "false")
# Returns: false
Phoenix.HTML.Form.normalize_value("checkbox", nil)
# Returns: false
# DateTime normalization
datetime = ~N[2023-12-25 14:30:45]
normalized = Phoenix.HTML.Form.normalize_value("datetime-local", datetime)
# Returns: {:safe, ["2023-12-25", ?T, "14:30"]}
# Textarea normalization (preserves leading newlines)
Phoenix.HTML.Form.normalize_value("textarea", "Hello\nWorld")
# Returns: {:safe, [?\n, "Hello\nWorld"]}
# Pass-through for other types
Phoenix.HTML.Form.normalize_value("text", "Some text")
# Returns: "Some text"Generate options for select elements with support for grouping, selection state, and complex data structures.
Create HTML option elements from various data structures with selection handling.
def options_for_select(options, selected_values) :: Phoenix.HTML.safe
# Parameters:
# - options: Enumerable.t() - Options data structure
# - selected_values: term | [term] - Currently selected values
#
# Returns:
# - Phoenix.HTML.safe - HTML option elementsSupported Option Formats:
{label, value}:key and :value: [key: "Label", value: "value", disabled: true]{group_label, [options]} for optgroup elements:hr for horizontal rulesUsage Examples:
# Simple tuples
options = [{"Admin", "admin"}, {"User", "user"}, {"Guest", "guest"}]
select_html = Phoenix.HTML.Form.options_for_select(options, "user")
# Returns HTML: <option value="admin">Admin</option>
# <option value="user" selected>User</option>
# <option value="guest">Guest</option>
# Multiple selections
options = [{"Red", "red"}, {"Green", "green"}, {"Blue", "blue"}]
select_html = Phoenix.HTML.Form.options_for_select(options, ["red", "blue"])
# Returns HTML with red and blue selected
# With additional attributes
options = [
[key: "Administrator", value: "admin", disabled: false],
[key: "Disabled User", value: "disabled_user", disabled: true]
]
select_html = Phoenix.HTML.Form.options_for_select(options, nil)
# Returns HTML: <option value="admin">Administrator</option>
# <option value="disabled_user" disabled>Disabled User</option>
# Option groups
grouped_options = [
{"North America", [{"USA", "us"}, {"Canada", "ca"}]},
{"Europe", [{"UK", "uk"}, {"Germany", "de"}]}
]
select_html = Phoenix.HTML.Form.options_for_select(grouped_options, "us")
# Returns HTML: <optgroup label="North America">
# <option value="us" selected>USA</option>
# <option value="ca">Canada</option>
# </optgroup>
# <optgroup label="Europe">...</optgroup>
# With separators
options = [{"Option 1", "1"}, {"Option 2", "2"}, :hr, {"Option 3", "3"}]
select_html = Phoenix.HTML.Form.options_for_select(options, nil)
# Returns HTML with <hr/> separator between option groupsIndividual form field representation returned by form field access operations.
defmodule Phoenix.HTML.FormField do
@enforce_keys [:id, :name, :errors, :field, :form, :value]
defstruct [:id, :name, :errors, :field, :form, :value]
@type t :: %__MODULE__{
id: String.t(), # HTML id attribute
name: String.t(), # HTML name attribute
errors: [term], # Field-specific errors
field: Phoenix.HTML.Form.field(), # Original field identifier
form: Phoenix.HTML.Form.t(), # Parent form reference
value: term # Current field value
}
endUsage Examples:
# Create form and access field
form = Phoenix.HTML.FormData.to_form(%{name: "John", email: "john@example.com"}, as: :user)
name_field = form[:name]
# Access field properties
name_field.id # Returns: "user_name"
name_field.name # Returns: "user[name]"
name_field.value # Returns: "John"
name_field.field # Returns: :name
name_field.errors # Returns: []
name_field.form # Returns: original form structThe FormData protocol enables any data structure to be converted into form representations, providing flexibility for different data sources like maps, structs, and changesets.
defprotocol Phoenix.HTML.FormData do
@spec to_form(t, Keyword.t()) :: Phoenix.HTML.Form.t()
def to_form(data, options)
@spec to_form(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field(), Keyword.t()) :: [Phoenix.HTML.Form.t()]
def to_form(data, parent_form, field, options)
@spec input_value(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field()) :: term
def input_value(data, form, field)
@spec input_validations(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field()) :: Keyword.t()
def input_validations(data, form, field)
endShared Options (all implementations):
:as - Form name for input naming:id - Form ID for HTML attributesNested Form Options:
:default - Default value for nested forms:prepend - Values to prepend to list forms:append - Values to append to list forms:action - Form action context:hidden - Hidden field specificationsBuilt-in implementation for Map data structures with comprehensive form generation capabilities. Maps are treated as parameter sources (user input) and should have string keys.
# Convert map to form
Phoenix.HTML.FormData.to_form(map, opts) :: Phoenix.HTML.Form.t()
# Parameters:
# - map: %{binary => term} - Map with string keys (warns if atom keys)
# - opts: Keyword.t() - Options including :as, :id, :errors, :action
#
# Returns:
# - Phoenix.HTML.Form.t() - Form struct with map as params
# Convert nested field to sub-forms
Phoenix.HTML.FormData.to_form(map, parent_form, field, opts) :: [Phoenix.HTML.Form.t()]
# Parameters:
# - map: %{binary => term} - Parent map data
# - parent_form: Phoenix.HTML.Form.t() - Parent form context
# - field: Phoenix.HTML.Form.field() - Field name for nested forms
# - opts: Keyword.t() - Options including :default, :prepend, :append
#
# Returns:
# - [Phoenix.HTML.Form.t()] - List of forms (cardinality: one or many)
# Get field value from map
def input_value(map, form, field) :: term
# Returns parameter value if present, otherwise data value
# Get validation attributes (Map implementation returns empty list)
def input_validations(map, form, field) :: []
# Map implementation doesn't provide validation metadataMap Implementation Characteristics:
:default option to determine single vs multiple formsinput_validations/3Usage Examples:
# Basic map form
user_data = %{name: "John", email: "john@example.com", active: true}
form = Phoenix.HTML.FormData.to_form(user_data, as: :user, id: "user-form")
# Nested object forms (cardinality: one)
profile_data = %{user: %{name: "John", profile: %{bio: "Developer"}}}
form = Phoenix.HTML.FormData.to_form(profile_data, as: :data)
profile_forms = Phoenix.HTML.FormData.to_form(profile_data, form, :profile, default: %{})
profile_form = List.first(profile_forms)
# Nested list forms (cardinality: many)
user_with_addresses = %{name: "John", addresses: []}
form = Phoenix.HTML.FormData.to_form(user_with_addresses, as: :user)
address_forms = Phoenix.HTML.FormData.to_form(
user_with_addresses,
form,
:addresses,
default: [],
prepend: [%{street: ""}]
)
# Returns list of forms for each address
# With form parameters (simulating user input)
form_params = %{"name" => "Jane", "email" => "jane@example.com"}
form_with_params = Phoenix.HTML.FormData.to_form(form_params, as: :user)Extend FormData protocol for custom data structures:
# Example: Custom struct with protocol implementation
defmodule UserProfile do
defstruct [:user, :preferences, :addresses]
end
defimpl Phoenix.HTML.FormData, for: UserProfile do
def to_form(%UserProfile{user: user, preferences: prefs}, opts) do
# Convert to form using user data
Phoenix.HTML.FormData.to_form(user, opts)
end
def to_form(%UserProfile{addresses: addresses}, form, :addresses, opts) do
# Handle nested address forms
Phoenix.HTML.FormData.to_form(addresses, form, :addresses, opts)
end
def input_value(%UserProfile{user: user}, form, field) do
Phoenix.HTML.FormData.input_value(user, form, field)
end
def input_validations(%UserProfile{}, _form, _field) do
[]
end
end
# Usage
profile = %UserProfile{
user: %{name: "John", email: "john@example.com"},
addresses: [%{street: "123 Main St", city: "Boston"}]
}
form = Phoenix.HTML.FormData.to_form(profile, as: :profile)# Invalid field access
form[:invalid_field_type]
# Raises: ArgumentError - field must be atom or string
# Protocol undefined
Phoenix.HTML.FormData.to_form(%DateTime{}, [])
# Raises: Protocol.UndefinedError - Phoenix.HTML.FormData not implemented:default values for nested form generationinput_validations/3 for HTML5 client-side validationInstall with Tessl CLI
npx tessl i tessl/hex-phoenix-html