0
# Form Handling and Data Processing
1
2
Comprehensive form handling including data conversion, validation, file uploads, and dataclass integration.
3
4
## Capabilities
5
6
### Form Data Processing
7
8
Convert and process form data from HTTP requests into Python objects.
9
10
```python { .api }
11
def form2dict(form) -> dict:
12
"""
13
Convert FormData to dictionary.
14
15
Processes multipart form data and URL-encoded form data
16
into Python dictionary format for easy handling.
17
18
Args:
19
form: FormData object from HTTP request
20
21
Returns:
22
dict: Converted form data with string keys and values
23
"""
24
```
25
26
### Form Population and Data Binding
27
28
Fill forms with data objects and bind data to form fields.
29
30
```python { .api }
31
def fill_form(form, obj):
32
"""
33
Fill form with data object.
34
35
Populates form fields with values from an object,
36
matching field names to object attributes.
37
38
Args:
39
form: HTML form element to populate
40
obj: Object with data to fill form fields
41
42
Returns:
43
Form element with populated values
44
"""
45
46
def fill_dataclass(src, dest):
47
"""
48
Fill dataclass from source object.
49
50
Populates dataclass fields from source object attributes,
51
useful for form data binding and validation.
52
53
Args:
54
src: Source object with data
55
dest: Destination dataclass to populate
56
57
Returns:
58
Populated dataclass instance
59
"""
60
```
61
62
### Form Element Discovery
63
64
Find and analyze form elements for processing and validation.
65
66
```python { .api }
67
def find_inputs(form):
68
"""
69
Find input elements in form.
70
71
Scans form element tree to locate all input, select,
72
and textarea elements for processing.
73
74
Args:
75
form: HTML form element to scan
76
77
Returns:
78
list: List of input elements found in form
79
"""
80
```
81
82
### File Upload Handling
83
84
Handle file uploads with proper encoding and validation.
85
86
```python { .api }
87
def File(*c, **kw):
88
"""
89
Handle file uploads in forms.
90
91
Creates file input element with proper multipart encoding
92
and file handling capabilities.
93
94
Args:
95
*c: Child content (usually none for file inputs)
96
**kw: Input attributes including:
97
- name: Field name for form submission
98
- accept: Allowed file types (e.g., '.jpg,.png,.pdf')
99
- multiple: Allow multiple file selection
100
- required: Make field required
101
102
Returns:
103
File input element with upload handling
104
"""
105
```
106
107
### Enhanced Form Components
108
109
Extended form components with additional functionality and validation.
110
111
```python { .api }
112
def Hidden(*c, **kw):
113
"""
114
Hidden input field.
115
116
Creates hidden input for storing form state and data
117
that should not be visible to users.
118
119
Args:
120
*c: Content (usually none)
121
**kw: Input attributes (name, value, etc.)
122
123
Returns:
124
Hidden input element
125
"""
126
127
def CheckboxX(*c, **kw):
128
"""
129
Enhanced checkbox with label.
130
131
Creates checkbox input with associated label
132
for better accessibility and styling.
133
134
Args:
135
*c: Label content
136
**kw: Input attributes and styling options
137
138
Returns:
139
Checkbox with label wrapper
140
"""
141
```
142
143
## Usage Examples
144
145
### Basic Form Processing
146
147
```python
148
from fasthtml.common import *
149
150
app, rt = fast_app()
151
152
@rt('/contact')
153
def contact_form():
154
return Titled("Contact Form",
155
Form(
156
Div(
157
Label("Name:", for_="name"),
158
Input(type="text", name="name", id="name", required=True),
159
cls="form-group"
160
),
161
Div(
162
Label("Email:", for_="email"),
163
Input(type="email", name="email", id="email", required=True),
164
cls="form-group"
165
),
166
Div(
167
Label("Message:", for_="message"),
168
Textarea(name="message", id="message", rows="5", required=True),
169
cls="form-group"
170
),
171
Button("Send Message", type="submit"),
172
method="post",
173
action="/contact/submit"
174
)
175
)
176
177
@rt('/contact/submit', methods=['POST'])
178
def submit_contact(request):
179
# Convert form data to dictionary
180
form_data = form2dict(await request.form())
181
182
# Process the form data
183
name = form_data.get('name', '')
184
email = form_data.get('email', '')
185
message = form_data.get('message', '')
186
187
# Validate and save (simplified)
188
if not all([name, email, message]):
189
return Div("All fields are required", style="color: red;")
190
191
# Save to database or send email here
192
193
return Div(
194
H2("Thank you!"),
195
P(f"Thanks {name}, we received your message and will respond to {email} soon."),
196
A("Send another message", href="/contact")
197
)
198
```
199
200
### File Upload Form
201
202
```python
203
from fasthtml.common import *
204
205
app, rt = fast_app()
206
207
@rt('/upload')
208
def upload_form():
209
return Titled("File Upload",
210
Form(
211
Div(
212
Label("Profile Picture:", for_="profile"),
213
File(
214
name="profile",
215
id="profile",
216
accept=".jpg,.jpeg,.png,.gif",
217
required=True
218
),
219
cls="form-group"
220
),
221
Div(
222
Label("Documents:", for_="documents"),
223
File(
224
name="documents",
225
id="documents",
226
accept=".pdf,.doc,.docx,.txt",
227
multiple=True
228
),
229
cls="form-group"
230
),
231
Div(
232
Label("Description:", for_="description"),
233
Textarea(name="description", id="description", rows="3"),
234
cls="form-group"
235
),
236
Button("Upload Files", type="submit"),
237
method="post",
238
action="/upload/process",
239
enctype="multipart/form-data"
240
)
241
)
242
243
@rt('/upload/process', methods=['POST'])
244
def process_upload(request):
245
form = await request.form()
246
form_data = form2dict(form)
247
248
# Handle uploaded files
249
files_info = []
250
251
# Profile picture (single file)
252
if 'profile' in form:
253
profile_file = form['profile']
254
if profile_file.filename:
255
# Save file and get info
256
filename = save_uploaded_file(profile_file, 'profiles')
257
files_info.append(f"Profile: {filename}")
258
259
# Documents (multiple files)
260
if 'documents' in form:
261
documents = form.getlist('documents')
262
for doc in documents:
263
if doc.filename:
264
filename = save_uploaded_file(doc, 'documents')
265
files_info.append(f"Document: {filename}")
266
267
return Div(
268
H2("Files Uploaded Successfully"),
269
Ul(*[Li(info) for info in files_info]),
270
P(f"Description: {form_data.get('description', 'None provided')}"),
271
A("Upload more files", href="/upload")
272
)
273
274
def save_uploaded_file(file, folder):
275
"""Helper function to save uploaded files"""
276
import os
277
import uuid
278
279
# Create unique filename
280
file_ext = os.path.splitext(file.filename)[1]
281
unique_filename = f"{uuid.uuid4()}{file_ext}"
282
283
# Save file (simplified - add proper error handling)
284
save_path = f"uploads/{folder}/{unique_filename}"
285
os.makedirs(os.path.dirname(save_path), exist_ok=True)
286
287
with open(save_path, "wb") as f:
288
f.write(file.file.read())
289
290
return unique_filename
291
```
292
293
### Dynamic Form with Validation
294
295
```python
296
from fasthtml.common import *
297
from dataclasses import dataclass
298
from typing import Optional
299
300
@dataclass
301
class UserRegistration:
302
username: str
303
email: str
304
password: str
305
confirm_password: str
306
newsletter: bool = False
307
age: Optional[int] = None
308
309
app, rt = fast_app()
310
311
@rt('/register')
312
def registration_form(errors: dict = None):
313
errors = errors or {}
314
315
return Titled("User Registration",
316
Form(
317
Div(
318
Label("Username:", for_="username"),
319
Input(
320
type="text",
321
name="username",
322
id="username",
323
required=True,
324
hx_post="/validate/username",
325
hx_target="#username-error",
326
hx_trigger="blur"
327
),
328
Div(id="username-error",
329
errors.get('username', ''),
330
style="color: red;" if errors.get('username') else ""
331
),
332
cls="form-group"
333
),
334
Div(
335
Label("Email:", for_="email"),
336
Input(
337
type="email",
338
name="email",
339
id="email",
340
required=True,
341
hx_post="/validate/email",
342
hx_target="#email-error",
343
hx_trigger="blur"
344
),
345
Div(id="email-error",
346
errors.get('email', ''),
347
style="color: red;" if errors.get('email') else ""
348
),
349
cls="form-group"
350
),
351
Div(
352
Label("Password:", for_="password"),
353
Input(type="password", name="password", id="password", required=True),
354
Div(id="password-error",
355
errors.get('password', ''),
356
style="color: red;" if errors.get('password') else ""
357
),
358
cls="form-group"
359
),
360
Div(
361
Label("Confirm Password:", for_="confirm_password"),
362
Input(type="password", name="confirm_password", id="confirm_password", required=True),
363
Div(id="confirm-password-error",
364
errors.get('confirm_password', ''),
365
style="color: red;" if errors.get('confirm_password') else ""
366
),
367
cls="form-group"
368
),
369
Div(
370
Label("Age (optional):", for_="age"),
371
Input(type="number", name="age", id="age", min="13", max="120"),
372
cls="form-group"
373
),
374
Div(
375
Label(
376
CheckboxX(name="newsletter", value="yes"),
377
" Subscribe to newsletter"
378
),
379
cls="form-group"
380
),
381
Button("Register", type="submit"),
382
method="post",
383
action="/register/submit"
384
)
385
)
386
387
@rt('/validate/username', methods=['POST'])
388
def validate_username(username: str):
389
if len(username) < 3:
390
return Div("Username must be at least 3 characters", style="color: red;")
391
elif username.lower() in ['admin', 'root', 'user', 'test']:
392
return Div("Username not available", style="color: red;")
393
else:
394
return Div("✓ Username available", style="color: green;")
395
396
@rt('/validate/email', methods=['POST'])
397
def validate_email(email: str):
398
import re
399
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
400
401
if not re.match(email_pattern, email):
402
return Div("Invalid email format", style="color: red;")
403
else:
404
return Div("✓ Email format valid", style="color: green;")
405
406
@rt('/register/submit', methods=['POST'])
407
def submit_registration(request):
408
form_data = form2dict(await request.form())
409
errors = {}
410
411
# Validation
412
if len(form_data.get('username', '')) < 3:
413
errors['username'] = "Username must be at least 3 characters"
414
415
if not form_data.get('email'):
416
errors['email'] = "Email is required"
417
418
password = form_data.get('password', '')
419
confirm_password = form_data.get('confirm_password', '')
420
421
if len(password) < 8:
422
errors['password'] = "Password must be at least 8 characters"
423
424
if password != confirm_password:
425
errors['confirm_password'] = "Passwords do not match"
426
427
# If validation fails, show form with errors
428
if errors:
429
return registration_form(errors)
430
431
# Create user registration object
432
registration = UserRegistration(
433
username=form_data['username'],
434
email=form_data['email'],
435
password=form_data['password'],
436
confirm_password=form_data['confirm_password'],
437
newsletter=form_data.get('newsletter') == 'yes',
438
age=int(form_data['age']) if form_data.get('age') else None
439
)
440
441
# Save registration (simplified)
442
# save_user_registration(registration)
443
444
return Div(
445
H2("Registration Successful!"),
446
P(f"Welcome {registration.username}!"),
447
P(f"We've sent a confirmation email to {registration.email}"),
448
A("Login", href="/login")
449
)
450
```
451
452
### Multi-Step Form
453
454
```python
455
from fasthtml.common import *
456
457
app, rt = fast_app(session_cookie='multistep')
458
459
@rt('/multistep')
460
def multistep_form(step: int = 1):
461
session = request.session
462
463
if step == 1:
464
return step1_form()
465
elif step == 2:
466
return step2_form(session)
467
elif step == 3:
468
return step3_form(session)
469
else:
470
return Redirect('/multistep')
471
472
def step1_form():
473
return Titled("Step 1: Personal Information",
474
Form(
475
H2("Personal Information"),
476
Div(
477
Label("First Name:", for_="first_name"),
478
Input(type="text", name="first_name", required=True),
479
cls="form-group"
480
),
481
Div(
482
Label("Last Name:", for_="last_name"),
483
Input(type="text", name="last_name", required=True),
484
cls="form-group"
485
),
486
Div(
487
Label("Date of Birth:", for_="birth_date"),
488
Input(type="date", name="birth_date", required=True),
489
cls="form-group"
490
),
491
Button("Next", type="submit"),
492
method="post",
493
action="/multistep/step1"
494
)
495
)
496
497
@rt('/multistep/step1', methods=['POST'])
498
def process_step1(request):
499
form_data = form2dict(await request.form())
500
request.session['step1'] = form_data
501
return Redirect('/multistep?step=2')
502
503
def step2_form(session):
504
return Titled("Step 2: Contact Information",
505
Form(
506
H2("Contact Information"),
507
P(f"Hello {session.get('step1', {}).get('first_name', 'there')}!"),
508
Div(
509
Label("Email:", for_="email"),
510
Input(type="email", name="email", required=True),
511
cls="form-group"
512
),
513
Div(
514
Label("Phone:", for_="phone"),
515
Input(type="tel", name="phone"),
516
cls="form-group"
517
),
518
Div(
519
Label("Address:", for_="address"),
520
Textarea(name="address", rows="3"),
521
cls="form-group"
522
),
523
Div(
524
Button("Previous", type="button", onclick="location.href='/multistep?step=1'"),
525
Button("Next", type="submit"),
526
cls="form-buttons"
527
),
528
method="post",
529
action="/multistep/step2"
530
)
531
)
532
533
@rt('/multistep/step2', methods=['POST'])
534
def process_step2(request):
535
form_data = form2dict(await request.form())
536
request.session['step2'] = form_data
537
return Redirect('/multistep?step=3')
538
539
def step3_form(session):
540
step1_data = session.get('step1', {})
541
step2_data = session.get('step2', {})
542
543
return Titled("Step 3: Review and Submit",
544
Div(
545
H2("Review Your Information"),
546
547
H3("Personal Information"),
548
P(f"Name: {step1_data.get('first_name', '')} {step1_data.get('last_name', '')}"),
549
P(f"Date of Birth: {step1_data.get('birth_date', '')}"),
550
551
H3("Contact Information"),
552
P(f"Email: {step2_data.get('email', '')}"),
553
P(f"Phone: {step2_data.get('phone', 'Not provided')}"),
554
P(f"Address: {step2_data.get('address', 'Not provided')}"),
555
556
Form(
557
Div(
558
Label(
559
CheckboxX(name="terms", value="agreed", required=True),
560
" I agree to the terms and conditions"
561
),
562
cls="form-group"
563
),
564
Div(
565
Button("Previous", type="button", onclick="location.href='/multistep?step=2'"),
566
Button("Submit", type="submit"),
567
cls="form-buttons"
568
),
569
method="post",
570
action="/multistep/submit"
571
)
572
)
573
)
574
575
@rt('/multistep/submit', methods=['POST'])
576
def submit_multistep(request):
577
# Combine all form data
578
step1_data = request.session.get('step1', {})
579
step2_data = request.session.get('step2', {})
580
581
# Process final submission
582
complete_data = {**step1_data, **step2_data}
583
584
# Save to database here
585
586
# Clear session
587
request.session.clear()
588
589
return Div(
590
H2("Submission Complete!"),
591
P("Thank you for completing the form."),
592
P("Your information has been saved successfully."),
593
A("Start Over", href="/multistep")
594
)
595
```