or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

forms.mdhtml-safety.mdindex.mdjavascript.md

forms.mddocs/

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