Skip to main content

Forms

Framefox provides a powerful form system for handling user input, validation, and data binding. Forms are built using the FormFactory and can be easily integrated into your controllers.

Creating Forms

Form Type Classes

Create a form by extending the FormTypeInterface and defining fields in the build_form() method:
from framefox.core.form.form_builder import FormBuilder
from framefox.core.form.type.form_type_interface import FormTypeInterface
from framefox.core.form.type.text_type import TextType
from framefox.core.form.type.email_type import EmailType
from framefox.core.form.type.password_type import PasswordType

class UserRegistrationForm(FormTypeInterface):
    def build_form(self, builder: FormBuilder):
        builder.add("username", TextType, {
            "label": "Username",
            "required": True
        })
        
        builder.add("email", EmailType, {
            "label": "Email Address",
            "required": True
        })
        
        builder.add("password", PasswordType, {
            "label": "Password",
            "required": True
        })
        
        builder.add("confirm_password", PasswordType, {
            "label": "Confirm Password",
            "required": True
        })

Using FormFactory

The FormFactory class provides methods to create forms:

create_form()

Creates a form instance from a form type class.
form_type
Type[FormTypeInterface]
required
The form type class that defines the form structure
data
Any
default:"None"
Optional entity instance or dictionary to bind to the form. Form fields will be populated with data from this object.
options
Dict[str, Any]
default:"None"
Additional options to pass to the form
Returns: Form instance
from framefox.core.form.form_factory import FormFactory
from app.forms.user_form import UserForm
from app.models.user import User

# Create empty form
form = FormFactory.create_form(UserForm)

# Create form with existing data
user = User.find(123)
form = FormFactory.create_form(UserForm, user)

create_builder()

Creates a form builder for manual form construction.
data
Any
default:"None"
Optional entity instance to bind to the form
options
Dict[str, Any]
default:"None"
Additional options for the builder
Returns: FormBuilder instance
builder = FormFactory.create_builder(user)
builder.add("name", TextType, {"label": "Name"})
form = builder.get_form()

Using Forms in Controllers

Basic Form Handling

from framefox.core.controller.abstract_controller import AbstractController
from framefox.core.routing.decorator.route import Route
from fastapi import Request
from app.forms.user_form import UserRegistrationForm
from app.models.user import User

class UserController(AbstractController):
    @Route(path="/register", name="user_register", methods=["GET", "POST"])
    async def register(self, request: Request):
        # Create form with empty user entity
        user = User()
        form = self.create_form(UserRegistrationForm, user)
        
        # Handle form submission
        if await form.handle_request(request):
            # Form is valid and data is bound to user entity
            user.save()
            
            self.flash("success", "Registration successful!")
            return self.redirect("/login")
        
        # Render form (with errors if submission failed)
        return self.render("user/register.html", {
            "form": form.create_view()
        })

Create Form Method

The AbstractController provides a create_form() method:
form_type_class
class
required
The form type class to instantiate
entity_instance
object
required
The entity object to bind to the form
Returns: Form instance bound to the entity
user = User()
form = self.create_form(UserForm, user)

Form Validation

Automatic Validation

Forms are automatically validated when handle_request() is called:
if await form.handle_request(request):
    # Form is valid
    data = form.get_data()
else:
    # Form has errors
    errors = form.errors

Required Fields

Mark fields as required to ensure they’re filled:
builder.add("email", EmailType, {
    "label": "Email",
    "required": True  # This field must be filled
})

Custom Validation

Add custom validation logic after form submission:
@Route(path="/user/edit", name="user_edit", methods=["GET", "POST"])
async def edit_user(self, request: Request):
    user = self.get_user()
    form = self.create_form(UserEditForm, user)
    
    if await form.handle_request(request):
        # Custom validation
        if form.fields["new_password"].get_value():
            confirm = form.fields["confirm_password"].get_value()
            if form.fields["new_password"].get_value() != confirm:
                form.fields["confirm_password"].errors.append(
                    "Passwords do not match"
                )
                form.valid = False
        
        if form.is_valid():
            user.save()
            self.flash("success", "Profile updated!")
            return self.redirect("/profile")
    
    return self.render("user/edit.html", {"form": form.create_view()})

Form Field Types

Framefox provides various field types for different input needs:

TextType

Single-line text input:
builder.add("name", TextType, {
    "label": "Full Name",
    "required": True,
    "placeholder": "Enter your name"
})

EmailType

Email address input with validation:
builder.add("email", EmailType, {
    "label": "Email Address",
    "required": True
})

PasswordType

Password input (hidden characters):
builder.add("password", PasswordType, {
    "label": "Password",
    "required": True
})

TextareaType

Multi-line text input:
from framefox.core.form.type.textarea_type import TextareaType

builder.add("bio", TextareaType, {
    "label": "Biography",
    "required": False,
    "rows": 5
})

NumberType

Numeric input:
from framefox.core.form.type.number_type import NumberType

