CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/hex-phoenix-html

Building blocks for working with HTML in Phoenix - provides HTML safety mechanisms, form abstractions, and JavaScript enhancements

Overview
Eval results
Files

forms.mddocs/

Forms and Data Binding

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.

Capabilities

Form Creation and Management

Convert data structures into form representations with configurable options for naming, validation, and field handling.

Phoenix.HTML.Form Struct

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()
end

Field Access and Information

Access form fields and retrieve field-specific information like IDs, names, and values through the Access behaviour and utility functions.

Form Field Access

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 invalid

Usage 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 field

Field Value Retrieval

Retrieve 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:

  1. Form parameters (user input)
  2. Form data (original data)
  3. nil if neither available

Usage 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: nil

Field ID Generation

Generate 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 value

Usage 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)

Field Name Generation

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 value

Usage 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]"

Field Validation and HTML5 Support

Retrieve HTML5 validation attributes and compare field changes between form states.

HTML5 Validation Attributes

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 implementation

Common 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 pattern

Usage 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)

Field Change Detection

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 values

Change Detection Criteria:

  • Field value changed
  • Field errors changed
  • Form action changed
  • Form implementation changed
  • Form ID or name changed

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: false

Value Normalization and Processing

Normalize input values according to their HTML input types and handle type-specific formatting requirements.

Input Value Normalization

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 type

Supported Input Types:

  • "checkbox": Returns boolean based on "true" string value
  • "datetime-local": Formats DateTime/NaiveDateTime to HTML datetime format
  • "textarea": Prefixes newline to preserve formatting
  • Other types: Returns value unchanged

Usage 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"

Select Options and Form Controls

Generate options for select elements with support for grouping, selection state, and complex data structures.

Select Option Generation

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 elements

Supported Option Formats:

  1. Two-element tuples: {label, value}
  2. Keyword lists with :key and :value: [key: "Label", value: "value", disabled: true]
  3. Simple values: Used as both label and value
  4. Groups: {group_label, [options]} for optgroup elements
  5. Separators: :hr for horizontal rules

Usage 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 groups

Phoenix.HTML.FormField Struct

Individual 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
  }
end

Usage 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 struct

Protocol: Phoenix.HTML.FormData

The 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)
end

FormData Protocol Options

Shared Options (all implementations):

  • :as - Form name for input naming
  • :id - Form ID for HTML attributes

Nested 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 specifications

Map Implementation

Built-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 metadata

Map Implementation Characteristics:

  • String Keys Required: Maps must have string keys to represent form parameters
  • Parameter Precedence: Form parameters take precedence over form data
  • Cardinality Detection: Uses :default option to determine single vs multiple forms
  • No Validations: Returns empty list for input_validations/3
  • Atom Key Warning: Issues warning if atom keys detected in map

Usage 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)

Custom Protocol Implementation

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)

Error Handling

Form Field Errors

# 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

Best Practices

  1. Field Names: Use atoms for Ecto changesets, strings for plain maps
  2. Nested Forms: Always provide :default values for nested form generation
  3. Validation: Implement input_validations/3 for HTML5 client-side validation
  4. Error Handling: Include error information in form data for field-level error display
  5. Parameters: Let Phoenix handle form parameter binding automatically
  6. Custom Types: Implement FormData protocol for domain-specific data structures

Install with Tessl CLI

npx tessl i tessl/hex-phoenix-html

docs

forms.md

html-safety.md

index.md

javascript.md

tile.json