0
# Forms and Data Binding
1
2
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.
3
4
## Capabilities
5
6
### Form Creation and Management
7
8
Convert data structures into form representations with configurable options for naming, validation, and field handling.
9
10
#### Phoenix.HTML.Form Struct
11
12
The main form data structure that contains all information needed for form rendering and data binding.
13
14
```elixir { .api }
15
defmodule Phoenix.HTML.Form do
16
defstruct [
17
:source, # Original data structure
18
:impl, # FormData protocol implementation module
19
:id, # Form ID for HTML attributes
20
:name, # Form name for input naming
21
:data, # Form data for field lookups
22
:action, # Current form action (:validate, :save, etc.)
23
:hidden, # Hidden fields required for submission
24
:params, # Current form parameters
25
:errors, # Form validation errors
26
:options, # Additional options
27
:index # Index for nested forms
28
]
29
30
@type t :: %__MODULE__{
31
source: Phoenix.HTML.FormData.t(),
32
impl: module,
33
id: String.t(),
34
name: String.t(),
35
data: %{field => term},
36
action: atom(),
37
hidden: Keyword.t(),
38
params: %{binary => term},
39
errors: [{field, term}],
40
options: Keyword.t(),
41
index: nil | non_neg_integer
42
}
43
44
@type field :: atom | String.t()
45
end
46
```
47
48
### Field Access and Information
49
50
Access form fields and retrieve field-specific information like IDs, names, and values through the Access behaviour and utility functions.
51
52
#### Form Field Access
53
54
Forms implement the Access behaviour, allowing bracket syntax for field access:
55
56
```elixir { .api }
57
# Access pattern returns Phoenix.HTML.FormField
58
form[field_name] :: Phoenix.HTML.FormField.t()
59
60
# Direct fetch function (Access behavior)
61
def fetch(form, field) :: {:ok, Phoenix.HTML.FormField.t()} | :error
62
# Parameters:
63
# - form: Phoenix.HTML.Form.t() - Form struct
64
# - field: Phoenix.HTML.Form.field() - Field identifier (atom or string)
65
#
66
# Returns:
67
# - {:ok, Phoenix.HTML.FormField.t()} - Success tuple with FormField
68
# - :error - When field type is invalid
69
```
70
71
**Usage Examples:**
72
73
```elixir
74
# Create form from map data
75
form_data = %{name: "John", email: "john@example.com"}
76
form = Phoenix.HTML.FormData.to_form(form_data, as: :user)
77
78
# Access fields using bracket notation
79
name_field = form[:name]
80
# Returns: %Phoenix.HTML.FormField{
81
# field: :name,
82
# form: form,
83
# id: "user_name",
84
# name: "user[name]",
85
# value: "John",
86
# errors: []
87
# }
88
89
# Access with string keys
90
email_field = form["email"]
91
# Returns field struct for email field
92
```
93
94
#### Field Value Retrieval
95
96
Retrieve the current value of form fields, considering parameters, changes, and default values.
97
98
```elixir { .api }
99
def input_value(form, field) :: term
100
# Parameters:
101
# - form: Phoenix.HTML.Form.t() | atom - Form struct or form name
102
# - field: Phoenix.HTML.Form.field() - Field identifier (atom or string)
103
#
104
# Returns:
105
# - term - Current field value (may be any type)
106
```
107
108
**Value Resolution Order:**
109
1. Form parameters (user input)
110
2. Form data (original data)
111
3. `nil` if neither available
112
113
**Usage Examples:**
114
115
```elixir
116
# Basic value retrieval
117
form_data = %{name: "John", age: 30}
118
form = Phoenix.HTML.FormData.to_form(form_data)
119
name = Phoenix.HTML.Form.input_value(form, :name)
120
# Returns: "John"
121
122
# With form parameters (user input)
123
form_with_params = %Phoenix.HTML.Form{
124
data: %{name: "John"},
125
params: %{"name" => "Jane"} # User changed the name
126
}
127
current_name = Phoenix.HTML.Form.input_value(form_with_params, :name)
128
# Returns: "Jane" (parameters take precedence)
129
130
# Non-existent field
131
missing = Phoenix.HTML.Form.input_value(form, :nonexistent)
132
# Returns: nil
133
```
134
135
#### Field ID Generation
136
137
Generate HTML ID attributes for form fields with proper namespacing and collision prevention.
138
139
```elixir { .api }
140
def input_id(form, field) :: String.t()
141
def input_id(form, field, value) :: String.t()
142
# Parameters:
143
# - form: Phoenix.HTML.Form.t() | atom - Form struct or form name
144
# - field: Phoenix.HTML.Form.field() - Field identifier
145
# - value: Phoenix.HTML.Safe.t() - Additional value for ID (optional)
146
#
147
# Returns:
148
# - String.t() - HTML ID attribute value
149
```
150
151
**Usage Examples:**
152
153
```elixir
154
# Basic ID generation
155
form = Phoenix.HTML.FormData.to_form(%{}, as: :user)
156
id = Phoenix.HTML.Form.input_id(form, :name)
157
# Returns: "user_name"
158
159
# With form atom shorthand
160
id = Phoenix.HTML.Form.input_id(:user, :email)
161
# Returns: "user_email"
162
163
# With value suffix (for radio buttons, checkboxes)
164
id = Phoenix.HTML.Form.input_id(:user, :role, "admin")
165
# Returns: "user_role_admin"
166
167
# Special characters in value are escaped
168
id = Phoenix.HTML.Form.input_id(:user, :pref, "option-1 & 2")
169
# Returns: "user_pref_option_1___2" (non-word chars become underscores)
170
```
171
172
#### Field Name Generation
173
174
Generate HTML name attributes for form fields with proper nested structure for parameter binding.
175
176
```elixir { .api }
177
def input_name(form, field) :: String.t()
178
# Parameters:
179
# - form: Phoenix.HTML.Form.t() | atom - Form struct or form name
180
# - field: Phoenix.HTML.Form.field() - Field identifier
181
#
182
# Returns:
183
# - String.t() - HTML name attribute value
184
```
185
186
**Usage Examples:**
187
188
```elixir
189
# Basic name generation
190
form = Phoenix.HTML.FormData.to_form(%{}, as: :user)
191
name = Phoenix.HTML.Form.input_name(form, :email)
192
# Returns: "user[email]"
193
194
# With form atom shorthand
195
name = Phoenix.HTML.Form.input_name(:post, :title)
196
# Returns: "post[title]"
197
198
# Nested forms create nested names
199
parent_form = Phoenix.HTML.FormData.to_form(%{}, as: :user)
200
nested_forms = Phoenix.HTML.FormData.to_form(%{}, parent_form, :addresses, as: :address)
201
nested_name = Phoenix.HTML.Form.input_name(List.first(nested_forms), :street)
202
# Returns: "user[addresses][0][street]"
203
```
204
205
### Field Validation and HTML5 Support
206
207
Retrieve HTML5 validation attributes and compare field changes between form states.
208
209
#### HTML5 Validation Attributes
210
211
Extract validation rules from form data sources for HTML5 client-side validation.
212
213
```elixir { .api }
214
def input_validations(form, field) :: Keyword.t()
215
# Parameters:
216
# - form: Phoenix.HTML.Form.t() - Form struct with source and impl
217
# - field: Phoenix.HTML.Form.field() - Field identifier (atom or string)
218
#
219
# Returns:
220
# - Keyword.t() - HTML5 validation attributes
221
#
222
# Delegates to the FormData protocol implementation
223
```
224
225
**Common Validation Attributes:**
226
- `:required` - Field is required
227
- `:minlength` - Minimum string length
228
- `:maxlength` - Maximum string length
229
- `:min` - Minimum numeric value
230
- `:max` - Maximum numeric value
231
- `:pattern` - Regular expression pattern
232
233
**Usage Examples:**
234
235
```elixir
236
# Basic usage (implementation-dependent)
237
form = Phoenix.HTML.FormData.to_form(%{}, as: :user)
238
validations = Phoenix.HTML.Form.input_validations(form, :email)
239
# Returns: [] (Map implementation returns empty list)
240
241
# With Ecto changeset (requires phoenix_ecto)
242
changeset = User.changeset(%User{}, %{})
243
ecto_form = Phoenix.HTML.FormData.to_form(changeset)
244
validations = Phoenix.HTML.Form.input_validations(ecto_form, :email)
245
# Returns: [required: true, type: "email"] (example)
246
```
247
248
#### Field Change Detection
249
250
Compare field values and metadata between two form states to detect changes.
251
252
```elixir { .api }
253
def input_changed?(form1, form2, field) :: boolean()
254
# Parameters:
255
# - form1: Phoenix.HTML.Form.t() - First form state
256
# - form2: Phoenix.HTML.Form.t() - Second form state
257
# - field: Phoenix.HTML.Form.field() - Field identifier (atom or string)
258
#
259
# Returns:
260
# - boolean() - True if field changed between forms
261
#
262
# Compares form implementation, id, name, action, field errors, and field values
263
```
264
265
**Change Detection Criteria:**
266
- Field value changed
267
- Field errors changed
268
- Form action changed
269
- Form implementation changed
270
- Form ID or name changed
271
272
**Usage Examples:**
273
274
```elixir
275
# Compare form states
276
original_form = Phoenix.HTML.FormData.to_form(%{name: "John"})
277
updated_form = Phoenix.HTML.FormData.to_form(%{name: "Jane"})
278
279
changed = Phoenix.HTML.Form.input_changed?(original_form, updated_form, :name)
280
# Returns: true
281
282
# No change detected
283
same_form = Phoenix.HTML.FormData.to_form(%{name: "John"})
284
no_change = Phoenix.HTML.Form.input_changed?(original_form, same_form, :name)
285
# Returns: false
286
```
287
288
### Value Normalization and Processing
289
290
Normalize input values according to their HTML input types and handle type-specific formatting requirements.
291
292
#### Input Value Normalization
293
294
Convert and format values based on HTML input type requirements.
295
296
```elixir { .api }
297
def normalize_value(input_type, value) :: term
298
# Parameters:
299
# - input_type: String.t() - HTML input type
300
# - value: term - Value to normalize
301
#
302
# Returns:
303
# - term - Normalized value appropriate for input type
304
```
305
306
**Supported Input Types:**
307
308
- **`"checkbox"`**: Returns boolean based on "true" string value
309
- **`"datetime-local"`**: Formats DateTime/NaiveDateTime to HTML datetime format
310
- **`"textarea"`**: Prefixes newline to preserve formatting
311
- **Other types**: Returns value unchanged
312
313
**Usage Examples:**
314
315
```elixir
316
# Checkbox normalization
317
Phoenix.HTML.Form.normalize_value("checkbox", "true")
318
# Returns: true
319
320
Phoenix.HTML.Form.normalize_value("checkbox", "false")
321
# Returns: false
322
323
Phoenix.HTML.Form.normalize_value("checkbox", nil)
324
# Returns: false
325
326
# DateTime normalization
327
datetime = ~N[2023-12-25 14:30:45]
328
normalized = Phoenix.HTML.Form.normalize_value("datetime-local", datetime)
329
# Returns: {:safe, ["2023-12-25", ?T, "14:30"]}
330
331
# Textarea normalization (preserves leading newlines)
332
Phoenix.HTML.Form.normalize_value("textarea", "Hello\nWorld")
333
# Returns: {:safe, [?\n, "Hello\nWorld"]}
334
335
# Pass-through for other types
336
Phoenix.HTML.Form.normalize_value("text", "Some text")
337
# Returns: "Some text"
338
```
339
340
### Select Options and Form Controls
341
342
Generate options for select elements with support for grouping, selection state, and complex data structures.
343
344
#### Select Option Generation
345
346
Create HTML option elements from various data structures with selection handling.
347
348
```elixir { .api }
349
def options_for_select(options, selected_values) :: Phoenix.HTML.safe
350
# Parameters:
351
# - options: Enumerable.t() - Options data structure
352
# - selected_values: term | [term] - Currently selected values
353
#
354
# Returns:
355
# - Phoenix.HTML.safe - HTML option elements
356
```
357
358
**Supported Option Formats:**
359
360
1. **Two-element tuples**: `{label, value}`
361
2. **Keyword lists with `:key` and `:value`**: `[key: "Label", value: "value", disabled: true]`
362
3. **Simple values**: Used as both label and value
363
4. **Groups**: `{group_label, [options]}` for optgroup elements
364
5. **Separators**: `:hr` for horizontal rules
365
366
**Usage Examples:**
367
368
```elixir
369
# Simple tuples
370
options = [{"Admin", "admin"}, {"User", "user"}, {"Guest", "guest"}]
371
select_html = Phoenix.HTML.Form.options_for_select(options, "user")
372
# Returns HTML: <option value="admin">Admin</option>
373
# <option value="user" selected>User</option>
374
# <option value="guest">Guest</option>
375
376
# Multiple selections
377
options = [{"Red", "red"}, {"Green", "green"}, {"Blue", "blue"}]
378
select_html = Phoenix.HTML.Form.options_for_select(options, ["red", "blue"])
379
# Returns HTML with red and blue selected
380
381
# With additional attributes
382
options = [
383
[key: "Administrator", value: "admin", disabled: false],
384
[key: "Disabled User", value: "disabled_user", disabled: true]
385
]
386
select_html = Phoenix.HTML.Form.options_for_select(options, nil)
387
# Returns HTML: <option value="admin">Administrator</option>
388
# <option value="disabled_user" disabled>Disabled User</option>
389
390
# Option groups
391
grouped_options = [
392
{"North America", [{"USA", "us"}, {"Canada", "ca"}]},
393
{"Europe", [{"UK", "uk"}, {"Germany", "de"}]}
394
]
395
select_html = Phoenix.HTML.Form.options_for_select(grouped_options, "us")
396
# Returns HTML: <optgroup label="North America">
397
# <option value="us" selected>USA</option>
398
# <option value="ca">Canada</option>
399
# </optgroup>
400
# <optgroup label="Europe">...</optgroup>
401
402
# With separators
403
options = [{"Option 1", "1"}, {"Option 2", "2"}, :hr, {"Option 3", "3"}]
404
select_html = Phoenix.HTML.Form.options_for_select(options, nil)
405
# Returns HTML with <hr/> separator between option groups
406
```
407
408
### Phoenix.HTML.FormField Struct
409
410
Individual form field representation returned by form field access operations.
411
412
```elixir { .api }
413
defmodule Phoenix.HTML.FormField do
414
@enforce_keys [:id, :name, :errors, :field, :form, :value]
415
defstruct [:id, :name, :errors, :field, :form, :value]
416
417
@type t :: %__MODULE__{
418
id: String.t(), # HTML id attribute
419
name: String.t(), # HTML name attribute
420
errors: [term], # Field-specific errors
421
field: Phoenix.HTML.Form.field(), # Original field identifier
422
form: Phoenix.HTML.Form.t(), # Parent form reference
423
value: term # Current field value
424
}
425
end
426
```
427
428
**Usage Examples:**
429
430
```elixir
431
# Create form and access field
432
form = Phoenix.HTML.FormData.to_form(%{name: "John", email: "john@example.com"}, as: :user)
433
name_field = form[:name]
434
435
# Access field properties
436
name_field.id # Returns: "user_name"
437
name_field.name # Returns: "user[name]"
438
name_field.value # Returns: "John"
439
name_field.field # Returns: :name
440
name_field.errors # Returns: []
441
name_field.form # Returns: original form struct
442
```
443
444
## Protocol: Phoenix.HTML.FormData
445
446
The FormData protocol enables any data structure to be converted into form representations, providing flexibility for different data sources like maps, structs, and changesets.
447
448
```elixir { .api }
449
defprotocol Phoenix.HTML.FormData do
450
@spec to_form(t, Keyword.t()) :: Phoenix.HTML.Form.t()
451
def to_form(data, options)
452
453
@spec to_form(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field(), Keyword.t()) :: [Phoenix.HTML.Form.t()]
454
def to_form(data, parent_form, field, options)
455
456
@spec input_value(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field()) :: term
457
def input_value(data, form, field)
458
459
@spec input_validations(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field()) :: Keyword.t()
460
def input_validations(data, form, field)
461
end
462
```
463
464
### FormData Protocol Options
465
466
**Shared Options (all implementations):**
467
468
- **`:as`** - Form name for input naming
469
- **`:id`** - Form ID for HTML attributes
470
471
**Nested Form Options:**
472
473
- **`:default`** - Default value for nested forms
474
- **`:prepend`** - Values to prepend to list forms
475
- **`:append`** - Values to append to list forms
476
- **`:action`** - Form action context
477
- **`:hidden`** - Hidden field specifications
478
479
### Map Implementation
480
481
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.
482
483
```elixir { .api }
484
# Convert map to form
485
Phoenix.HTML.FormData.to_form(map, opts) :: Phoenix.HTML.Form.t()
486
# Parameters:
487
# - map: %{binary => term} - Map with string keys (warns if atom keys)
488
# - opts: Keyword.t() - Options including :as, :id, :errors, :action
489
#
490
# Returns:
491
# - Phoenix.HTML.Form.t() - Form struct with map as params
492
493
# Convert nested field to sub-forms
494
Phoenix.HTML.FormData.to_form(map, parent_form, field, opts) :: [Phoenix.HTML.Form.t()]
495
# Parameters:
496
# - map: %{binary => term} - Parent map data
497
# - parent_form: Phoenix.HTML.Form.t() - Parent form context
498
# - field: Phoenix.HTML.Form.field() - Field name for nested forms
499
# - opts: Keyword.t() - Options including :default, :prepend, :append
500
#
501
# Returns:
502
# - [Phoenix.HTML.Form.t()] - List of forms (cardinality: one or many)
503
504
# Get field value from map
505
def input_value(map, form, field) :: term
506
# Returns parameter value if present, otherwise data value
507
508
# Get validation attributes (Map implementation returns empty list)
509
def input_validations(map, form, field) :: []
510
# Map implementation doesn't provide validation metadata
511
```
512
513
**Map Implementation Characteristics:**
514
515
- **String Keys Required**: Maps must have string keys to represent form parameters
516
- **Parameter Precedence**: Form parameters take precedence over form data
517
- **Cardinality Detection**: Uses `:default` option to determine single vs multiple forms
518
- **No Validations**: Returns empty list for `input_validations/3`
519
- **Atom Key Warning**: Issues warning if atom keys detected in map
520
521
**Usage Examples:**
522
523
```elixir
524
# Basic map form
525
user_data = %{name: "John", email: "john@example.com", active: true}
526
form = Phoenix.HTML.FormData.to_form(user_data, as: :user, id: "user-form")
527
528
# Nested object forms (cardinality: one)
529
profile_data = %{user: %{name: "John", profile: %{bio: "Developer"}}}
530
form = Phoenix.HTML.FormData.to_form(profile_data, as: :data)
531
profile_forms = Phoenix.HTML.FormData.to_form(profile_data, form, :profile, default: %{})
532
profile_form = List.first(profile_forms)
533
534
# Nested list forms (cardinality: many)
535
user_with_addresses = %{name: "John", addresses: []}
536
form = Phoenix.HTML.FormData.to_form(user_with_addresses, as: :user)
537
address_forms = Phoenix.HTML.FormData.to_form(
538
user_with_addresses,
539
form,
540
:addresses,
541
default: [],
542
prepend: [%{street: ""}]
543
)
544
# Returns list of forms for each address
545
546
# With form parameters (simulating user input)
547
form_params = %{"name" => "Jane", "email" => "jane@example.com"}
548
form_with_params = Phoenix.HTML.FormData.to_form(form_params, as: :user)
549
```
550
551
### Custom Protocol Implementation
552
553
Extend FormData protocol for custom data structures:
554
555
```elixir
556
# Example: Custom struct with protocol implementation
557
defmodule UserProfile do
558
defstruct [:user, :preferences, :addresses]
559
end
560
561
defimpl Phoenix.HTML.FormData, for: UserProfile do
562
def to_form(%UserProfile{user: user, preferences: prefs}, opts) do
563
# Convert to form using user data
564
Phoenix.HTML.FormData.to_form(user, opts)
565
end
566
567
def to_form(%UserProfile{addresses: addresses}, form, :addresses, opts) do
568
# Handle nested address forms
569
Phoenix.HTML.FormData.to_form(addresses, form, :addresses, opts)
570
end
571
572
def input_value(%UserProfile{user: user}, form, field) do
573
Phoenix.HTML.FormData.input_value(user, form, field)
574
end
575
576
def input_validations(%UserProfile{}, _form, _field) do
577
[]
578
end
579
end
580
581
# Usage
582
profile = %UserProfile{
583
user: %{name: "John", email: "john@example.com"},
584
addresses: [%{street: "123 Main St", city: "Boston"}]
585
}
586
form = Phoenix.HTML.FormData.to_form(profile, as: :profile)
587
```
588
589
## Error Handling
590
591
### Form Field Errors
592
593
```elixir
594
# Invalid field access
595
form[:invalid_field_type]
596
# Raises: ArgumentError - field must be atom or string
597
598
# Protocol undefined
599
Phoenix.HTML.FormData.to_form(%DateTime{}, [])
600
# Raises: Protocol.UndefinedError - Phoenix.HTML.FormData not implemented
601
```
602
603
### Best Practices
604
605
1. **Field Names**: Use atoms for Ecto changesets, strings for plain maps
606
2. **Nested Forms**: Always provide `:default` values for nested form generation
607
3. **Validation**: Implement `input_validations/3` for HTML5 client-side validation
608
4. **Error Handling**: Include error information in form data for field-level error display
609
5. **Parameters**: Let Phoenix handle form parameter binding automatically
610
6. **Custom Types**: Implement FormData protocol for domain-specific data structures