builder.add("age", NumberType, {
    "label": "Age",
    "required": True,
    "min": 18,
    "max": 120
})

CheckboxType

Checkbox for boolean values:
from framefox.core.form.type.checkbox_type import CheckboxType

builder.add("agree_terms", CheckboxType, {
    "label": "I agree to the terms and conditions",
    "required": True
})

ChoiceType

Select dropdown or radio buttons:
from framefox.core.form.type.choice_type import ChoiceType

builder.add("country", ChoiceType, {
    "label": "Country",
    "choices": {
        "us": "United States",
        "uk": "United Kingdom",
        "ca": "Canada"
    },
    "expanded": False,  # False = dropdown, True = radio buttons
    "required": True
})

DateTimeType

Date and time picker:
from framefox.core.form.type.date_time_type import DateTimeType

builder.add("birth_date", DateTimeType, {
    "label": "Date of Birth",
    "required": True
})

FileType

File upload:
from framefox.core.form.type.file_type import FileType

builder.add("avatar", FileType, {
    "label": "Profile Picture",
    "required": False,
    "upload_dir": "/uploads/avatars"
})

Handling Form Submissions

Form Methods

The Form class provides several methods for handling submissions:

handle_request()

Handles form submission from an HTTP request:
request
Request
required
FastAPI Request object containing form data
Returns: bool - True if form is valid, False otherwise
if await form.handle_request(request):
    # Form is valid
    pass

is_valid()

Checks if the form is valid:
if form.is_valid():
    # All validation passed
    pass

is_submitted()

Checks if the form has been submitted:
if form.is_submitted() and not form.is_valid():
    # Form was submitted but has errors
    pass

get_data()

Retrieves the form data as an object or dictionary:
if form.is_valid():
    user_data = form.get_data()
    # user_data is the entity object with updated values

Complete Example

class PostController(AbstractController):
    @Route(path="/posts/create", name="post_create", methods=["GET", "POST"])
    async def create_post(self, request: Request):
        from app.forms.post_form import PostForm
        from app.models.post import Post
        
        post = Post()
        post.author = self.get_user()  # Set default values
        
        form = self.create_form(PostForm, post)
        
        if await form.handle_request(request):
            # Form is valid, data is bound to post entity
            post.save()
            
            self.flash("success", f"Post '{post.title}' created successfully!")
            return self.redirect(self.generate_url("post_detail", post_id=post.id))
        
        # Show form (GET request or validation failed)
        return self.render("posts/create.html", {
            "form": form.create_view(),
            "page_title": "Create New Post"
        })
    
    @Route(path="/posts/{post_id}/edit", name="post_edit", methods=["GET", "POST"])
    async def edit_post(self, post_id: int, request: Request):
        from app.forms.post_form import PostForm
        from app.models.post import Post
        
        post = Post.find(post_id)
        if not post:
            return self.json({"error": "Post not found"}, status=404)
        
        form = self.create_form(PostForm, post)
        
        if await form.handle_request(request):
            post.save()
            
            self.flash("success", "Post updated successfully!")
            return self.redirect(self.generate_url("post_detail", post_id=post.id))
        
        return self.render("posts/edit.html", {
            "form": form.create_view(),
            "post": post,
            "page_title": f"Edit Post: {post.title}"
        })

Rendering Forms in Templates

Create a form view and pass it to your template:
return self.render("form.html", {
    "form": form.create_view()
})
In your template:
<form method="post" enctype="multipart/form-data">
    {% for field in form.fields %}
        <div class="form-group">
            {{ field.label }}
            {{ field.widget }}
            
            {% if field.errors %}
                <div class="errors">
                    {% for error in field.errors %}
                        <span class="error">{{ error }}</span>
                    {% endfor %}
                </div>
            {% endif %}
        </div>
    {% endfor %}
    
    <button type="submit">Submit</button>
</form>

Form Field Options

Common options available for all field types:
  • label (str): Display label for the field
  • required (bool): Whether the field is required
  • placeholder (str): Placeholder text
  • help_text (str): Help text displayed below the field
  • default (Any): Default value
  • disabled (bool): Whether the field is disabled
  • readonly (bool): Whether the field is read-only
  • attr (dict): Additional HTML attributes
builder.add("email", EmailType, {
    "label": "Email Address",
    "required": True,
    "placeholder": "user@example.com",
    "help_text": "We'll never share your email",
    "attr": {
        "class": "form-control",
        "autocomplete": "email"
    }
})

Best Practices

Bind forms to entity objects for automatic data synchronization. This eliminates manual field-to-object mapping.
Always validate on the server even if you have client-side validation. Client-side validation can be bypassed.
Provide user feedback with flash messages after successful form submissions.
Follow the Post-Redirect-Get pattern to prevent duplicate submissions when users refresh the page.

Next Steps

Controllers Overview

Learn more about controller basics

Responses

Explore different response types

Build docs developers (and LLMs) love