Forms and Validation
Working with forms is part of every web application. Framefox makes it straightforward to create forms, validate user input, and handle data safely.
You define your forms as Python classes, specify validation rules, and let Framefox handle the rest - including CSRF protection and error handling.
Creating Forms
Section titled âCreating FormsâBasic Form Structure
Section titled âBasic Form StructureâForms in Framefox are created by implementing the FormTypeInterface
and defining fields using the FormBuilder
:
from framefox.core.form.type.form_type_interface import FormTypeInterfacefrom framefox.core.form.form_builder import FormBuilderfrom framefox.core.form.type.text_type import TextTypefrom framefox.core.form.type.email_type import EmailTypefrom framefox.core.form.type.password_type import PasswordType
class UserType(FormTypeInterface): """Form type for user registration and updates."""
def build_form(self, builder: FormBuilder) -> None: """Configure form fields with validation rules.""" builder.add('name', TextType, { 'required': True, 'label': 'Full Name', 'attr': {'placeholder': 'Enter your full name'} })
builder.add('email', EmailType, { 'required': True, 'label': 'Email Address', 'attr': {'placeholder': 'user@example.com'} })
builder.add('password', PasswordType, { 'required': True, 'label': 'Password', 'attr': {'placeholder': 'Minimum 8 characters'} })
def get_options(self) -> dict: """Return form-level options and attributes.""" return { 'attr': {'class': 'needs-validation', 'novalidate': 'novalidate'} }
Form Options and Attributes
Section titled âForm Options and AttributesâForms support various configuration options for enhanced functionality:
from framefox.core.form.type.textarea_type import TextareaType
class ContactType(FormTypeInterface): def build_form(self, builder: FormBuilder) -> None: builder.add('subject', TextType, { 'required': True, 'label': 'Subject', 'attr': { 'class': 'form-control', 'maxlength': '100' }, 'help': 'Brief description of your inquiry' })
builder.add('message', TextareaType, { 'required': True, 'label': 'Message', 'attr': { 'rows': 5, 'placeholder': 'Enter your message here...' } })
def get_options(self) -> dict: return { 'attr': { 'class': 'contact-form', 'data-validate': 'true' }, 'method': 'POST' }
Controller Integration
Section titled âController IntegrationâUsing Forms in Controllers
Section titled âUsing Forms in ControllersâForms integrate seamlessly with Framefox controllers through the create_form()
method:
from framefox.core.routing.decorator.route import Routefrom framefox.core.controller.abstract_controller import AbstractControllerfrom fastapi import Request
from src.forms.user_type import UserTypefrom src.entity.user import User
class UserController(AbstractController): @Route("/register", "user.register", methods=["GET", "POST"]) async def register(self, request: Request): # Create form instance with optional data binding form = self.create_form(UserType)
if request.method == "POST": # Handle form submission with automatic validation await form.handle_request(request)
if form.is_valid(): # Extract validated data data = form.get_data()
# Create new user entity user = User() user.name = data["name"] user.email = data["email"] user.password = self.hash_password(data["password"])
# Save to database await self.get_entity_manager().persist(user) await self.get_entity_manager().flush()
# Flash success message and redirect self.add_flash("success", "Account created successfully!") return self.redirect("user.dashboard") else: # Form has validation errors self.add_flash("error", "Please correct the errors below.")
# Render form (GET request or validation errors) return self.render("user/register.html", {"form": form})
Form Data Binding
Section titled âForm Data BindingâForms can be pre-populated with existing entity data for edit operations:
@Route("/user/{user_id}/edit", "user.edit", methods=["GET", "POST"])async def edit(self, request: Request, user_id: int): # Load existing user user = await self.get_repository("user").find(user_id) if not user: raise HTTPException(status_code=404, detail="User not found")
# Create form with pre-populated data form = self.create_form(UserType, data=user)
if request.method == "POST": await form.handle_request(request)
if form.is_valid(): # Update existing entity with form data updated_data = form.get_data() user.name = updated_data["name"] user.email = updated_data["email"]
await self.get_entity_manager().flush()
self.add_flash("success", "User updated successfully!") return self.redirect("user.show", user_id=user.id)
return self.render("user/edit.html", {"form": form, "user": user})
Error Handling and Flash Messages
Section titled âError Handling and Flash MessagesâForms provide comprehensive error handling with field-level and form-level validation:
@Route("/contact", "contact.submit", methods=["GET", "POST"])async def contact(self, request: Request): form = self.create_form(ContactType)
if request.method == "POST": await form.handle_request(request)
if form.is_valid(): try: # Process form data await self.get_service("email").send_contact_email(form.get_data()) self.add_flash("success", "Message sent successfully!") return self.redirect("contact.thanks")
except Exception as e: # Handle business logic errors self.add_flash("error", "Failed to send message. Please try again.") form.add_error("general", str(e)) else: # Validation errors are automatically available in template self.add_flash("error", "Please correct the errors below.")
return self.render("contact/form.html", {"form": form})
Template Rendering
Section titled âTemplate RenderingâForm Rendering Functions
Section titled âForm Rendering FunctionsâFramefox provides powerful template functions for rendering forms with automatic CSRF protection and error handling:
<!DOCTYPE html><html><head> <title>User Registration</title> <link href="{{ asset('css/forms.css') }}" rel="stylesheet"></head><body> <div class="container"> <h1>Create Account</h1>
{{ form_start(form, {'action': url_for('user.register')}) }} {{ csrf_token() }}
<!-- Render complete field with label, input, and errors --> {{ form_row(form, 'name') }} {{ form_row(form, 'email') }} {{ form_row(form, 'password') }}
<div class="form-actions"> <button type="submit" class="btn btn-primary"> Create Account </button> <a href="{{ url_for('home') }}" class="btn btn-secondary"> Cancel </a> </div> {{ form_end(form) }} </div></body></html>
Individual Field Rendering
Section titled âIndividual Field RenderingâFor more control over form layout, render individual components:
<!-- Custom form layout with individual components --><form method="POST" action="{{ url_for('user.register') }}"> {{ csrf_token() }}
<div class="row"> <div class="col-md-6"> <div class="form-group"> {{ form_label(form, 'name') }} {{ form_widget(form, 'name', {'attr': {'class': 'form-control'}}) }} {{ form_errors(form, 'name') }} </div> </div>
<div class="col-md-6"> <div class="form-group"> {{ form_label(form, 'email') }} {{ form_widget(form, 'email') }} {{ form_errors(form, 'email') }} </div> </div> </div>
<div class="form-group"> {{ form_label(form, 'password') }} {{ form_widget(form, 'password') }} {{ form_errors(form, 'password') }} <small class="form-text text-muted"> Must be at least 8 characters long </small> </div>
<button type="submit" class="btn btn-primary">Register</button></form>
Form Rendering Functions Reference
Section titled âForm Rendering Functions ReferenceâFunction | Purpose | Example |
---|---|---|
form_start(form, options) | Opens form tag with attributes | form_start(form, {'method': 'POST'}) |
form_end(form) | Closes form tag | form_end(form) |
form_row(form, field) | Complete field with label and errors | form_row(form, 'email') |
form_label(form, field) | Field label only | form_label(form, 'name') |
form_widget(form, field) | Field input only | form_widget(form, 'password') |
form_errors(form, field) | Field errors only | form_errors(form, 'email') |
Available Field Types
Section titled âAvailable Field TypesâBasic Field Types
Section titled âBasic Field TypesâFramefox provides comprehensive field types for all common form inputs:
Type | Purpose | Features |
---|---|---|
TextType | Basic text input | String validation, length constraints |
EmailType | Email address input | Built-in email format validation |
PasswordType | Password input | Masked input, strength validation |
NumberType | Numeric input | Integer/float validation, min/max constraints |
TextareaType | Multi-line text | Configurable rows, character limits |
CheckboxType | Boolean checkbox | True/false values |
DateTimeType | Date/time picker | Native HTML5 datetime-local support |
Choice and Selection Types
Section titled âChoice and Selection Typesâfrom framefox.core.form.type.select_type import SelectTypefrom framefox.core.form.type.choice_type import ChoiceType
class ProfileType(FormTypeInterface): def build_form(self, builder: FormBuilder) -> None: # Dropdown selection builder.add('country', SelectType, { 'required': True, 'label': 'Country', 'choices': { 'us': 'United States', 'ca': 'Canada', 'uk': 'United Kingdom', 'fr': 'France' }, 'empty_label': 'Select your country...' })
# Radio buttons (expanded choice) builder.add('gender', ChoiceType, { 'required': False, 'label': 'Gender', 'choices': { 'male': 'Male', 'female': 'Female', 'other': 'Other' }, 'expanded': True, # Renders as radio buttons 'multiple': False })
# Multiple checkboxes builder.add('interests', ChoiceType, { 'required': False, 'label': 'Interests', 'choices': { 'tech': 'Technology', 'sports': 'Sports', 'music': 'Music', 'travel': 'Travel' }, 'expanded': True, # Renders as checkboxes 'multiple': True # Allows multiple selections })
File Upload Type
Section titled âFile Upload TypeâThe FileType
provides secure file upload functionality with validation:
from framefox.core.form.type.file_type import FileType
class DocumentType(FormTypeInterface): def build_form(self, builder: FormBuilder) -> None: builder.add('title', TextType, { 'required': True, 'label': 'Document Title' })
builder.add('document', FileType, { 'required': True, 'label': 'Upload File', 'accept': 'image/*,.pdf,.doc,.docx', 'allowed_extensions': ['.jpg', '.jpeg', '.png', '.pdf', '.doc', '.docx'], 'max_file_size': 5 * 1024 * 1024, # 5MB 'storage_path': 'public/documents', 'rename': True, # Generate unique filename 'help': 'Accepted formats: Images, PDF, Word documents. Max size: 5MB' })
Entity Relationship Type
Section titled âEntity Relationship TypeâThe EntityType
enables forms to work directly with database entities:
from framefox.core.form.type.entity_type import EntityType
class OrderType(FormTypeInterface): def build_form(self, builder: FormBuilder) -> None: # Single entity selection builder.add('customer', EntityType, { 'class': 'Customer', 'required': True, 'label': 'Customer', 'choice_label': 'name', # Display customer name 'show_id': False # Hide ID in display })
# Multiple entity selection builder.add('products', EntityType, { 'class': 'Product', 'multiple': True, 'required': False, 'label': 'Products', 'choice_label': 'name', 'show_id': True })
Collection Type
Section titled âCollection TypeâHandle dynamic collections of form data:
from framefox.core.form.type.collection_type import CollectionType
class InvoiceType(FormTypeInterface): def build_form(self, builder: FormBuilder) -> None: builder.add('invoice_number', TextType, { 'required': True, 'label': 'Invoice Number' })
# Collection of line items builder.add('line_items', CollectionType, { 'entry_type': TextType, 'entry_options': { 'label': 'Item Description' }, 'allow_add': True, 'allow_delete': True, 'label': 'Line Items' })
Form Validation
Section titled âForm ValidationâBuilt-in Validation
Section titled âBuilt-in ValidationâForms automatically validate field types and constraints:
class RegistrationType(FormTypeInterface): def build_form(self, builder: FormBuilder) -> None: builder.add('username', TextType, { 'required': True, 'label': 'Username', 'attr': { 'minlength': '3', 'maxlength': '20', 'pattern': '[a-zA-Z0-9_]+' } })
builder.add('age', NumberType, { 'required': True, 'label': 'Age', 'attr': { 'min': '18', 'max': '120', 'step': '1' } })
builder.add('email', EmailType, { 'required': True, 'label': 'Email Address' # Automatic email format validation })
Custom Field Validation
Section titled âCustom Field ValidationâImplement custom validation logic for specific business rules:
from framefox.core.form.type.text_type import TextType
class CustomPasswordType(TextType): """Custom password field with strength validation."""
def transform_to_model(self, value): """Validate password strength before transformation.""" if not value: return value
# Check password strength if len(value) < 8: raise ValueError("Password must be at least 8 characters long")
if not any(c.isupper() for c in value): raise ValueError("Password must contain at least one uppercase letter")
if not any(c.isdigit() for c in value): raise ValueError("Password must contain at least one digit")
if not any(not c.isalnum() for c in value): raise ValueError("Password must contain at least one special character")
return value
def get_block_prefix(self) -> str: return "password"
# Use custom field typeclass SecureUserType(FormTypeInterface): def build_form(self, builder: FormBuilder) -> None: builder.add('password', CustomPasswordType, { 'required': True, 'label': 'Secure Password' })
Form-Level Validation
Section titled âForm-Level ValidationâImplement cross-field validation at the form level:
class PasswordChangeType(FormTypeInterface): def build_form(self, builder: FormBuilder) -> None: builder.add('current_password', PasswordType, { 'required': True, 'label': 'Current Password' })
builder.add('new_password', PasswordType, { 'required': True, 'label': 'New Password' })
builder.add('confirm_password', PasswordType, { 'required': True, 'label': 'Confirm New Password' })
def validate(self, form) -> bool: """Custom form validation.""" if not super().validate(form): return False
data = form.get_data()
# Check if new passwords match if data['new_password'] != data['confirm_password']: form.add_error('confirm_password', 'Passwords do not match') return False
# Check if new password is different from current if data['current_password'] == data['new_password']: form.add_error('new_password', 'New password must be different from current password') return False
return True
File Upload Handling
Section titled âFile Upload HandlingâController File Processing
Section titled âController File ProcessingâHandle uploaded files securely in your controllers:
from fastapi import UploadFileimport os
@Route("/upload", "document.upload", methods=["GET", "POST"])async def upload_document(self, request: Request): form = self.create_form(DocumentType)
if request.method == "POST": await form.handle_request(request)
if form.is_valid(): data = form.get_data() uploaded_file = data["document"]
if uploaded_file: try: # File is automatically saved by FileType # Get the saved file path file_path = uploaded_file # FileType returns the saved path
# Save file metadata to database document = Document() document.title = data["title"] document.file_path = file_path document.original_name = uploaded_file.filename document.file_size = uploaded_file.size document.content_type = uploaded_file.content_type
await self.get_entity_manager().persist(document) await self.get_entity_manager().flush()
self.add_flash("success", "Document uploaded successfully!") return self.redirect("document.list")
except Exception as e: self.add_flash("error", f"Upload failed: {str(e)}")
return self.render("document/upload.html", {"form": form})
File Upload Configuration
Section titled âFile Upload ConfigurationâConfigure file uploads with security best practices:
class AvatarUploadType(FormTypeInterface): def build_form(self, builder: FormBuilder) -> None: builder.add('avatar', FileType, { 'required': False, 'label': 'Profile Picture', 'accept': 'image/*', 'allowed_extensions': ['.jpg', '.jpeg', '.png', '.gif'], 'max_file_size': 2 * 1024 * 1024, # 2MB 'storage_path': 'public/avatars', 'rename': True, 'attr': { 'class': 'form-control-file' }, 'help': 'Upload a profile picture (JPG, PNG, GIF). Max size: 2MB.' })
CRUD Generation
Section titled âCRUD GenerationâUsing the Create CRUD Command
Section titled âUsing the Create CRUD CommandâFramefox can automatically generate complete CRUD forms based on your entities:
framefox create crud
This interactive command will:
- Select Entity: Choose from existing entities in your project
- Generate Form Type: Create a form class with all entity fields
- Create Controller: Generate CRUD controller with form handling
- Generate Templates: Create complete view templates for all operations
Generated Form Example
Section titled âGenerated Form ExampleâWhen you run framefox create crud
on a User
entity, it generates:
from framefox.core.form.type.form_type_interface import FormTypeInterfacefrom framefox.core.form.form_builder import FormBuilderfrom framefox.core.form.type.text_type import TextTypefrom framefox.core.form.type.email_type import EmailTypefrom framefox.core.form.type.checkbox_type import CheckboxTypefrom framefox.core.form.type.entity_type import EntityType
class UserType(FormTypeInterface): """Form for User entity."""
def build_form(self, builder: FormBuilder) -> None: """Configure form fields.""" builder.add('name', TextType, { 'required': True, 'label': 'Name', })
builder.add('email', EmailType, { 'required': True, 'label': 'Email', })
builder.add('active', CheckboxType, { 'required': False, 'label': 'Active', })
builder.add('role', EntityType, { 'class': 'Role', 'required': True, 'label': 'Role', 'choice_label': 'name', 'show_id': True, })
def get_options(self) -> dict: return { 'attr': {'class': 'needs-validation', 'novalidate': 'novalidate'} }
Generated Controller with Forms
Section titled âGenerated Controller with FormsâThe CRUD command also generates a complete controller with form handling:
from framefox.core.routing.decorator.route import Routefrom framefox.core.controller.abstract_controller import AbstractControllerfrom fastapi import Request
from src.forms.user_type import UserTypefrom src.entity.user import User
class UserController(AbstractController): @Route("/user/create", "user.create", methods=["GET", "POST"]) async def create(self, request: Request): form = self.create_form(UserType)
if request.method == "POST": await form.handle_request(request)
if form.is_valid(): data = form.get_data() user = User()
# Map form data to entity for field_name, value in data.items(): setattr(user, field_name, value)
await self.get_entity_manager().persist(user) await self.get_entity_manager().flush()
self.add_flash("success", "User created successfully!") return self.redirect("user.index")
return self.render("user/create.html", {"form": form})
@Route("/user/{user_id}/edit", "user.edit", methods=["GET", "POST"]) async def edit(self, request: Request, user_id: int): user = await self.get_repository("user").find(user_id) if not user: raise HTTPException(status_code=404)
form = self.create_form(UserType, data=user)
if request.method == "POST": await form.handle_request(request)
if form.is_valid(): data = form.get_data()
# Update entity with form data for field_name, value in data.items(): setattr(user, field_name, value)
await self.get_entity_manager().flush()
self.add_flash("success", "User updated successfully!") return self.redirect("user.index")
return self.render("user/edit.html", {"form": form, "user": user})
Advanced Form Features
Section titled âAdvanced Form FeaturesâDynamic Form Fields
Section titled âDynamic Form FieldsâCreate forms that adapt based on user input or conditions:
class ConditionalFormType(FormTypeInterface): def build_form(self, builder: FormBuilder) -> None: builder.add('user_type', SelectType, { 'required': True, 'label': 'User Type', 'choices': { 'individual': 'Individual', 'business': 'Business' } })
builder.add('name', TextType, { 'required': True, 'label': 'Full Name' })
# Conditional fields based on user type # These would be shown/hidden via JavaScript builder.add('company_name', TextType, { 'required': False, 'label': 'Company Name', 'attr': { 'data-show-when': 'user_type=business' } })
builder.add('tax_id', TextType, { 'required': False, 'label': 'Tax ID', 'attr': { 'data-show-when': 'user_type=business' } })
Form Themes and Customization
Section titled âForm Themes and CustomizationâCustomize form rendering with themes and custom templates:
<!-- Custom form theme template --><!-- templates/forms/custom_theme.html -->
{% macro form_row(form, field_name) %} {% set field = form.get_field(field_name) %} <div class="custom-form-group {{ 'has-error' if field.has_errors() else '' }}"> <label for="{{ field.get_id() }}" class="custom-label"> {{ field.options.label }} {% if field.options.required %} <span class="required-indicator">*</span> {% endif %} </label>
<div class="custom-input-wrapper"> {{ form_widget(form, field_name) }} {% if field.options.help %} <small class="custom-help-text">{{ field.options.help }}</small> {% endif %} </div>
{% if field.has_errors() %} <div class="custom-error-messages"> {% for error in field.get_errors() %} <span class="custom-error">{{ error }}</span> {% endfor %} </div> {% endif %} </div>{% endmacro %}
Best Practices
Section titled âBest PracticesâForm Organization
Section titled âForm OrganizationâStructure your forms for maintainability and reusability:
# src/forms/base/base_form.pyclass BaseFormType(FormTypeInterface): """Base form with common functionality."""
def get_options(self) -> dict: return { 'attr': { 'class': 'needs-validation', 'novalidate': 'novalidate' } }
# src/forms/user/user_registration_type.pyclass UserRegistrationType(BaseFormType): """Specific form for user registration."""
def build_form(self, builder: FormBuilder) -> None: # Registration-specific fields pass
# src/forms/user/user_profile_type.pyclass UserProfileType(BaseFormType): """Form for user profile editing."""
def build_form(self, builder: FormBuilder) -> None: # Profile-specific fields pass
Performance Optimization
Section titled âPerformance OptimizationâOptimize forms for better performance:
class OptimizedFormType(FormTypeInterface): def build_form(self, builder: FormBuilder) -> None: # Use lazy loading for entity choices builder.add('category', EntityType, { 'class': 'Category', 'choice_label': 'name', 'query_builder': self._get_category_query # Custom query })
def _get_category_query(self): """Custom query for better performance.""" return self.get_repository('category').createQueryBuilder('c').where('c.active = :active').setParameter('active', True)
Testing Forms
Section titled âTesting FormsâWrite comprehensive tests for your forms:
import pytestfrom src.forms.user_type import UserType
class TestUserType: def test_form_validation_success(self): """Test successful form validation.""" form = UserType() form_builder = FormBuilder() form.build_form(form_builder)
# Simulate valid form data form_instance = form_builder.get_form() form_instance.fields['name'].set_value('John Doe') form_instance.fields['email'].set_value('john@example.com') form_instance.fields['password'].set_value('SecurePass123!')
assert form_instance.validate() == True
def test_form_validation_failure(self): """Test form validation with invalid data.""" form = UserType() form_builder = FormBuilder() form.build_form(form_builder)
form_instance = form_builder.get_form() form_instance.fields['email'].set_value('invalid-email')
assert form_instance.validate() == False assert len(form_instance.fields['email'].errors) > 0
Forms in Framefox provide a robust, secure, and flexible foundation for handling user input in your web applications. By following the patterns and best practices outlined in this guide, you can create maintainable and secure forms that scale with your applicationâs needs.