# Starlette-Templates
Serve Starlette templates with file-based routing, Pydantic forms, and reusable components
# Getting Started
# Starlette-Templates
This package extends [Starlette](https://starlette.dev/) with support for template-driven routing, form handling, and reusable UI components, built on [Jinja2](https://jinja.palletsprojects.com/en/stable/) and [Pydantic](https://docs.pydantic.dev/latest/).
**Why does this exist?** Starlette is a toolkit that offers building blocks for web apps. But common tasks like template routing, form validation, and UI reuse require significant boilerplate. This package streamlines those workflows by directly routing URLs to templates, validating form data with Pydantic, and enabling type-safe, reusable UI components built as Jinja templates. This makes applications easier to build, reason about, and scale.
## Features
- Serve HTML templates with file-based routing
- Pydantic forms with validation and rendering
- Reusable UI components using Jinja2 and Pydantic with type and validation safety
- Static files with gzip compression and multi-directory support
- JSON:API compliant error responses with custom error pages
- ETag and Last-Modified headers with 304 Not Modified support
## Installation
```
pip install starlette-templates
```
## Quick Start
```
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount
from jinja2 import PackageLoader
from starlette_templates.routing import TemplateRouter
from starlette_templates.middleware import JinjaMiddleware
app = Starlette(
routes=[
# File-based routing for the "templates" package directory
Mount("/", TemplateRouter()),
],
middleware=[
# Configure Jinja2 environment for template autodiscovery
Middleware(
JinjaMiddleware,
template_loaders=[PackageLoader("mypackage", "templates")],
)
]
)
```
Create `templates/index.html`:
```
My App
Welcome to {{ request.url.hostname }}
```
Visit `http://localhost:8000/` and your template will be rendered automatically.
## Next Steps
- Learn about [Applications](https://starlette-templates.tycho.engineering/applications/index.md) for setup and configuration
- Learn about [Template Files](https://starlette-templates.tycho.engineering/templates/index.md) for file-based routing
- Explore [Static Files](https://starlette-templates.tycho.engineering/static-files/index.md) for serving assets with gzip compression
- Explore [Forms](https://starlette-templates.tycho.engineering/forms/index.md) for type-safe form handling
- Discover [Components](https://starlette-templates.tycho.engineering/components/index.md) for reusable UI elements
- Learn about [Error Handling](https://starlette-templates.tycho.engineering/errors/index.md) for custom exceptions and error pages
- Check the [API Reference](https://starlette-templates.tycho.engineering/api/index.md) for detailed documentation
## LLM context
This documentation is available as an LLM-friendly markdown file:
- [llms.txt](https://starlette-templates.tycho.engineering/llms.txt) - links to documentation pages
- [llms-full.txt](https://starlette-templates.tycho.engineering/llms-full.txt) - concatenated full docs in one file
# Core Concepts
# Applications
Use the JinjaMiddleware middleware and TemplateRouter ASGI app to add template support to Starlette applications.
## Project Structure
A typical Starlette-Templates application is organized as a Python package:
```
myapp/
├── __init__.py
├── app.py # Main application file
├── templates/ # Template files
│ ├── base.html # Base layout
│ ├── index.html # Homepage
│ ├── about.html # About page
│ ├── 404.html # Custom 404 page
│ ├── 500.html # Custom 500 page
│ ├── partials/ # Reusable fragments
│ │ ├── navigation.html
│ │ └── footer.html
│ └── components/ # Component templates
│ ├── alert.html
│ └── card.html
└── static/ # Static files
├── css/
├── js/
└── images/
```
Your `app.py` would typically look like:
```
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount, Route
from starlette.responses import JSONResponse
from jinja2 import PackageLoader
from importlib.resources import files
from starlette_templates.routing import TemplateRouter
from starlette_templates.staticfiles import StaticFiles
from starlette_templates.middleware import JinjaMiddleware
PKG_DIR = files("myapp")
async def api(request):
return JSONResponse({"message": "API Root"})
app = Starlette(
routes=[
# Custom routes first
Mount("/api", api, name="api"),
# Static files
Mount("/static", StaticFiles(directories=[PKG_DIR / "static"]), name="static"),
# Template files for remaining routes
Mount("/", TemplateRouter()),
],
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[PackageLoader("myapp", "templates")],
)
]
)
```
## Basic Setup
A minimal Starlette Templates application requires two components:
1. JinjaMiddleware - Middleware that configures the Jinja2 environment, making it available on request state
1. TemplateRouter - An ASGI app that serves HTML templates
```
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount
from jinja2 import PackageLoader
from starlette_templates.routing import TemplateRouter
from starlette_templates.middleware import JinjaMiddleware
app = Starlette(
routes=[
Mount("/", TemplateRouter()),
],
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[PackageLoader("mypackage", "templates")],
)
]
)
```
## Static Files
Starlette Templates provides enhanced static file handling with gzip compression and multi-directory support using StaticFiles.
```
from starlette.routing import Mount
from starlette_templates.staticfiles import StaticFiles
app = Starlette(
routes=[
Mount("/static", StaticFiles(directories=["static"]), name="static"),
Mount("/", TemplateRouter()),
]
)
```
For detailed information about static file handling, including pre-compressed files, multi-directory support, and HTTP caching, see the [Static Files](https://starlette-templates.tycho.engineering/static-files/index.md) guide.
## Custom Routes
Override the default template routing by defining custom routes. Routes defined before the TemplateRouter mount take precedence:
```
from starlette.routing import Route
from starlette.requests import Request
from starlette_templates.responses import TemplateResponse
async def homepage(request: Request) -> TemplateResponse:
return TemplateResponse("home.html", context={"title": "Welcome"})
app = Starlette(
routes=[
# Custom route for homepage
Route("/", homepage, name="home"),
# Template routing for all other routes
Mount("/", TemplateRouter())
],
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[PackageLoader("myapp", "templates")]
)
]
)
```
## Middleware Configuration
### JinjaMiddleware
JinjaMiddleware configures and injects the Jinja2 environment into request state, making it available at `request.state.jinja_env`.
```
from jinja2 import PackageLoader, FileSystemLoader
from starlette_templates.middleware import JinjaMiddleware
app = Starlette(
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[
PackageLoader("myapp", "templates"),
FileSystemLoader("custom/templates"),
],
include_default_loader=True, # Include built-in templates
)
]
)
```
The middleware provides:
- Automatic template loader configuration
- Built-in Jinja2 globals: `url_for`, `url`, `absurl`, `jsonify`
- Request state injection for all templates
### Registering Components
Register ComponentModel classes to make them available in all templates:
```
from starlette_templates.components.base import ComponentModel
class Alert(ComponentModel):
template: str = "components/alert.html"
message: str
variant: str = "info"
app = Starlette(
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[PackageLoader("myapp", "templates")],
extra_components=[Alert], # Register component
)
]
)
```
Then use directly in templates:
```
{{ Alert(message="System error", variant="danger") }}
```
## Context Processors
Add custom context variables to all templates using context processors:
```
from starlette.requests import Request
async def add_user_context(request: Request) -> dict:
return {"user": await get_current_user(request)}
async def add_site_context(request: Request) -> dict:
return {
"site_name": "My Site",
"year": datetime.now().year,
}
templates = TemplateRouter(
context_processors=[add_user_context, add_site_context]
)
```
Context processors are async functions that receive the [Request](https://starlette.dev/requests/) object and return a dictionary of context variables.
### Route-Specific Context Processors
Context processors can also be applied to specific routes using Route objects with path patterns:
```
from starlette.requests import Request
from starlette.routing import Route
async def add_product_context(request: Request) -> dict:
"""Context processor that only runs for product pages."""
product_id = request.path_params.get("product_id")
return {"product": await get_product(product_id)}
async def add_user_context(request: Request) -> dict:
"""Global context processor that runs for all templates."""
return {"user": await get_current_user(request)}
templates = TemplateRouter(
context_processors=[
add_user_context, # Runs for all templates
# Only runs when path matches /products/{product_id}
Route("/products/{product_id}", add_product_context),
]
)
```
Route-specific context processors only execute when the URL path matches the route pattern, allowing you to add context data conditionally based on the request path. Path parameters from the route (like `{product_id}`) are available via `request.path_params`.
## Error Handling
Starlette Templates provides error handling with custom exceptions, error pages, and JSON:API error responses.
Create custom error pages by adding templates like `404.html`, `500.html`, or `error.html` to your template directory. The framework automatically serves these pages for browser requests and returns JSON:API compliant responses for API requests.
For detailed information about error handling, custom exceptions, error pages, and JSON:API responses, see the [Error Handling](https://starlette-templates.tycho.engineering/errors/index.md) guide.
## HTTP Caching
Configure HTTP caching with ETag and Last-Modified headers on TemplateRouter:
```
templates = TemplateRouter(
cache_max_age=3600, # Cache for 1 hour
gzip_min_size=1024, # Compress files > 1KB
)
```
TemplateRouter automatically:
- Generates ETag headers based on file modification time
- Sets Last-Modified headers
- Returns 304 Not Modified responses when appropriate
- Compresses responses over the threshold with gzip
# Multi-Application Sites
A multi-application setup allows you to have multiple independent Starlette applications that can share templates and middleware while maintaining separate routing and configuration. This is useful for large projects that need to be modular or have distinct sections (e.g., a blog, an e-commerce store, and an admin panel) where each section can be developed and maintained independently, even in separate packages, but still share common resources and served by a single main application.
Multi-app architecture leverages Starlette's ability to mount sub-applications using the [Mount](https://starlette.dev/routing/) routing class, allowing each sub-application to have its own routes, middleware, and template loaders. This setup provides flexibility through shared templates and scalability through separation of concerns for complex web applications.
## When to use multi-app architecture
Multi-app sites are useful when you want to:
- Split different parts of your application into distinct modules (e.g., blog, e-commerce, admin)
- Reuse common templates, components, and middleware across applications
- Potentially deploy sub-applications separately while maintaining consistency
- Allow different teams to work on separate applications with clear boundaries
- Incrementally add new sections without restructuring existing code
## Project Structure
A multi-app project structure that uses shared templates but has separate sub-applications might look like this:
```
myproject/
├── __init__.py
├── app.py # Main application that mounts sub-apps
├── camping/ # Camping sub-application
│ ├── __init__.py
│ └── templates/
│ ├── base.html
│ ├── index.html
│ └── products.html
├── hiking/ # Hiking sub-application
│ ├── __init__.py
│ └── templates/
│ ├── base.html
│ ├── index.html
│ └── gear.html
└── shared/ # Shared templates across all apps
├── __init__.py
└── templates/
├── nav.html
├── footer.html
└── error.html
```
This shows a single package example, but you can also have each sub-application (shared, camping, hiking) in its own package with its own templates, dependencies, and codebase.
## Basic Multi-App Setup
Here's a complete example of a multi-app setup:
```
from jinja2 import PackageLoader
from starlette.routing import Mount
from starlette.middleware import Middleware
from starlette.applications import Starlette
from starlette_templates.routing import TemplateRouter
from starlette_templates.middleware import JinjaMiddleware
# Create camping sub-application
camping = Starlette(
routes=[
# Use Mount (not Route) to match all sub-paths
# Give a name for URL generation, {{ url('camping', '/') }}
Mount("/", TemplateRouter(debug=True), name="camping")
],
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[
PackageLoader("myproject.camping", "templates"),
# Add shared templates
PackageLoader("myproject.shared", "templates"),
]
)
],
debug=True,
)
# Create hiking sub-application
hiking = Starlette(
routes=[
# Use Mount (not Route) to match all sub-paths
# Give a name for URL generation, {{ url('hiking', '/') }}
Mount("/", TemplateRouter(debug=True), name="hiking")
],
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[
PackageLoader("myproject.hiking", "templates"),
# Add shared templates
PackageLoader("myproject.shared", "templates"),
]
)
],
debug=True,
)
# Main application that mounts sub-applications
app = Starlette(
routes=[
# Mount sub-applications at desired paths, with names for URL generation
Mount("/camping", camping, name="camping"),
Mount("/hiking", hiking, name="hiking"),
# Serve shared templates at root
Mount("/", TemplateRouter(debug=True), name="shared")
],
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[
# Shared templates for root app
PackageLoader("myproject.shared", "templates"),
]
)
],
debug=True,
)
```
## URL Routing in Multi-App Sites
When you mount sub-applications, the URL routing works hierarchically:
- `/camping/` → `camping` app → `/` → `camping/templates/index.html`
- `/camping/products` → `camping` app → `/products` → `camping/templates/products.html`
- `/hiking/` → `hiking` app → `/` → `hiking/templates/index.html`
- `/hiking/gear` → `hiking` app → `/gear` → `hiking/templates/gear.html`
- `/` → `shared` app → `/` → `shared/templates/index.html`
The mount path (`/camping`, `/hiking`, `/`) is stripped before passing to the sub-application, so the sub-app sees only the remaining path.
## Template Sharing
Sub-applications can share templates in two ways:
### 1. Shared Template Package
Add a shared templates package as a fallback loader:
```
Middleware(
JinjaEnvMiddleware,
template_loaders=[
PackageLoader("myproject.camping", "templates"), # App-specific first
PackageLoader("myproject.shared", "templates"), # Shared as fallback
]
)
```
This allows:
- App-specific templates override shared ones
- Common templates (nav, footer) from shared package are reused
- Each app can customize shared templates by creating local versions with the same name (e.g., `camping/templates/nav.html` overrides `shared/templates/nav.html`)
### 2. Template Inheritance
Shared templates can define base layouts that sub-apps extend:
**shared/templates/base.html:**
```
{% block title %}My Site{% endblock %}
{% include 'nav.html' %}
{% block content %}{% endblock %}
{% include 'footer.html' %}
```
**camping/templates/index.html:**
```
{% extends 'base.html' %}
{% block title %}Camping{% endblock %}
{% block content %}
Welcome to Camping
View Products
{% endblock %}
```
## Debug mode configuration
### Setting debug mode
To enable detailed error pages with full tracebacks, code context, and local variables, you need to set `debug=True` on TemplateRouter, not just the Starlette app:
```
routes=[
Mount("/", TemplateRouter(debug=True), name="camping")
]
```
Common Mistake
Setting `debug=True` only on `Starlette(debug=True)` is snot enoughs. You must also set `debug=True` on each `TemplateRouter` instance to see detailed error pages.
Both are needed because:
`Starlette(debug=True)` enables Starlette's debug features
`TemplateRouter(debug=True)` enables detailed error pages for template rendering errors
TemplateRouter has its own internal error handler that catches exceptions before they reach the Starlette app level, so it needs its own `debug` flag.
### Debug error page features
When `TemplateRouter(debug=True)` is set, error pages show:
- Exception type and message with HTTP status code
- Full traceback with expandable frames
- Code context (5 lines before and after each frame)
- Local variables for each frame
- Request information (method, URL, headers, query params, cookies)
- Validation errors with detailed field information (for Pydantic errors)
## Critical distinction Mount vs Route
Important: Use Mount, Not Route
When using TemplateRouter as a catch-all handler, you must use `Mount`, not `Route`. This is a common source of 404 errors.
Using `Route("/", TemplateRouter(...))` only matches the **exact path** `/`:
```
# WRONG Only matches exact path "/"
routes=[
Route("/", TemplateRouter(), name="camping")
]
```
This causes:
- `/camping/` → Works (matches exact path)
- `/camping/products` → 404 Not Found (doesn't match)
- `/camping/gear` → 404 Not Found (doesn't match)
Using `Mount("/", TemplateRouter(...))` matches `/` and all sub-paths:
```
# CORRECT Matches all paths
routes=[
Mount("/", TemplateRouter(), name="camping")
]
```
This makes everything work:
- `/camping/` → Works
- `/camping/products` → Works
- `/camping/gear` → Works
- `/camping/anything/else` → Works
- Use `Route` for specific endpoints with defined paths:
```
Route("/api/users", users_endpoint)
Route("/health", health_check)
```
- Use `Mount` for catch-all handlers like TemplateRouter:
```
Mount("/", TemplateRouter())
Mount("/static", StaticFiles(directories=["static"]))
```
## URL generation with `url(mount_name, path)`
In multi-app sites, you can generate URLs using the `url(mount_name, path)` function in templates. The route names you define in `Mount()` become available:
```
# Route names defined here
app = Starlette(
routes=[
Mount("/camping", camping, name="camping"),
Mount("/hiking", hiking, name="hiking"),
]
)
```
Use in templates:
```
CampingCamping ProductsHikingHiking Gear
```
The `url()` function (alias for `url_for()`) takes:
1. Route name (e.g., `"camping"`)
1. Path within that route (e.g., `"/products"`)
## Custom exception handlers
For routes that don't use TemplateFiles, you can add custom exception handlers at the app level:
```
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import Response
from starlette_templates.errors import httpexception_handler, exception_handler
async def handle_http_exception(request: Request, exc: HTTPException) -> Response:
jinja_env = request.state.jinja_env
return await httpexception_handler(request, exc, jinja_env, debug=True)
async def handle_exception(request: Request, exc: Exception) -> Response:
jinja_env = request.state.jinja_env
return await exception_handler(request, exc, jinja_env, debug=True)
app = Starlette(
routes=[
Mount("/api", api_routes), # Non-TemplateRouter routes
Mount("/", TemplateRouter(debug=True)),
],
exception_handlers={
HTTPException: handle_http_exception,
Exception: handle_exception,
},
debug=True,
)
```
Note
If all your routes use TemplateRouter, you don't need custom exception handlers. TemplateRouter has its own internal error handler that works with `TemplateRouter(debug=True)`.
# Templates
This guide covers template files, routing, and rendering in Starlette Templates.
## Template Router
TemplateRouter is an ASGI app that serves HTML templates with automatic routing and file resolution.
```
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount
from jinja2 import PackageLoader
from starlette_templates.routing import TemplateRouter
from starlette_templates.middleware import JinjaMiddleware
app = Starlette(
routes=[
Mount("/", TemplateRouter()),
],
middleware=[
# Required for template autodiscovery
Middleware(
JinjaMiddleware,
template_loaders=[PackageLoader("myapp", "templates")],
)
]
)
```
### Features
- Automatically tries `.html`, `.htm`, `.jinja`, `.jinja2` extensions
- Serves `index.html` or `index.htm` for directory requests
- HTTP caching with ETag and Last-Modified headers with 304 responses
- Automatic gzip compression for responses over 500 bytes
- Custom error pages with `404.html`, `500.html`, `error.html`
- Custom context variables with context processors
### File Resolution
When a request is made, `TemplateRouter` resolves templates in this order:
1. Exact match: `/about` → `about.html`
1. With extensions: `/about` → tries `about.html`, `about.htm`, `about.jinja`, `about.jinja2`
1. Index files: `/blog/` → tries `blog/index.html`, `blog/index.htm`
### Configuration
```
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount
from starlette.requests import Request
from jinja2 import PackageLoader
from starlette_templates.routing import TemplateRouter
from starlette_templates.middleware import JinjaMiddleware
async def add_context(request: Request) -> dict:
return {"user": await get_current_user(request)}
app = Starlette(
routes=[
Mount("/", TemplateRouter(
context_processors=[add_context], # Add custom context
cache_max_age=3600, # 1 hour cache
gzip_min_size=500, # Compress files > 500 bytes
)),
],
middleware=[
# Required for template autodiscovery
Middleware(
JinjaMiddleware,
template_loaders=[PackageLoader("myapp", "templates")],
)
]
)
```
### Route-Specific Context Processors
Add context processors that only run for specific URL patterns using Starlette's `Route`:
```
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Mount, Route
from starlette.requests import Request
from jinja2 import PackageLoader
from starlette_templates.routing import TemplateRouter
from starlette_templates.middleware import JinjaMiddleware
# Global context processor - runs for ALL templates
async def add_user_context(request: Request) -> dict:
return {"user": await get_current_user(request)}
# Route-specific context processors
async def add_country_context(request: Request) -> dict:
"""Only runs for /country/{code} paths."""
code = request.path_params["code"]
return {"country": await get_country(code)}
async def add_post_context(request: Request) -> dict:
"""Only runs for /blog/{post_id} paths."""
post_id = request.path_params["post_id"]
return {"post": await get_post(post_id)}
app = Starlette(
routes=[
Mount("/", TemplateRouter(
context_processors=[
add_user_context, # Global - runs for all templates
Route('/country/{code}', add_country_context), # Only /country/* paths
Route('/blog/{post_id}', add_post_context), # Only /blog/* paths
]
)),
],
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[PackageLoader("myapp", "templates")],
)
]
)
```
In your template at `/country/us`:
```
{{ country.name }}
User: {{ user.name }}
```
Route-specific processors:
- Have access to path parameters via `request.path_params`
- Only execute when their route pattern matches the current request
- Can be mixed with global processors in the same list
- Use Starlette's existing `Route` class - no new abstractions
## Template Response
Use TemplateResponse to render templates in route handlers:
```
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Route
from starlette.requests import Request
from jinja2 import PackageLoader
from starlette_templates.responses import TemplateResponse
from starlette_templates.middleware import JinjaEnvMiddleware
async def homepage(request: Request) -> TemplateResponse:
return TemplateResponse(
"home.html",
context={"title": "Welcome", "items": [1, 2, 3]}
)
app = Starlette(
routes=[
Route("/", homepage),
],
middleware=[
# Required for template autodiscovery
Middleware(
JinjaMiddleware,
template_loaders=[PackageLoader("myapp", "templates")],
)
]
)
```
### Context Variables
TemplateResponse automatically includes:
- `request` - The Starlette `Request` object
- All context variables from context processors
- Any additional context passed to `TemplateResponse`
## Template Context
All templates have access to:
### Request Object
```
Current path: {{ request.url.path }}
Host: {{ request.url.hostname }}
Method: {{ request.method }}
User agent: {{ request.headers.get('user-agent') }}
```
### URL Helpers
#### url_for
Generate URLs for named routes:
```
Profile
```
#### url
Generate URLs for mounted apps:
```
```
#### absurl
Generate absolute URLs:
```
```
### JSON Serialization
Safely embed Python data in templates with `jsonify`:
```
```
This properly escapes and serializes Python objects to JSON.
## Custom Filters and Globals
Add custom Jinja2 filters and globals through JinjaMiddleware:
```
from starlette.applications import Starlette
from starlette.middleware import Middleware
from jinja2 import Environment, PackageLoader
from starlette_templates.middleware import JinjaMiddleware
def setup_jinja_env(env: Environment):
# Add custom filter
env.filters['uppercase'] = lambda x: x.upper()
# Add custom global
env.globals['site_name'] = "My Site"
app = Starlette(
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[PackageLoader("myapp", "templates")],
# Custom setup function
environment_setup=setup_jinja_env,
)
]
)
```
Then use in templates:
```
{{ "hello" | uppercase }}
{{ site_name }}
```
## Template Loaders
Configure template loading with [Jinja2 loaders](https://jinja.palletsprojects.com/en/stable/api/#loaders):
### PackageLoader
Load templates from a Python package:
```
from starlette.applications import Starlette
from starlette.middleware import Middleware
from jinja2 import PackageLoader
from starlette_templates.middleware import JinjaMiddleware
app = Starlette(
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[
PackageLoader("myapp", "templates"),
]
)
]
)
```
### FileSystemLoader
Load templates from filesystem directories:
```
from starlette.applications import Starlette
from starlette.middleware import Middleware
from jinja2 import FileSystemLoader
from starlette_templates.middleware import JinjaMiddleware
app = Starlette(
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[
FileSystemLoader("custom/templates"),
]
)
]
)
```
### Multiple Loaders
Use multiple loaders with fallback priority:
```
from starlette.applications import Starlette
from starlette.middleware import Middleware
from jinja2 import PackageLoader, FileSystemLoader
from starlette_templates.middleware import JinjaMiddleware
app = Starlette(
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[
PackageLoader("myapp", "templates"), # Check here first
FileSystemLoader("custom/templates"), # Then here
],
include_default_loader=True, # Then built-in templates
)
]
)
```
## Error Pages
Create custom error pages by adding templates to your template directory:
### 404.html
```
404 Not Found
{% endblock %}
```
## Including Templates
Include reusable template fragments:
```
{% include "partials/navigation.html" %}
{% include "partials/alert.html" with context %}
```
## Macros
Define reusable template functions with macros:
```
{% macro render_field(field) %}
{% if field.errors %}
{{ field.errors }}
{% endif %}
{% endmacro %}
{{ render_field(form.name) }}
{{ render_field(form.email) }}
```
# Static Files
Starlette-Templates provides static file handling with
- Pre-compressed gzip file support
- Automatic HTTP caching headers
- Multi-directory fallback support
- ETag and Last-Modified headers
## StaticFiles
StaticFiles serves static files with support for pre-compressed `.gz` files that are automatically decompressed in browsers and multi-directory fallback support.
### Basic Usage
```
from starlette.applications import Starlette
from starlette.routing import Mount
from starlette_templates.staticfiles import StaticFiles
app = Starlette(
routes=[
Mount("/static", StaticFiles(directories=["static"]), name="static"),
]
)
```
### Pre-compressed Files
Create pre-compressed versions of your static files for better performance:
```
# Create pre-compressed CSS files
gzip -k static/css/bootstrap.css
gzip -k static/css/style.css
# Create pre-compressed JavaScript files
gzip -k static/js/app.js
gzip -k static/js/vendor.js
```
This creates `.gz` versions alongside the original files:
```
static/
├── css/
│ ├── bootstrap.css
│ ├── bootstrap.css.gz
│ ├── style.css
│ └── style.css.gz
└── js/
├── app.js
├── app.js.gz
├── vendor.js
└── vendor.js.gz
```
### Using Pre-compressed Files
Reference the `.gz` files in your templates:
```
```
The browser automatically decompresses the files and applies proper caching headers.
### How It Works
When serving a `.gz` file, StaticFiles:
1. Serves the pre-compressed content
1. Sets `Content-Encoding: gzip` header
1. Sets appropriate `Content-Type` based on the original file extension
1. Adds `ETag` and `Last-Modified` headers for caching
1. Returns `304 Not Modified` for cached requests
### Configuration
```
from pathlib import Path
static_files = StaticFiles(
directories=[Path("static")],
packages=None, # Optional: list of package names
html=False, # Don't serve HTML files
check_dir=True, # Verify directory exists
)
```
## Multi-Directory Support
StaticFiles serves static files from multiple directories with fallback priority, useful for:
- Theme overrides
- Plugin systems
- Framework + application static files
- Development vs production assets
### Basic Usage
```
from pathlib import Path
from starlette.applications import Starlette
from starlette.routing import Mount
from starlette_templates.staticfiles import StaticFiles
static_files = StaticFiles(
directories=[
Path("myapp/static"), # Check here first
Path("framework/static"), # Fallback to here
Path("vendor/static"), # Then here
]
)
app = Starlette(
routes=[
Mount("/static", static_files, name="static"),
]
)
```
### Fallback Priority
Files are resolved in order:
1. If `myapp/static/css/style.css` exists, serve it
1. Otherwise, if `framework/static/css/style.css` exists, serve it
1. Otherwise, if `vendor/static/css/style.css` exists, serve it
1. Otherwise, return 404
This allows you to override framework files with your application files.
### Example Directory Structure
```
myapp/
└── static/
└── css/
└── custom.css # Application-specific styles
framework/
└── static/
├── css/
│ ├── base.css # Framework base styles
│ └── components.css # Framework components
└── js/
└── framework.js # Framework JavaScript
vendor/
└── static/
└── js/
├── jquery.js # Third-party libraries
└── bootstrap.js
```
### Usage in Templates
```
```
### Combining Multi-Directory and Gzip Support
StaticFiles automatically supports both multi-directory fallback and pre-compressed files:
```
from pathlib import Path
from starlette_templates.staticfiles import StaticFiles
# StaticFiles automatically supports .gz files and multiple directories
static_files = StaticFiles(
directories=[
Path("myapp/static"),
Path("framework/static"),
]
)
```
Then create compressed versions in any directory:
```
gzip -k myapp/static/css/custom.css
gzip -k framework/static/css/base.css
```
## HTTP Caching
StaticFiles supports HTTP caching:
### ETag Headers
Automatically generated based on file modification time and size:
```
ETag: "abc123-1234567890"
```
### Last-Modified Headers
Set from file modification time:
```
Last-Modified: Wed, 20 Dec 2023 12:00:00 GMT
```
### 304 Not Modified
When client sends `If-None-Match` or `If-Modified-Since` headers that match:
```
HTTP/1.1 304 Not Modified
ETag: "abc123-1234567890"
```
### Cache-Control Headers
Set appropriate cache headers for static files:
```
from starlette.responses import Response
# In your static files configuration
static_files = StaticFiles(
directories=["static"],
# Files are served with cache headers
)
```
Response includes:
```
Cache-Control: public, max-age=3600
```
## Best Practices
### Pre-compression
Pre-compress large static files for production:
```
# Find and compress all CSS and JS files
find static -type f \( -name "*.css" -o -name "*.js" \) -exec gzip -k {} \;
# Or use a more selective approach
gzip -k static/css/bootstrap.css
gzip -k static/js/app.bundle.js
```
### Build Process Integration
Integrate compression into your build process:
```
# build.py
import gzip
import shutil
from pathlib import Path
def compress_static_files():
static_dir = Path("static")
for file in static_dir.rglob("*.css"):
with open(file, 'rb') as f_in:
with gzip.open(f"{file}.gz", 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
for file in static_dir.rglob("*.js"):
with open(file, 'rb') as f_in:
with gzip.open(f"{file}.gz", 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
if __name__ == "__main__":
compress_static_files()
```
### Directory Organization
Organize static files by type and priority:
```
static/
├── css/
│ ├── vendor/ # Third-party CSS
│ ├── base/ # Base styles
│ └── components/ # Component styles
├── js/
│ ├── vendor/ # Third-party JS
│ └── app/ # Application JS
├── img/
│ ├── icons/
│ └── photos/
└── fonts/
```
### Development vs Production
Use different configurations for development and production:
```
import os
from pathlib import Path
from starlette_templates.staticfiles import StaticFiles
# StaticFiles works for both development and production
static_files = StaticFiles(directories=["static"])
# In production, ensure you have .gz files created for better performance
```
### CDN Integration
For production, consider using a CDN with fallback:
```
# In your context processor
async def add_static_context(request: Request) -> dict:
cdn_url = os.getenv("CDN_URL", "")
return {
"static_url": cdn_url if cdn_url else request.url_for("static", path="/"),
}
```
In templates:
```
```
## Troubleshooting
### File Not Found
If static files aren't loading:
1. Verify the directory path is correct
1. Check file permissions
1. Ensure the mount path is correct
1. Verify the static files route comes before catch-all routes
```
app = Starlette(
routes=[
# Static files BEFORE template files
Mount("/static", StaticFiles(directories=["static"]), name="static"),
# Template files last
Mount("/", TemplateRouter()),
]
)
```
### Gzip Not Working
If pre-compressed files aren't being served:
1. Verify `.gz` files exist
1. Check file naming (must end in `.gz`)
1. Ensure you're using starlette-templates `StaticFiles` (not Starlette's built-in `StaticFiles`)
1. Check browser accepts gzip encoding
### Caching Issues
If files aren't being cached:
1. Check browser caching is enabled
1. Verify ETag headers are being sent
1. Clear browser cache for testing
1. Check proxy/CDN configuration doesn't strip cache headers
# Forms
Starlette-Templates provides type-safe forms with automatic validation and rendering using Pydantic models. The FormModel base class extends Pydantic's `BaseModel` with form-specific functionality.
## Creating forms
Define forms using the FormModel base class and field types:
```
from starlette_templates.forms import (
FormModel, TextField, SelectField, SubmitButtonField
)
class ContactForm(FormModel):
name: str = TextField(
label="Your Name",
placeholder="Enter your name",
required=True,
min_length=2,
)
category: str = SelectField(
default="general",
choices={
"general": "General Inquiry",
"support": "Technical Support",
"sales": "Sales",
},
label="Category",
)
submit: str = SubmitButtonField(text="Send Message")
```
FormModel supports various field types (see [Available Form Fields](#available-form-fields) below) and validation using Pydantic's validators.
The fields, like TextField and SelectField, are [Pydantic model fields](https://docs.pydantic.dev/latest/concepts/fields/) with additional metadata for rendering and validation. These form fields define template components, labels, placeholders, and validation rules, in-addition to the standard Pydantic field behavior.
## Using forms in routes
### Basic usage
```
from starlette.routing import Route
from starlette.requests import Request
from starlette_templates.responses import TemplateResponse
async def contact_page(request: Request) -> TemplateResponse:
# Parse form data and validate without raising on error
form = await ContactForm.from_request(request, raise_on_error=False)
# Check if form is valid and was submitted
if form.is_valid(request):
# Send email, save to database, etc.
await send_contact_email(form.name, form.email, form.category)
return TemplateResponse("success.html", context={"form": form})
# Show form (with errors if validation failed)
return TemplateResponse("contact.html", context={"form": form})
app = Starlette(
routes=[Route("/contact", contact_page, methods=["GET", "POST"])]
)
```
### Error handling
```
from pydantic import ValidationError
async def signup_page(request: Request) -> TemplateResponse:
try:
# Raise ValidationError on invalid data
form = await SignupForm.from_request(request, raise_on_error=True)
if form.is_valid(request):
# Process valid form
user = await create_user(form)
return TemplateResponse("welcome.html", context={"user": user})
except ValidationError as e:
# Handle validation errors
return TemplateResponse(
"signup.html",
context={"errors": e.errors()},
status_code=400
)
# Show empty form for GET requests
form = SignupForm()
return TemplateResponse("signup.html", context={"form": form})
```
## Rendering forms
### Complete form
A FormModel instance can be rendered in templates using the `form()` function or by rendering individual fields.
Render the entire form with a single call:
```
{{ form() }}
```
You can pass form attributes like `method` `action`, and `enctype`:
```
{{ form(
method="POST",
action="/submit",
enctype="multipart/form-data")
}}
```
### Individual fields
Render individual fields with full control:
```
```
### Accessing field values
Access validated field values directly:
```
{% if form.is_valid(request) %}
Thank you, {{ form.name }}!
We'll contact you at {{ form.email }}
{% endif %}
```
### Validation errors
Validation errors are automatically displayed when you render fields. The FormModel injects validation state and error messages into rendered components.
```
```
Check if the form has validation errors:
```
{% if form.has_errors() %}
{{ form.get_error_banner_text() }}
{% endif %}
```
The `has_errors()` method returns `True` if there are any validation errors, and `get_error_banner_text()` returns the configured error banner message (default: "Please correct the errors below").
### How error handling works
When FormModel.from_request() is called with `raise_on_error=False`:
1. The form attempts to validate the request data
1. If validation fails, errors are captured internally in `_field_errors`
1. When you call `form.render('field_name')`, the error is automatically injected into the component
1. The component displays the error message with appropriate styling (e.g., Bootstrap's `is-invalid` class)
This means you don't need to manually access or display errors - they're handled automatically by the rendering system.
## Available form fields
### TextField
TextField - Single-line text input:
```
name: str = TextField(
label="Name",
placeholder="Enter your name",
required=True,
min_length=2,
max_length=100,
)
```
### TextAreaField
TextAreaField - Multi-line text input:
```
message: str = TextAreaField(
label="Message",
placeholder="Enter your message",
rows=5,
required=True,
min_length=10,
)
```
### EmailField
EmailField - Email input with validation:
```
email: str = EmailField(
label="Email",
placeholder="you@example.com",
required=True,
)
```
### IntegerField
IntegerField - Numeric input for integers:
```
age: int = IntegerField(
label="Age",
min_value=18,
max_value=120,
required=True,
)
```
### FloatField
FloatField - Numeric input for floats:
```
price: float = FloatField(
label="Price",
min_value=0.0,
step=0.01,
required=True,
)
```
### CheckboxField
CheckboxField - Boolean checkbox:
```
agree: bool = CheckboxField(
default=False,
label="I agree to the terms",
required=True,
)
```
### SelectField
SelectField - Dropdown select (single or multiple):
```
# Single select
country: str = SelectField(
label="Country",
choices={
"us": "United States",
"uk": "United Kingdom",
"ca": "Canada",
},
default="us",
)
# Multiple select
interests: list[str] = SelectField(
label="Interests",
choices={
"tech": "Technology",
"music": "Music",
"sports": "Sports",
},
multiple=True,
)
```
### DateField
DateField - Date picker with Flatpickr:
```
from datetime import date
birth_date: date = DateField(
label="Birth Date",
required=True,
)
```
### HiddenField
HiddenField - Hidden input:
```
user_id: int = HiddenField(default=0)
```
### SubmitButtonField
SubmitButtonField - Submit button:
```
submit: str = SubmitButtonField(text="Submit")
```
### TagField
TagField - Tag input with comma separation:
```
tags: list[str] = TagField(
label="Tags",
placeholder="Enter tags separated by commas",
)
```
## Form validation
### Pydantic validators
Use [Pydantic validators](https://docs.pydantic.dev/latest/concepts/validators/) for custom validation logic:
```
from pydantic import field_validator, model_validator
class RegistrationForm(FormModel):
username: str = TextField(label="Username", required=True)
password: str = TextField(label="Password", type="password", required=True)
confirm_password: str = TextField(label="Confirm Password", type="password", required=True)
@field_validator('username')
@classmethod
def validate_username(cls, v):
if len(v) < 3:
raise ValueError('Username must be at least 3 characters')
if not v.isalnum():
raise ValueError('Username must be alphanumeric')
return v
@model_validator(mode='after')
def validate_passwords(self):
if self.password != self.confirm_password:
raise ValueError('Passwords do not match')
return self
```
### Custom validation
Add custom validation methods:
```
class PaymentForm(FormModel):
amount: float = FloatField(label="Amount", required=True)
currency: str = SelectField(
label="Currency",
choices={"usd": "USD", "eur": "EUR", "gbp": "GBP"}
)
@field_validator('amount')
@classmethod
def validate_amount(cls, v):
if v <= 0:
raise ValueError('Amount must be positive')
if v > 10000:
raise ValueError('Amount cannot exceed 10,000')
return v
```
## Parsing request data
The model_from_request helper combines path parameters, query parameters, and body data:
```
from starlette_templates.forms import model_from_request
from pydantic import BaseModel
class UserData(BaseModel):
name: str
email: str
age: int
async def create_user(request: Request):
# Combines path params, query params, and body data
# data is a validated UserData instance
data = await model_from_request(request, UserData)
```
If path, query, and body parameters overlap, body data takes precedence over query parameters, which take precedence over path parameters.
If you are using a FormModel, use FormModel.from_request() instead to get form-specific behavior and validation.
```
from starlette_templates.forms import FormModel
class ProfileForm(FormModel):
username: str
bio: str
async def edit_profile(request: Request):
# Combines path params, query params, and body data
# form is a validated ProfileForm instance
form = await ProfileForm.from_request(request)
```
# Components
Components in Starlette-Templates are reusable UI elements built with Jinja2 templates and Pydantic models. They provide:
- Type safety with Pydantic validation
- Reusable, testable UI elements
- Template-based rendering
- Integration with the Jinja2 environment
## Creating Components
Define components using the ComponentModel base class:
```
from starlette_templates.components.base import ComponentModel
from pydantic import Field
class Alert(ComponentModel):
# Path to component template
template: str = "components/alert.html"
# Component properties with validation
message: str = Field(..., description="Alert message")
variant: str = Field(default="info", description="Alert variant")
dismissible: bool = Field(default=False, description="Show close button")
```
### Component Template
Create the template file (`components/alert.html`):
```
{{ message }}
{% if dismissible %}
{% endif %}
```
## Using Components
### In Routes
Create component instances and pass them to templates:
```
from starlette.requests import Request
from starlette_templates.responses import TemplateResponse
async def dashboard(request: Request) -> TemplateResponse:
alert = Alert(
message="Welcome back!",
variant="success",
dismissible=True
)
return TemplateResponse(
"dashboard.html",
context={"alert": alert}
)
```
### In Templates
Render components by calling them:
```
{{ alert }}
```
### Global Registration
Register components globally to use them in any template:
```
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette_templates.middleware import JinjaEnvMiddleware
app = Starlette(
middleware=[
Middleware(
JinjaEnvMiddleware,
template_loaders=[PackageLoader("myapp", "templates")],
extra_components=[Alert], # Register component
)
]
)
```
Then use directly in templates without passing from routes:
```
{{ Alert(message="System error", variant="danger", dismissible=True) }}
```
## Built-in Form Components
Starlette Templates includes Bootstrap-compatible form components:
### Input
Input - Text, email, password, number inputs:
```
from starlette_templates.components.forms import Input
# In route
input_field = Input(
name="username",
label="Username",
type="text",
placeholder="Enter username",
required=True,
)
# In template
{{ Input(name="email", label="Email", type="email", required=True) }}
```
### Textarea
Textarea - Multi-line text input:
```
from starlette_templates.components.forms import Textarea
textarea = Textarea(
name="message",
label="Message",
rows=5,
placeholder="Enter your message",
)
```
### Select
Select - Native select dropdown:
```
from starlette_templates.components.forms import Select
select = Select(
name="country",
label="Country",
choices={
"us": "United States",
"uk": "United Kingdom",
"ca": "Canada",
},
selected="us",
)
```
### Checkbox
Checkbox - Checkbox input:
```
from starlette_templates.components.forms import Checkbox
checkbox = Checkbox(
name="agree",
label="I agree to the terms",
checked=False,
)
```
### Radio
Radio - Radio button:
```
from starlette_templates.components.forms import Radio
# In template
{{ Radio(name="plan", label="Basic Plan", value="basic", checked=True) }}
{{ Radio(name="plan", label="Pro Plan", value="pro") }}
```
### Switch
Switch - Toggle switch:
```
from starlette_templates.components.forms import Switch
switch = Switch(
name="notifications",
label="Enable notifications",
checked=True,
)
```
### FileInput
FileInput - File upload:
```
from starlette_templates.components.forms import FileInput
file_input = FileInput(
name="avatar",
label="Profile Picture",
accept="image/*",
)
```
### Range
Range - Range slider:
```
from starlette_templates.components.forms import Range
range_slider = Range(
name="volume",
label="Volume",
min_value=0,
max_value=100,
step=1,
value=50,
)
```
### ChoicesSelect
ChoicesSelect - Enhanced select with Choices.js:
```
from starlette_templates.components.forms import ChoicesSelect
choices_select = ChoicesSelect(
name="tags",
label="Tags",
choices={
"python": "Python",
"javascript": "JavaScript",
"go": "Go",
},
multiple=True,
)
```
### DatePicker
DatePicker - Date picker with Flatpickr:
```
from starlette_templates.components.forms import DatePicker
date_picker = DatePicker(
name="birth_date",
label="Birth Date",
placeholder="Select date",
)
```
### SubmitButton
SubmitButton - Form submit button:
```
from starlette_templates.components.forms import SubmitButton
submit_btn = SubmitButton(
text="Submit Form",
variant="primary",
)
```
## Component Examples
### Card Component
```
class Card(ComponentModel):
template: str = "components/card.html"
title: str = Field(..., description="Card title")
body: str = Field(..., description="Card body content")
footer: str | None = Field(None, description="Card footer")
variant: str = Field(default="default", description="Card style variant")
```
Template (`components/card.html`):
```
```
# Reference
# API Reference
This page contains the complete API reference for Starlette Templates.
## TemplateRouter
```
TemplateRouter(
*,
directory: PathLike | None = None,
extensions: list[str] | None = None,
index_files: list[str] | None = None,
check_dir: bool = True,
follow_symlink: bool = False,
cache_max_age: int = 3600,
gzip_min_size: int = 500,
context_processors: list[
Callable[[Request], Awaitable[dict[str, Any]]]
| Route
]
| None = None,
jinja_env: Environment | None = None,
debug: bool = False,
error_handler: Callable[
[Request, Exception], Awaitable[Response]
]
| None = None,
)
```
ASGI app for routing HTTP requests to Jinja2 templates with automatic resolution.
Routes incoming requests to templates with Jinja2 rendering, HTTP caching, gzip compression, and automatic file resolution (extension fallbacks, index files).
Architecture with two modes of operation: The system supports two distinct operational modes. When initialized with a directory parameter, it uses filesystem lookups to find templates through direct file access with stat and permissions checking, which is useful when templates are in a known directory. Alternatively, when initialized without a directory parameter (directory=None), it uses jinja_env loaders such as PackageLoader or FileSystemLoader, allowing templates to be served from locations defined in JinjaMiddleware, providing a single source of truth for template locations without configuration duplication.
Flow
1. JinjaMiddleware defines template loaders
1. Mount TemplateRouter() without directory parameter
1. Request comes in: /forms
1. TemplateRouter.get_response() called
1. Delegates to render_template_from_loaders()
1. Gets jinja_env from request.state
1. Tries candidates: forms.html, forms.htm, etc.
1. Finds template using jinja_env.loader.get_source()
1. Renders template with context {"request": request}
1. Returns ContentResponse with caching headers
Context processors are callables that accept a Request and return a dict of additional context variables to be merged into the template context.
Parameters:
- **`directory`** (`PathLike | None`, default: `None` ) – Optional path to templates directory (None = use jinja_env loaders)
- **`extensions`** (`list[str] | None`, default: `None` ) – List of allowed file extensions (default: [".html", ".htm", ".jinja", ".jinja2"])
- **`index_files`** (`list[str] | None`, default: `None` ) – List of index filenames to search for (default: ["index.html", "index.htm"])
- **`check_dir`** (`bool`, default: `True` ) – Whether to check if directory exists on init
- **`follow_symlink`** (`bool`, default: `False` ) – Whether to follow symlinks in directory
- **`cache_max_age`** (`int`, default: `3600` ) – Max age for HTTP caching (in seconds)
- **`gzip_min_size`** (`int`, default: `500` ) – Minimum size (in bytes) to apply gzip compression
- **`context_processors`** (`list[Callable[[Request], Awaitable[dict[str, Any]]] | Route] | None`, default: `None` ) – List of async callables to add to template context
- **`jinja_env`** (`Environment | None`, default: `None` ) – Optional Jinja2 Environment (falls back to request.state.jinja_env)
- **`debug`** (`bool`, default: `False` ) – Whether to enable debug mode (shows detailed error pages)
- **`error_handler`** (`Callable[[Request, Exception], Awaitable[Response]] | None`, default: `None` ) – Callable to handle exceptions and return custom responses (uses built-in handler if None)
Example
```
app = Starlette(
routes=[
# Override specific paths, like / which would map to index.html
Route("/", homepage, name="home"),
# Catch-all for templates
Mount("/", TemplateRouter()),
],
middleware=[
Middleware(
JinjaMiddleware,
template_loaders=[PackageLoader("myapp", "templates")]
)
]
)
```
## StaticFiles
```
StaticFiles(
*,
directories: List[PathLike] | None = None,
packages: list[str | tuple[str, str]] | None = None,
html: bool = False,
check_dir: bool = True,
follow_symlink: bool = False,
)
```
Enhanced StaticFiles with gzip compression, caching, and multi-directory support.
Features:
- Automatic gzip handling for .gz files with proper Content-Encoding headers
- Strong HTTP caching (ETag, Cache-Control, Last-Modified)
- Multi-directory fallback support (searches directories in priority order)
When a .gz file is requested directly, this class automatically:
1. Strips the .gz extension from the Content-Type detection
1. Sets Content-Encoding: gzip header
1. Adds Vary: Accept-Encoding header
1. Adds strong caching headers (Cache-Control: public, max-age=31536000, immutable)
When multiple directories are provided, files are searched in priority order, allowing application static files to override framework static files.
Parameters:
- **`directories`** (`List[PathLike] | None`, default: `None` ) – Directory paths to search (in priority order)
- **`packages`** (`list[str | tuple[str, str]] | None`, default: `None` ) – Package resources to serve
- **`html`** (`bool`, default: `False` ) – Enable HTML mode
- **`check_dir`** (`bool`, default: `True` ) – Whether to check if directories exist on initialization
- **`follow_symlink`** (`bool`, default: `False` ) – Whether to follow symbolic links
Example with single directory
```
static = StaticFiles(directories=["static"])
```
Example with multiple directories (priority-based override):
```
static = StaticFiles(
directories=[Path("app/static"), Path("framework/static")],
packages=[("myapp", "static")],
)
```
Example serving pre-compressed files
```
```
The browser receives:
- Content-Type: text/css
- Content-Encoding: gzip
- Cache-Control: public, max-age=31536000, immutable
- And automatically decompresses the content
## TemplateResponse
```
TemplateResponse(
template_name: str,
context: dict[str, Any] | None = None,
status_code: int = 200,
headers: Mapping[str, str] | None = None,
media_type: str | None = None,
background: Any = None,
)
```
HTMLResponse subclass that renders Jinja2 templates.
This class provides a convenient way to render Jinja2 templates from route handlers without manually accessing the Jinja2 environment.
The template will have access to standard context variables:
- request: The Starlette Request object
- url_for: Function to generate URLs for named routes
- absurl: Function to generate absolute URLs
- base_url: The base URL of the application
- url: Function to build full URLs from paths
- jsonify: Function to serialize data to JSON for embedding in HTML
- Plus any custom context variables provided
Parameters:
- **`template_name`** (`str`) – The name of the Jinja2 template to render
- **`request`** – The Starlette Request object (required for accessing app's Jinja2 env)
- **`context`** (`dict[str, Any] | None`, default: `None` ) – The context data to pass to the template
- **`status_code`** (`int`, default: `200` ) – The HTTP status code for the response
- **`headers`** (`Mapping[str, str] | None`, default: `None` ) – Additional headers for the response
- **`media_type`** (`str | None`, default: `None` ) – The media type for the response
- **`background`** (`Any`, default: `None` ) – A background task to run after the response is sent
Example
```
from starlette_templates import Application, TemplateResponse, route, model_from_request
from starlette.requests import Request
from pydantic import BaseModel
class UserModel(BaseModel):
id: int
name: str
@route("/users/{user_id}")
async def user_profile(request: Request) -> TemplateResponse:
user = model_from_request(request, UserModel)
return TemplateResponse(
"user_profile.html",
context={"user": user},
)
app = Application(
routes=[user_profile],
package_name="myapp",
)
```
## JinjaMiddleware
```
JinjaMiddleware(
app: ASGIApp,
*,
state_key: str = "jinja_env",
include_websocket: bool = True,
template_loaders: list[
Union[PackageLoader, FileSystemLoader]
]
| None = None,
include_default_loader: bool = True,
extra_components: ModuleType
| type[ComponentModel]
| Sequence[ModuleType | type[ComponentModel]]
| None = None,
jinja_globals: dict[str, Any] | None = None,
jinja_filters: dict[str, Callable] | None = None,
)
```
Middleware to inject Jinja2 environment into request state.
This middleware creates a Jinja2 environment with the provided template loaders and makes it available in the request scope under a specified key, by default "jinja_env". This allows downstream components to access the Jinja2 environment for template rendering.
Parameters:
- **`app`** (`ASGIApp`) – The ASGI application
- **`state_key`** (`str`, default: `'jinja_env'` ) – The key under which to store the Jinja2 environment in request state
- **`include_websocket`** (`bool`, default: `True` ) – Whether to include WebSocket connections for Jinja2 env injection
- **`template_loaders`** (`list[Union[PackageLoader, FileSystemLoader]] | None`, default: `None` ) – List of Jinja2 template loaders to configure the environment.
- **`include_default_loader`** (`bool`, default: `True` ) – Include the default PackageLoader for "starlette_templates"
- **`component_modules`** – Optional module, ComponentModel class, or sequence of either to auto-register components from. Can be a single module, a single ComponentModel class, or a list containing any mix of modules and ComponentModel classes.
Example
```
from starlette.applications import Starlette
from starlette_templates.middleware import JinjaMiddleware
from jinja2 import PackageLoader, FileSystemLoader
from myapp import custom_components # A module with custom components
from myapp.components import CustomButton, CustomCard # Individual component classes
app = Starlette()
app.add_middleware(
JinjaMiddleware,
template_loaders=[
PackageLoader("myapp", "templates"),
FileSystemLoader("custom/templates"),
],
include_default_loader=True,
# Can pass a single module, a single component, or a list of either
component_modules=[custom_components, CustomButton, CustomCard],
)
```
## FormModel
```
FormModel(**data)
```
Base class for form models with rendering capabilities.
Attributes:
- **`model_config`** – Configuration for form rendering and behavior
Example
Define a form model by inheriting from `FormModel` and using form field helpers:
```
import datetime
from starlette_templates.forms import FormModel, SelectField, DateField, SubmitButtonField
class Filters(FormModel):
storefront: str = SelectField(
default="ww",
choices={"ww": "Worldwide", "us": "United States"},
label="Storefront",
)
start_date: datetime.date = DateField(
default_factory=lambda: datetime.date.today(),
label="Start Date",
)
submit: str = SubmitButtonField(text="Submit")
```
Add forms to a specific template using `TemplateResponse`
```
async def overview(request: Request) -> TemplateResponse:
form = await Filters.from_request(request)
return TemplateResponse("overview.html", {"form": form})
app = Starlette(routes=[
Route("/overview", overview, methods=["GET", "POST"], name="overview")
])
```
Add forms to all templates using `context_processors` parameter of `TemplateRouter`
```
async def form_context_processor(request: Request) -> dict:
form = await Filters.from_request(request)
return {"form": form}
app = Starlette(
routes=[
Mount("/", TemplateRouter(
context_processors=[form_context_processor],
)),
],
middleware=[Middleware(Jinja2TemplatesMiddleware)],
)
```
Or assign forms to specific routes using `Route` in `context_processors`
```
app = Starlette(
routes=[
Route(
"/",
TemplateRouter(
context_processors=[
# Only add form to /search route templates
Route("/search", form_context_processor)
],
),
),
],
middleware=[Middleware(Jinja2TemplatesMiddleware)],
)
```
Methods:
- **`from_request`** – Create form instance from request data.
- **`is_valid`** – Check if form has no validation errors. If request is provided,
- **`render`** – Render a specific field in HTML. This is a Jinja2 template method.
- **`has_errors`** – Check if form has validation errors. If name is provided, checks for errors on that specific field.
- **`get_field_error`** – Get validation error message for a specific field, if any.
- **`get_error_banner_text`** – Get the error banner text from form config. This is text that can be displayed
- **`model_dump`** – Override model_dump to exclude fields marked with exclude_from_dump.
- **`__call__`** – Render the entire form. Alias for render_form().
### from_request
```
from_request(
request: Request, raise_on_error: bool = True
) -> FormModel
```
Create form instance from request data.
This class method parses data from the request (path params, query params, form data, and JSON body) and validates it against the form model and returns an instance of the form model. If validation errors occur, they are either raised as ValidationError or captured in the instance based on the `raise_on_error` flag.
If a field with the same name appears in multiple sources, the precedence is as follows: path params < query params < form data < JSON body. For example, if a field is present in both query params and form data, the value from form data will be used.
Use the `.is_valid()` method on the returned instance to check if the form data is valid when `raise_on_error` is False.
Parameters:
- **`request`** (`Request`) – Starlette Request object
- **`raise_on_error`** (`bool`, default: `True` ) – If True, raises ValidationError. If False, captures errors in instance.
Returns:
- `FormModel` – FormModel instance (with errors if raise_on_error=False)
### is_valid
```
is_valid(request: Request | None = None) -> bool
```
Check if form has no validation errors. If request is provided, checks for both HTTP method and field errors to determine validity.
If `request` is provided, the form is only considered valid if the request method is the same as the form's configured method (default: POST) and there are no field errors. This means `is_valid()` will return False for GET requests even if there are no field errors and will only return True for requests with the correct method and no field errors, which is the typical behavior for form submissions.
Parameters:
- **`request`** (`Request | None`, default: `None` ) – Starlette Request object (optional)
Returns:
- `bool` – True if form is valid, False otherwise
Example
```
async def search_page(request: Request):
form = await SearchForm.from_request(request)
if form.is_valid(request):
# Process valid form submission
...
return TemplateResponse("search.html", {"form": form})
```
### render
```
render(
ctx: Context,
field_name: str,
request: Request | None = None,
) -> Markup
```
Render a specific field in HTML. This is a Jinja2 template method.
This calls the component's render method with appropriate parameters defined in the form.
If the request parameter is not provided, it will attempt to get it from the template context. If the request is still not found, an error will be raised.
Parameters:
- **`ctx`** (`Context`) – Jinja2 context
- **`field_name`** (`str`) – Name of the field to render
- **`request`** (`Request | None`, default: `None` ) – Starlette request object
Returns:
- `Markup` – Markup of the rendered component HTML
Example
In a Jinja2 templates, use sync or async to render the field:
```
{{ form.render('start_date', request) }}
{{ await form.render('start_date', request) }}
```
You can also omit the request parameter if the template context has a `request` variable:
```
{{ form.render('start_date') }}
```
Raises:
- `ValueError` – If the request object is not provided or found in context
- `ValueError` – If the field name is not found in the form
- `ValueError` – If the field does not have a component_cls defined
### has_errors
```
has_errors(name: str | None = None) -> bool
```
Check if form has validation errors. If name is provided, checks for errors on that specific field.
Parameters:
- **`name`** (`str | None`, default: `None` ) – Name of the field to check for errors (optional)
Returns:
- `bool` – True if there are validation errors, False otherwise
Example
```
if form.has_errors():
print("Form has validation errors")
if form.has_errors('email'):
print("Email field has a validation error")
```
You can also use this in Jinja2 templates:
```
{% if form.has_errors() %}
{{ form.get_error_banner_text() }}
{% endif %}
{% if form.has_errors('email') %}
{{ form.get_field_error('email') }}
{% endif %}
```
### get_field_error
```
get_field_error(
name: str, default: str | None = None
) -> str | None
```
Get validation error message for a specific field, if any.
Parameters:
- **`name`** (`str`) – Name of the field
- **`default`** (`str | None`, default: `None` ) – Default value to return if no error exists for the field
Returns:
- `str | None` – The error message for the field, or the default value if no error exists
Example
```
error_message = form.get_field_error('email', default='No error')
```
### get_error_banner_text
```
get_error_banner_text() -> str
```
Get the error banner text from form config. This is text that can be displayed at the top of the form when there are validation errors.
### model_dump
```
model_dump(**kwargs) -> dict[str, Any]
```
Override model_dump to exclude fields marked with exclude_from_dump.
### __call__
```
__call__(
ctx: Context,
request: Request | None = None,
*,
id: str | None = None,
action: str | None = None,
method: str | None = None,
enctype: str | None = None,
classes: list[str] | None = None,
novalidate: bool = False,
) -> Markup
```
Render the entire form. Alias for render_form().
Example
```
{{ form() }}
{{ form(action='/submit', classes=['my-form'], enctype='multipart/form-data') }}
```
## TextField
```
TextField(
default: Any = PydanticUndefined,
*,
min_length: int | None = None,
max_length: int | None = None,
regex: str | None = None,
name: Any = PydanticUndefined,
id: Any = PydanticUndefined,
label: Any = PydanticUndefined,
title: str = PydanticUndefined,
placeholder: str = PydanticUndefined,
description: str = PydanticUndefined,
required: bool = False,
hidden: bool = False,
disabled: bool = False,
readonly: bool = False,
col_class: str | None = None,
**kwargs: Any,
) -> Any
```
Create a string form field with validation and formatting options.
Parameters:
- **`default`** (`Any`, default: `PydanticUndefined` ) – Default value for the field
- **`min_length`** (`int | None`, default: `None` ) – Minimum length validation
- **`max_length`** (`int | None`, default: `None` ) – Maximum length validation
- **`regex`** (`str | None`, default: `None` ) – Regular expression pattern for validation
- **`name`** (`Any`, default: `PydanticUndefined` ) – Field name attribute
- **`id`** (`Any`, default: `PydanticUndefined` ) – Field id attribute
- **`label`** (`Any`, default: `PydanticUndefined` ) – Field label for display
- **`title`** (`str`, default: `PydanticUndefined` ) – Field title
- **`placeholder`** (`str`, default: `PydanticUndefined` ) – Placeholder text for input
- **`description`** (`str`, default: `PydanticUndefined` ) – Field description
- **`required`** (`bool`, default: `False` ) – Whether field is required
- **`hidden`** (`bool`, default: `False` ) – Whether field is hidden
- **`disabled`** (`bool`, default: `False` ) – Whether field is disabled
- **`readonly`** (`bool`, default: `False` ) – Whether field is readonly
- **`col_class`** (`str | None`, default: `None` ) – Bootstrap column class for horizontal layout (e.g., "col-md-4")
- **`**kwargs`** (`Any`, default: `{}` ) – Additional field attributes
Returns:
- `Any` – A Pydantic Field configured for string validation
## TextAreaField
```
TextAreaField(
default: Any = PydanticUndefined,
*,
min_length: int | None = None,
max_length: int | None = None,
rows: int = 3,
name: Any = PydanticUndefined,
id: Any = PydanticUndefined,
label: Any = PydanticUndefined,
title: str = PydanticUndefined,
placeholder: str = PydanticUndefined,
description: str = PydanticUndefined,
required: bool = False,
disabled: bool = False,
readonly: bool = False,
**kwargs: Any,
) -> Any
```
Create a textarea form field for multi-line text input.
Parameters:
- **`default`** (`Any`, default: `PydanticUndefined` ) – Default value for the field
- **`min_length`** (`int | None`, default: `None` ) – Minimum length validation
- **`max_length`** (`int | None`, default: `None` ) – Maximum length validation
- **`rows`** (`int`, default: `3` ) – Number of rows for textarea
- **`name`** (`Any`, default: `PydanticUndefined` ) – Field name attribute
- **`id`** (`Any`, default: `PydanticUndefined` ) – Field id attribute
- **`label`** (`Any`, default: `PydanticUndefined` ) – Field label for display
- **`title`** (`str`, default: `PydanticUndefined` ) – Field title
- **`placeholder`** (`str`, default: `PydanticUndefined` ) – Placeholder text for textarea
- **`description`** (`str`, default: `PydanticUndefined` ) – Field description
- **`required`** (`bool`, default: `False` ) – Whether field is required
- **`disabled`** (`bool`, default: `False` ) – Whether field is disabled
- **`readonly`** (`bool`, default: `False` ) – Whether field is readonly
- **`**kwargs`** (`Any`, default: `{}` ) – Additional field attributes
Returns:
- `Any` – A Pydantic Field configured for textarea input
## EmailField
```
EmailField(
default: Any = PydanticUndefined,
*,
max_length: int | None = None,
name: Any = PydanticUndefined,
id: Any = PydanticUndefined,
label: Any = PydanticUndefined,
title: str = PydanticUndefined,
placeholder: str = PydanticUndefined,
description: str = PydanticUndefined,
required: bool = False,
hidden: bool = False,
disabled: bool = False,
readonly: bool = False,
**kwargs: Any,
) -> Any
```
Create an email form field with email validation.
Parameters:
- **`default`** (`Any`, default: `PydanticUndefined` ) – Default value for the field
- **`max_length`** (`int | None`, default: `None` ) – Maximum length validation
- **`name`** (`Any`, default: `PydanticUndefined` ) – Field name attribute
- **`id`** (`Any`, default: `PydanticUndefined` ) – Field id attribute
- **`label`** (`Any`, default: `PydanticUndefined` ) – Field label for display
- **`title`** (`str`, default: `PydanticUndefined` ) – Field title
- **`placeholder`** (`str`, default: `PydanticUndefined` ) – Placeholder text for input
- **`description`** (`str`, default: `PydanticUndefined` ) – Field description
- **`required`** (`bool`, default: `False` ) – Whether field is required
- **`hidden`** (`bool`, default: `False` ) – Whether field is hidden
- **`disabled`** (`bool`, default: `False` ) – Whether field is disabled
- **`readonly`** (`bool`, default: `False` ) – Whether field is readonly
- **`**kwargs`** (`Any`, default: `{}` ) – Additional field attributes
Returns:
- `Any` – A Pydantic Field configured for email validation
## IntegerField
```
IntegerField(
default: Any = PydanticUndefined,
*,
gt: int | None = None,
ge: int | None = None,
lt: int | None = None,
le: int | None = None,
multiple_of: int | None = None,
name: Any = PydanticUndefined,
id: Any = PydanticUndefined,
label: Any = PydanticUndefined,
title: str = PydanticUndefined,
placeholder: str = PydanticUndefined,
description: str = PydanticUndefined,
required: bool = False,
hidden: bool = False,
disabled: bool = False,
readonly: bool = False,
**kwargs: Any,
) -> Any
```
Create an integer form field with numeric validation options.
Parameters:
- **`default`** (`Any`, default: `PydanticUndefined` ) – Default value for the field
- **`gt`** (`int | None`, default: `None` ) – Greater than validation
- **`ge`** (`int | None`, default: `None` ) – Greater than or equal validation
- **`lt`** (`int | None`, default: `None` ) – Less than validation
- **`le`** (`int | None`, default: `None` ) – Less than or equal validation
- **`multiple_of`** (`int | None`, default: `None` ) – Multiple of validation
- **`name`** (`Any`, default: `PydanticUndefined` ) – Field name attribute
- **`id`** (`Any`, default: `PydanticUndefined` ) – Field id attribute
- **`label`** (`Any`, default: `PydanticUndefined` ) – Field label for display
- **`title`** (`str`, default: `PydanticUndefined` ) – Field title
- **`placeholder`** (`str`, default: `PydanticUndefined` ) – Placeholder text for input
- **`description`** (`str`, default: `PydanticUndefined` ) – Field description
- **`required`** (`bool`, default: `False` ) – Whether field is required
- **`hidden`** (`bool`, default: `False` ) – Whether field is hidden
- **`disabled`** (`bool`, default: `False` ) – Whether field is disabled
- **`readonly`** (`bool`, default: `False` ) – Whether field is readonly
- **`**kwargs`** (`Any`, default: `{}` ) – Additional field attributes
Returns:
- `Any` – A Pydantic Field configured for integer validation
## FloatField
```
FloatField(
default: Any = PydanticUndefined,
*,
gt: float | None = None,
ge: float | None = None,
lt: float | None = None,
le: float | None = None,
multiple_of: float | None = None,
name: Any = PydanticUndefined,
id: Any = PydanticUndefined,
label: Any = PydanticUndefined,
title: str = PydanticUndefined,
placeholder: str = PydanticUndefined,
description: str = PydanticUndefined,
required: bool = False,
hidden: bool = False,
disabled: bool = False,
readonly: bool = False,
**kwargs: Any,
) -> Any
```
Create a float form field with numeric validation options.
Parameters:
- **`default`** (`Any`, default: `PydanticUndefined` ) – Default value for the field
- **`gt`** (`float | None`, default: `None` ) – Greater than validation
- **`ge`** (`float | None`, default: `None` ) – Greater than or equal validation
- **`lt`** (`float | None`, default: `None` ) – Less than validation
- **`le`** (`float | None`, default: `None` ) – Less than or equal validation
- **`multiple_of`** (`float | None`, default: `None` ) – Multiple of validation
- **`name`** (`Any`, default: `PydanticUndefined` ) – Field name attribute
- **`id`** (`Any`, default: `PydanticUndefined` ) – Field id attribute
- **`label`** (`Any`, default: `PydanticUndefined` ) – Field label for display
- **`title`** (`str`, default: `PydanticUndefined` ) – Field title
- **`placeholder`** (`str`, default: `PydanticUndefined` ) – Placeholder text for input
- **`description`** (`str`, default: `PydanticUndefined` ) – Field description
- **`required`** (`bool`, default: `False` ) – Whether field is required
- **`hidden`** (`bool`, default: `False` ) – Whether field is hidden
- **`disabled`** (`bool`, default: `False` ) – Whether field is disabled
- **`readonly`** (`bool`, default: `False` ) – Whether field is readonly
- **`**kwargs`** (`Any`, default: `{}` ) – Additional field attributes
Returns:
- `Any` – A Pydantic Field configured for float validation
## CheckboxField
```
CheckboxField(
default: Any = PydanticUndefined,
*,
name: Any = PydanticUndefined,
id: Any = PydanticUndefined,
label: Any = PydanticUndefined,
title: str = PydanticUndefined,
description: str = PydanticUndefined,
value: str = "1",
required: bool = False,
disabled: bool = False,
inline: bool = False,
**kwargs: Any,
) -> Any
```
Create a checkbox form field for boolean input.
Parameters:
- **`default`** (`Any`, default: `PydanticUndefined` ) – Default value for the field (True/False)
- **`name`** (`Any`, default: `PydanticUndefined` ) – Field name attribute
- **`id`** (`Any`, default: `PydanticUndefined` ) – Field id attribute
- **`label`** (`Any`, default: `PydanticUndefined` ) – Field label for display
- **`title`** (`str`, default: `PydanticUndefined` ) – Field title
- **`description`** (`str`, default: `PydanticUndefined` ) – Field description
- **`value`** (`str`, default: `'1'` ) – Value submitted when checkbox is checked
- **`required`** (`bool`, default: `False` ) – Whether field is required
- **`disabled`** (`bool`, default: `False` ) – Whether field is disabled
- **`inline`** (`bool`, default: `False` ) – Whether to display inline
- **`**kwargs`** (`Any`, default: `{}` ) – Additional field attributes
Returns:
- `Any` – A Pydantic Field configured for checkbox input
## SelectField
```
SelectField(
default: Any = PydanticUndefined,
default_factory: Callable[[], Any]
| None = PydanticUndefined,
*,
choices: Dict[Any, str]
| List[Any]
| List[Choice]
| None = None,
choices_factory: Callable[
[], Dict[Any, str] | List[Any] | List[Choice]
]
| None = None,
multiple: bool = False,
size: int | None = None,
name: Any = PydanticUndefined,
id: Any = PydanticUndefined,
label: Any = PydanticUndefined,
title: str = PydanticUndefined,
placeholder: str = PydanticUndefined,
required: bool = False,
description: str = PydanticUndefined,
disabled: bool = False,
search_enabled: bool = True,
search_placeholder: str = "Type to search",
remove_button: bool = True,
max_item_count: int | None = None,
col_class: str | None = None,
**kwargs: Any,
) -> Any
```
Create a select form field with options.
Parameters:
- **`default`** (`Any`, default: `PydanticUndefined` ) – Default value for the field
- **`choices`** (`Dict[Any, str] | List[Any] | List[Choice] | None`, default: `None` ) – Dict of {value: label}, list of values where value=label, or list of Choice objects. Order is preserved in all formats.
- **`choices_factory`** (`Callable[[], Dict[Any, str] | List[Any] | List[Choice]] | None`, default: `None` ) – Callable that returns a dict, list, or list of Choice objects
- **`multiple`** (`bool`, default: `False` ) – Whether multiple selections are allowed (uses ChoicesSelect when True)
- **`size`** (`int | None`, default: `None` ) – Number of visible options (for Select component)
- **`name`** (`Any`, default: `PydanticUndefined` ) – Field name attribute
- **`id`** (`Any`, default: `PydanticUndefined` ) – Field id attribute
- **`label`** (`Any`, default: `PydanticUndefined` ) – Field label for display
- **`title`** (`str`, default: `PydanticUndefined` ) – Field title
- **`placeholder`** (`str`, default: `PydanticUndefined` ) – Placeholder text (for ChoicesSelect)
- **`required`** (`bool`, default: `False` ) – Whether field is required
- **`description`** (`str`, default: `PydanticUndefined` ) – Field description
- **`hidden`** – Whether field is hidden
- **`disabled`** (`bool`, default: `False` ) – Whether field is disabled
- **`search_enabled`** (`bool`, default: `True` ) – Enable search functionality (for ChoicesSelect)
- **`search_placeholder`** (`str`, default: `'Type to search'` ) – Search placeholder text (for ChoicesSelect)
- **`remove_button`** (`bool`, default: `True` ) – Show remove button for selected items (for ChoicesSelect)
- **`max_item_count`** (`int | None`, default: `None` ) – Max items that can be selected (for ChoicesSelect with multiple)
- **`col_class`** (`str | None`, default: `None` ) – Bootstrap column class for horizontal layout (e.g., "col-md-4")
- **`**kwargs`** (`Any`, default: `{}` ) – Additional field attributes
Returns:
- `Any` – A Pydantic Field configured for select input
Example
Single select with dict choices (different value and label)
```
status = SelectField(
choices={"active": "Active", "inactive": "Inactive", "pending": "Pending"},
label="Status"
)
```
Single select with list choices (same value and label)
```
fruits = SelectField(
choices=["Apple", "Orange", "Banana"],
label="Fruit"
)
```
Single select with Choice objects
```
country = SelectField(
choices=[
Choice(value="us", label="United States"),
Choice(value="ca", label="Canada"),
Choice(value="uk", label="United Kingdom"),
],
label="Country"
)
```
Multiple select (uses ChoicesSelect component)
```
interests = SelectField(
choices={"sports": "Sports", "music": "Music", "reading": "Reading"},
multiple=True,
label="Interests"
)
```
Dynamic choices using a factory function
```
def get_country_choices():
return {"us": "United States", "uk": "United Kingdom", "ca": "Canada"}
country = SelectField(
choices_factory=get_country_choices,
label="Country"
)
```
## DateField
```
DateField(
default: Any = PydanticUndefined,
default_factory: Callable[[], Any]
| None = PydanticUndefined,
*,
min: Any = PydanticUndefined,
max: Any = PydanticUndefined,
name: Any = PydanticUndefined,
id: Any = PydanticUndefined,
label: Any = PydanticUndefined,
title: str = PydanticUndefined,
placeholder: str = "Select date",
description: str = PydanticUndefined,
required: bool = False,
disabled: bool = False,
mode: str = "single",
enable_time: bool = False,
time_24hr: bool = True,
date_format: str = "Y-m-d",
inline: bool = False,
col_class: str | None = None,
**kwargs: Any,
) -> Any
```
Create a date form field with Flatpickr date picker.
This field renders using the Flatpickr date picker component with advanced date selection features.
Parameters:
- **`default`** (`Any`, default: `PydanticUndefined` ) – Default value for the field (can be date, datetime, or string)
- **`default_factory`** (`Callable[[], Any] | None`, default: `PydanticUndefined` ) – Factory function to generate default value
- **`min`** (`Any`, default: `PydanticUndefined` ) – Minimum allowed date (date, datetime, or ISO string)
- **`max`** (`Any`, default: `PydanticUndefined` ) – Maximum allowed date (date, datetime, or ISO string)
- **`name`** (`Any`, default: `PydanticUndefined` ) – Field name attribute
- **`id`** (`Any`, default: `PydanticUndefined` ) – Field id attribute
- **`label`** (`Any`, default: `PydanticUndefined` ) – Field label for display
- **`title`** (`str`, default: `PydanticUndefined` ) – Field title
- **`placeholder`** (`str`, default: `'Select date'` ) – Placeholder text for input
- **`description`** (`str`, default: `PydanticUndefined` ) – Field description
- **`required`** (`bool`, default: `False` ) – Whether field is required
- **`hidden`** – Whether field is hidden
- **`disabled`** (`bool`, default: `False` ) – Whether field is disabled
- **`mode`** (`str`, default: `'single'` ) – Selection mode: "single", "multiple", or "range"
- **`enable_time`** (`bool`, default: `False` ) – Enable time picker
- **`time_24hr`** (`bool`, default: `True` ) – Use 24-hour time format
- **`date_format`** (`str`, default: `'Y-m-d'` ) – Date format string (Flatpickr format)
- **`inline`** (`bool`, default: `False` ) – Display calendar inline
- **`col_class`** (`str | None`, default: `None` ) – Bootstrap column class for horizontal layout (e.g., "col-md-4")
- **`**kwargs`** (`Any`, default: `{}` ) – Additional field attributes
Returns:
- `Any` – A Pydantic Field configured for date input
Example
Basic date field
```
birth_date = DateField(label="Birth Date", required=True)
```
Date field with range constraints
```
event_date = DateField(
label="Event Date",
min="2024-01-01",
max="2024-12-31"
)
```
Date field with default value
```
from datetime import date
appointment_date = DateField(
label="Appointment Date",
default=date.today(),
required=True
)
```
Date range picker
```
date_range = DateField(
label="Date Range",
mode="range"
)
```
Date and time picker
```
appointment = DateField(
label="Appointment",
enable_time=True,
date_format="Y-m-d H:i"
)
```
## HiddenField
```
HiddenField(
default: Any = PydanticUndefined,
*,
name: Any = PydanticUndefined,
id: Any = PydanticUndefined,
title: str = PydanticUndefined,
description: str = PydanticUndefined,
**kwargs: Any,
) -> Any
```
Create a hidden form field for passing data without user interaction.
Parameters:
- **`default`** (`Any`, default: `PydanticUndefined` ) – Default value for the field
- **`name`** (`Any`, default: `PydanticUndefined` ) – Field name attribute
- **`id`** (`Any`, default: `PydanticUndefined` ) – Field id attribute
- **`title`** (`str`, default: `PydanticUndefined` ) – Field title
- **`description`** (`str`, default: `PydanticUndefined` ) – Field description
- **`**kwargs`** (`Any`, default: `{}` ) – Additional field attributes
Returns:
- `Any` – A Pydantic Field configured for hidden input
Example
Basic hidden field
```
csrf_token = HiddenField(default="abc123")
```
Hidden field with custom name
```
user_id = HiddenField(default=42, name="user_id")
```
Hidden field that excludes from validation
```
session_key = HiddenField(
default="session_xyz",
exclude_from_validation=True
)
```
## SubmitButtonField
```
SubmitButtonField(
text: str = "Submit",
*,
button_type: str = "submit",
name: Any = PydanticUndefined,
id: Any = PydanticUndefined,
classes: list[str] | None = None,
disabled: bool = False,
form: Any = PydanticUndefined,
formaction: Any = PydanticUndefined,
formenctype: Any = PydanticUndefined,
formmethod: Any = PydanticUndefined,
formnovalidate: bool = False,
formtarget: Any = PydanticUndefined,
col_class: str | None = None,
**kwargs: Any,
) -> Any
```
Create a submit button form field with customization options.
Parameters:
- **`text`** (`str`, default: `'Submit'` ) – Button text/label
- **`button_type`** (`str`, default: `'submit'` ) – Button type (submit, button, reset)
- **`name`** (`Any`, default: `PydanticUndefined` ) – Button name attribute
- **`id`** (`Any`, default: `PydanticUndefined` ) – Button id attribute
- **`classes`** (`list[str] | None`, default: `None` ) – CSS classes for the button
- **`disabled`** (`bool`, default: `False` ) – Whether button is disabled
- **`form`** (`Any`, default: `PydanticUndefined` ) – Form element the button is associated with
- **`formaction`** (`Any`, default: `PydanticUndefined` ) – URL where form data is sent when button is clicked
- **`formenctype`** (`Any`, default: `PydanticUndefined` ) – How form data is encoded when button is clicked
- **`formmethod`** (`Any`, default: `PydanticUndefined` ) – HTTP method when button is clicked
- **`formnovalidate`** (`bool`, default: `False` ) – Whether form validation is bypassed when button is clicked
- **`formtarget`** (`Any`, default: `PydanticUndefined` ) – Where to display response when button is clicked
- **`col_class`** (`str | None`, default: `None` ) – Bootstrap column class for horizontal layout (e.g., "col-md-4")
- **`template`** – Jinja2 template for rendering the field
- **`**kwargs`** (`Any`, default: `{}` ) – Additional button attributes
Returns:
- `Any` – A Pydantic Field configured for submit button
Example
Basic submit button
```
submit = SubmitButtonField("Save Changes")
```
Customized submit button
```
submit = SubmitButtonField(
text="Create Account",
classes=["btn", "btn-primary", "btn-lg"],
id="create-account-btn"
)
```
Button with form attributes
```
delete_btn = SubmitButtonField(
text="Delete",
button_type="submit",
formmethod="DELETE",
formnovalidate=True,
classes=["btn", "btn-danger"]
)
```
## TagField
```
TagField(
default: Any = PydanticUndefined,
*,
separator: str = ",",
placeholder: str = "Enter tags separated by commas",
max_tags: int | None = None,
tag_pattern: str | None = None,
badge_class: str = "badge bg-primary text-white",
show_validation: bool = True,
name: Any = PydanticUndefined,
id: Any = PydanticUndefined,
label: Any = PydanticUndefined,
title: str = PydanticUndefined,
description: str = PydanticUndefined,
required: bool = False,
hidden: bool = False,
disabled: bool = False,
**kwargs: Any,
) -> Any
```
Create a tag input form field for comma-separated values.
This field renders as a text input with live tag visualization using Bootstrap badges. It supports tag validation, max tag limits, and custom styling.
Parameters:
- **`default`** (`Any`, default: `PydanticUndefined` ) – Default value for the field (can be list or comma-separated string)
- **`separator`** (`str`, default: `','` ) – Character used to separate tags (default: comma)
- **`placeholder`** (`str`, default: `'Enter tags separated by commas'` ) – Placeholder text for the input field
- **`max_tags`** (`int | None`, default: `None` ) – Maximum number of tags allowed (None for unlimited)
- **`tag_pattern`** (`str | None`, default: `None` ) – Regex pattern for individual tag validation
- **`badge_class`** (`str`, default: `'badge bg-primary text-white'` ) – Bootstrap badge CSS classes for tag display
- **`show_validation`** (`bool`, default: `True` ) – Whether to show validation styling (is-valid/is-invalid)
- **`name`** (`Any`, default: `PydanticUndefined` ) – Field name attribute
- **`id`** (`Any`, default: `PydanticUndefined` ) – Field id attribute
- **`label`** (`Any`, default: `PydanticUndefined` ) – Field label for display
- **`title`** (`str`, default: `PydanticUndefined` ) – Field title
- **`description`** (`str`, default: `PydanticUndefined` ) – Field description
- **`required`** (`bool`, default: `False` ) – Whether field is required
- **`template`** – Jinja2 template for rendering the field
- **`**kwargs`** (`Any`, default: `{}` ) – Additional field attributes
Returns:
- `Any` – A Pydantic Field configured for tag input
Example
Basic tag field
```
tags = TagField(label="Tags", default=["python", "javascript"])
```
Tag field with custom separator
```
keywords = TagField(
label="Keywords",
separator=";",
placeholder="Enter keywords separated by semicolons"
)
```
Limited tag field with custom styling
```
categories = TagField(
label="Categories",
max_tags=5,
badge_class="badge bg-secondary text-white",
required=True
)
```
Tag field without validation styling
```
draft_tags = TagField(
label="Draft Tags",
show_validation=False,
badge_class="badge border border-primary text-primary"
)
```
## model_from_request
```
model_from_request(request: Request, model: Type[T]) -> T
```
```
model_from_request(
request: Request, model: None = None
) -> dict
```
```
model_from_request(
request: Request, model: Type[T] | None = None
) -> T | dict
```
Parse and validate request body against a Pydantic model.
Loads data from path params, query params, and request body (JSON or form), in that order, overriding earlier values. Form fields that allow multiple values (like multi-select) are handled appropriately.
This function can be used to populate a Pydantic model instance from request data, or return a dict of parameters if no model is provided.
Parameters:
- **`request`** (`Request`) – Starlette Request object
- **`model`** (`Type[T] | None`, default: `None` ) – Pydantic model class to validate against (optional)
Returns:
- `T | dict` – An instance of the Pydantic model if provided, otherwise a dict of parameters.
## AppException
```
AppException(
detail: str,
status_code: int = 400,
code: ErrorCode | str | None = None,
source: ErrorSource | None = None,
meta: dict[str, Any] | None = None,
)
```
Base exception for all application errors with JSON:API compliance.
This exception provides structured error information that can be rendered as either JSON (for API requests) or HTML (for browser requests).
Parameters:
- **`detail`** (`str`) – Human-readable description of the error
- **`status_code`** (`int`, default: `400` ) – HTTP status code (default: 400)
- **`code`** (`ErrorCode | str | None`, default: `None` ) – Machine-readable error code
- **`source`** (`ErrorSource | None`, default: `None` ) – Location of the error (JSON pointer, parameter name, etc.)
- **`meta`** (`dict[str, Any] | None`, default: `None` ) – Additional metadata about the error
Example
```
raise AppException(
detail="User with email 'john@example.com' already exists",
status_code=409,
code=ErrorCode.DUPLICATE_RESOURCE,
source=ErrorSource(parameter="email"),
meta={"email": "john@example.com"}
)
```
## ErrorCode
Error codes for API responses following JSON:API specification.
Attributes:
- **`INTERNAL_ERROR`** – Internal server error not caused by client request.
- **`INVALID_REQUEST`** – The request is malformed or contains invalid parameters.
- **`INVALID_PARAMETER`** – A specific parameter in the request is invalid.
- **`NOT_FOUND`** – The requested resource could not be found.
- **`VALIDATION_ERROR`** – The request data failed validation checks.
- **`UNAUTHORIZED`** – Authentication is required and has failed or not been provided.
- **`FORBIDDEN`** – Access to the requested resource is forbidden.
- **`METHOD_NOT_ALLOWED`** – The HTTP method used is not allowed for the requested resource.
### INTERNAL_ERROR
```
INTERNAL_ERROR = 'internal_error'
```
Internal server error not caused by client request.
### INVALID_REQUEST
```
INVALID_REQUEST = 'invalid_request'
```
The request is malformed or contains invalid parameters.
### INVALID_PARAMETER
```
INVALID_PARAMETER = 'invalid_parameter'
```
A specific parameter in the request is invalid.
### NOT_FOUND
```
NOT_FOUND = 'not_found'
```
The requested resource could not be found.
### VALIDATION_ERROR
```
VALIDATION_ERROR = 'validation_error'
```
The request data failed validation checks.
### UNAUTHORIZED
```
UNAUTHORIZED = 'unauthorized'
```
Authentication is required and has failed or not been provided.
### FORBIDDEN
```
FORBIDDEN = 'forbidden'
```
Access to the requested resource is forbidden.
### METHOD_NOT_ALLOWED
```
METHOD_NOT_ALLOWED = 'method_not_allowed'
```
The HTTP method used is not allowed for the requested resource.
## ErrorSource
JSON:API error source object indicating where the error originated.
Attributes:
- **`pointer`** (`str | None`) –
- **`parameter`** (`str | None`) –
- **`header`** (`str | None`) –
### pointer
```
pointer: str | None = None
```
### parameter
```
parameter: str | None = None
```
### header
```
header: str | None = None
```
______________________________________________________________________
## context
Jinja2 context builders and custom filters for starlette_templates templates.
Functions:
- **`jsonify`** – Convert data to a JSON string for embedding in HTML templates.
- **`url_for`** – Generate URL for a named route.
- **`url`** – Generate URL for a mounted application.
- **`absurl`** – Build absolute URL from path.
- **`register_jinja_globals_filters`** – Register global functions and filters in the Jinja2 environment.
### jsonify
```
jsonify(data: Any, indent: int | None = None) -> Markup
```
Convert data to a JSON string for embedding in HTML templates.
Uses Pydantic's built-in JSON serialization to handle complex data types like datetime, Decimal, and BaseModel instances.
Parameters:
- **`data`** (`Any`) – The data to serialize to JSON
Returns:
- `Markup` – A Markup-safe JSON string representation of the data
### url_for
```
url_for(
context: dict, name: str, **path_params: Any
) -> str
```
Generate URL for a named route.
This function uses @pass_context to access the request from the template context. The request must be added to the context by TemplateRouter or TemplateResponse.
Parameters:
- **`context`** (`dict`) – Jinja2 context (automatically passed by Jinja2)
- **`name`** (`str`) – Route name
- **`**path_params`** (`Any`, default: `{}` ) – Path parameters for the route
Returns:
- `str` – URL path for the named route
Example
Assuming a route defined as:
```
async def user_profile(request: Request):
...
app = Starlette(routes=[
Route("/users/{user_id}/profile", user_profile, name="user_profile"),
])
```
You can generate the URL by name in a template like this:
```
{{ url_for('user_profile', user_id=123) }}
```
Note
This is registered as a Jinja2 global by register_jinja_globals_filters() and can be used directly in templates without importing.
### url
```
url(context: dict, name: str, path: str = '/') -> str
```
Generate URL for a mounted application.
This function builds URLs for Starlette Mount instances by name. It's a convenience wrapper around url_for specifically for mounts.
Parameters:
- **`context`** (`dict`) – Jinja2 context (automatically passed by Jinja2)
- **`name`** (`str`) – Mount name
- **`path`** (`str`, default: `'/'` ) – Path within the mount (default: "/")
Returns:
- `str` – URL path for the mounted application
Example
Assuming a mount defined as:
```
app = Starlette(routes=[
Mount("/static", StaticFiles(directory="static"), name="static_files"),
])
```
You can generate URLs for files within the mount in a template like this:
```
{{ url('static_files', '/css/style.css') }}
```
Note
For named routes, use url_for instead. This is specifically for Mount instances.
### absurl
```
absurl(context: dict, path: str) -> str
```
Build absolute URL from path.
Converts a relative path to an absolute URL by combining it with the request's base URL (scheme + host).
Parameters:
- **`context`** (`dict`) – Jinja2 context (automatically passed by Jinja2)
- **`path`** (`str`) – URL path (relative or absolute)
Returns:
- `str` – Full absolute URL with scheme and host
Example
Creates an absolute URL for the given path:
```
{{ absurl('/path/to/resource') }}
```
Note
Different from url_for which uses named routes. This takes a literal path.
### register_jinja_globals_filters
```
register_jinja_globals_filters(
jinja_env: Environment,
jinja_globals: dict[str, Any] | None = None,
jinja_filters: dict[str, Callable] | None = None,
) -> None
```
Register global functions and filters in the Jinja2 environment.
This function adds commonly used utilities to the Jinja2 environment so they can be accessed directly in templates without importing.
Called by JinjaMiddleware during initialization to set up the environment.
Uses setdefault to avoid overriding any custom globals that may have been set, following the same pattern as Starlette's built-in template functions.
Parameters:
- **`jinja_env`** (`Environment`) – The Jinja2 Environment instance
Flow
1. JinjaMiddleware creates jinja_env with template loaders
1. Calls this function to register globals/filters
1. Stores jinja_env in request.state.jinja_env
1. Templates can use these functions: {{ url_for(...) }}, {{ data|jsonify }}, etc.
1. @pass_context functions access request from context["request"]
# Components API Reference
This page contains the API reference for component classes and form components that are bundled with Starlette-Templates. All components are built using Pydantic models for type safety and validation.
The base class for all components is ComponentModel. All components extend this class and define their own properties and templates. Components must specify a `template` property that points to the Jinja2 template used for rendering. The template path is relative to the template loaders configured in the Jinja environment in JinjaEnvMiddleware. The Jinja2 template can access all properties defined on the component model.
## ComponentModel
Base class for renderable UI components. Components should inherit from this class.
Components are rendered using Jinja2 templates, which can be organized and loaded via the application's Jinja2 environment template loaders configured in `JinjaMiddleware`.
```
class HeaderComponent(ComponentModel):
template: str = "components/header.html"
title: str
subtitle: str
header = HeaderComponent(title="Welcome", subtitle="Enjoy your stay")
rendered_html = await header.render(request)
```
The `context` method can be overridden to provide custom context data for the Jinja templates.
```
class UserCardComponent(ComponentModel):
template: str = "components/user_card.html"
user_id: int
def context(self) -> dict[str, t.Any]:
user = get_user_from_db(self.user_id)
return {"user": user}
user_card = UserCardComponent(user_id=42)
rendered_html = await user_card.render(request)
```
Attributes:
- **`id`** (`str`) – Unique identifier for this component. Auto-generated by default.
- **`template`** (`str`) – Path to the Jinja2 template used to render this component. Required.
Methods:
- **`prepare_field_params`** – Transform form field params before component instantiation.
- **`context`** – Context variables to set in parent template when this component is used.
- **`render`** – Render this component to HTML.
### model_config
```
model_config = ConfigDict(
arbitrary_types_allowed=True, extra="allow"
)
```
### id
```
id: str = Field(default_factory=generate_component_id)
```
Unique identifier for this component. Auto-generated by default.
### template
```
template: str
```
Path to the Jinja2 template used to render this component.
### prepare_field_params
```
prepare_field_params(
params: dict[str, Any], field_value: Any
) -> dict[str, Any]
```
Transform form field params before component instantiation.
Override in subclasses that need special handling when used with FormModel. For example, Select components need to convert choices to SelectOption objects, and Checkbox components need to set checked state from the field value.
By default, this method sets the 'value' param to the field value.
Parameters:
- **`params`** (`dict[str, Any]`) – Parameters from the form field's json_schema_extra
- **`field_value`** (`Any`) – The current value of the form field
Returns:
- `dict[str, Any]` – Modified params dict ready for component instantiation
### context
```
context(
jinja_context: dict[str, Any], request: Request
) -> dict[str, Any]
```
Context variables to set in parent template when this component is used.
Override this method to set context variables that should be available to the parent template. For example, components can set library inclusion flags like `include_datatables` or any other context variables.
Parameters:
- **`jinja_context`** (`dict[str, Any]`) – The current Jinja2 rendering context
- **`request`** (`Request`) – The Starlette Request object
Returns:
- `dict[str, Any]` – Dictionary of context variables to merge into parent template context
### render
```
render(request: Request, **kwargs) -> Markup
```
Render this component to HTML.
## Form Components
These components are designed to work with the [FormModel](https://starlette-templates.tycho.engineering/forms/index.md) for building Pydantic-based forms with validation and rendering.
## Input
Bootstrap form input component.
Methods:
- **`prepare_field_params`** – Convert field value to string for input element.
Attributes:
- **`template`** (`str`) – Bootstrap input form control component.
- **`name`** (`str`) – Input name attribute.
- **`label`** (`str | None`) – Input label.
- **`type`** (`str`) – Input type, e.g., text, email, password, number, tel, url, etc.
- **`value`** (`str | None`) – Input value.
- **`placeholder`** (`str | None`) – Placeholder text.
- **`help_text`** (`str | None`) – Help text below input.
- **`required`** (`bool`) – Whether the input is required.
- **`disabled`** (`bool`) – Whether the input is disabled.
- **`readonly`** (`bool`) – Whether the input is readonly.
- **`validation_state`** (`str | None`) – Validation state: valid, invalid.
- **`validation_message`** (`str | None`) – Validation feedback message.
- **`prepend`** (`str | None`) – Prepend text/icon to input.
- **`append`** (`str | None`) – Append text/icon to input.
- **`size`** (`str | None`) – Input size: sm, lg.
### template
```
template: str = Field('components/input.html', frozen=True)
```
Bootstrap input form control component.
### name
```
name: str = Field(..., description='Input name attribute')
```
Input name attribute.
### label
```
label: str | None = Field(
default=None, description="Input label"
)
```
Input label.
### type
```
type: str = Field(
default="text",
description="Input type: text, email, password, number, tel, url, etc.",
)
```
Input type, e.g., text, email, password, number, tel, url, etc.
### value
```
value: str | None = Field(
default=None, description="Input value"
)
```
Input value.
### placeholder
```
placeholder: str | None = Field(
default=None, description="Placeholder text"
)
```
Placeholder text.
### help_text
```
help_text: str | None = Field(
default=None, description="Help text below input"
)
```
Help text below input.
### required
```
required: bool = Field(
default=False, description="Required field"
)
```
Whether the input is required.
### disabled
```
disabled: bool = Field(
default=False, description="Disabled input"
)
```
Whether the input is disabled.
### readonly
```
readonly: bool = Field(
default=False, description="Readonly input"
)
```
Whether the input is readonly.
### validation_state
```
validation_state: str | None = Field(
default=None,
description="Validation state: valid, invalid",
)
```
Validation state: valid, invalid.
### validation_message
```
validation_message: str | None = Field(
default=None, description="Validation feedback message"
)
```
Validation feedback message.
### prepend
```
prepend: str | None = Field(
default=None, description="Prepend text/icon to input"
)
```
Prepend text/icon to input.
### append
```
append: str | None = Field(
default=None, description="Append text/icon to input"
)
```
Append text/icon to input.
### size
```
size: str | None = Field(
default=None, description="Input size: sm, lg"
)
```
Input size: sm, lg.
### prepare_field_params
```
prepare_field_params(
params: dict[str, Any], field_value: Any
) -> dict[str, Any]
```
Convert field value to string for input element.
## Textarea
Bootstrap textarea component.
Methods:
- **`prepare_field_params`** – Convert field value to string for textarea element.
Attributes:
- **`template`** (`str`) – Bootstrap textarea component template.
- **`name`** (`str`) – Textarea name attribute.
- **`label`** (`str | None`) – Textarea label.
- **`value`** (`str | None`) – Textarea value.
- **`placeholder`** (`str | None`) – Placeholder text.
- **`help_text`** (`str | None`) – Help text below textarea.
- **`rows`** (`int`) – Number of rows.
- **`required`** (`bool`) – Whether the textarea is required.
- **`disabled`** (`bool`) – Whether the textarea is disabled.
- **`readonly`** (`bool`) – Whether the textarea is readonly.
- **`validation_state`** (`str | None`) – Validation state: valid, invalid.
- **`validation_message`** (`str | None`) – Validation feedback message.
### template
```
template: str = Field(
"components/textarea.html", frozen=True
)
```
Bootstrap textarea component template.
### name
```
name: str = Field(
..., description="Textarea name attribute"
)
```
Textarea name attribute.
### label
```
label: str | None = Field(
default=None, description="Textarea label"
)
```
Textarea label.
### value
```
value: str | None = Field(
default=None, description="Textarea value"
)
```
Textarea value.
### placeholder
```
placeholder: str | None = Field(
default=None, description="Placeholder text"
)
```
Placeholder text.
### help_text
```
help_text: str | None = Field(
default=None, description="Help text below textarea"
)
```
Help text below textarea.
### rows
```
rows: int = Field(default=3, description='Number of rows')
```
Number of rows.
### required
```
required: bool = Field(
default=False, description="Required field"
)
```
Whether the textarea is required.
### disabled
```
disabled: bool = Field(
default=False, description="Disabled textarea"
)
```
Whether the textarea is disabled.
### readonly
```
readonly: bool = Field(
default=False, description="Readonly textarea"
)
```
Whether the textarea is readonly.
### validation_state
```
validation_state: str | None = Field(
default=None,
description="Validation state: valid, invalid",
)
```
Validation state: valid, invalid.
### validation_message
```
validation_message: str | None = Field(
default=None, description="Validation feedback message"
)
```
Validation feedback message.
### prepare_field_params
```
prepare_field_params(
params: dict[str, Any], field_value: Any
) -> dict[str, Any]
```
Convert field value to string for textarea element.
## Select
Bootstrap select dropdown component.
Methods:
- **`prepare_field_params`** – Convert choices to SelectOption objects with proper selected state.
Attributes:
- **`template`** (`str`) – Bootstrap select dropdown component template.
- **`name`** (`str`) – Select name attribute.
- **`label`** (`str | None`) – Select label.
- **`options`** (`list[SelectOption]`) – Select options.
- **`help_text`** (`str | None`) – Help text below select.
- **`required`** (`bool`) – Whether the select is required.
- **`disabled`** (`bool`) – Whether the select is disabled.
- **`multiple`** (`bool`) – Allow multiple selections.
- **`size`** (`str | None`) – Select size: sm, lg.
- **`validation_state`** (`str | None`) – Validation state: valid, invalid.
- **`validation_message`** (`str | None`) – Validation feedback message.
### template
```
template: str = Field("components/select.html", frozen=True)
```
Bootstrap select dropdown component template.
### name
```
name: str = Field(..., description='Select name attribute')
```
Select name attribute.
### label
```
label: str | None = Field(
default=None, description="Select label"
)
```
Select label.
### options
```
options: list[SelectOption] = Field(
default_factory=list, description="Select options"
)
```
Select options.
### help_text
```
help_text: str | None = Field(
default=None, description="Help text below select"
)
```
Help text below select.
### required
```
required: bool = Field(
default=False, description="Required field"
)
```
Whether the select is required.
### disabled
```
disabled: bool = Field(
default=False, description="Disabled select"
)
```
Whether the select is disabled.
### multiple
```
multiple: bool = Field(
default=False, description="Allow multiple selections"
)
```
Allow multiple selections.
### size
```
size: str | None = Field(
default=None, description="Select size: sm, lg"
)
```
Select size: sm, lg.
### validation_state
```
validation_state: str | None = Field(
default=None,
description="Validation state: valid, invalid",
)
```
Validation state: valid, invalid.
### validation_message
```
validation_message: str | None = Field(
default=None, description="Validation feedback message"
)
```
Validation feedback message.
### prepare_field_params
```
prepare_field_params(
params: dict[str, Any], field_value: Any
) -> dict[str, Any]
```
Convert choices to SelectOption objects with proper selected state.
## Checkbox
Bootstrap checkbox component.
Methods:
- **`prepare_field_params`** – Set checked state from field value instead of using value.
Attributes:
- **`template`** (`str`) – Bootstrap checkbox component template.
- **`name`** (`str`) – Checkbox name attribute.
- **`label`** (`str`) – Checkbox label.
- **`value`** (`str`) – Checkbox value.
- **`checked`** (`bool`) – Checked state.
- **`disabled`** (`bool`) – Whether the checkbox is disabled.
- **`inline`** (`bool`) – Display inline.
- **`validation_state`** (`str | None`) – Validation state: valid, invalid.
- **`validation_message`** (`str | None`) – Validation feedback message.
### template
```
template: str = Field(
"components/checkbox.html", frozen=True
)
```
Bootstrap checkbox component template.
### name
```
name: str = Field(
..., description="Checkbox name attribute"
)
```
Checkbox name attribute.
### label
```
label: str = Field(..., description='Checkbox label')
```
Checkbox label.
### value
```
value: str = Field(
default="1", description="Checkbox value"
)
```
Checkbox value.
### checked
```
checked: bool = Field(
default=False, description="Checked state"
)
```
Checked state.
### disabled
```
disabled: bool = Field(
default=False, description="Disabled checkbox"
)
```
Whether the checkbox is disabled.
### inline
```
inline: bool = Field(
default=False, description="Display inline"
)
```
Display inline.
### validation_state
```
validation_state: str | None = Field(
default=None,
description="Validation state: valid, invalid",
)
```
Validation state: valid, invalid.
### validation_message
```
validation_message: str | None = Field(
default=None, description="Validation feedback message"
)
```
Validation feedback message.
### prepare_field_params
```
prepare_field_params(
params: dict[str, Any], field_value: Any
) -> dict[str, Any]
```
Set checked state from field value instead of using value.
## Radio
Bootstrap radio button component.
Attributes:
- **`template`** (`str`) – Bootstrap radio button component template.
- **`name`** (`str`) – Radio name attribute (same for group).
- **`label`** (`str`) – Radio label.
- **`value`** (`str`) – Radio value.
- **`checked`** (`bool`) – Checked state.
- **`disabled`** (`bool`) – Whether the radio is disabled.
- **`inline`** (`bool`) – Display inline.
- **`validation_state`** (`str | None`) – Validation state: valid, invalid.
- **`validation_message`** (`str | None`) – Validation feedback message.
### template
```
template: str = Field('components/radio.html', frozen=True)
```
Bootstrap radio button component template.
### name
```
name: str = Field(
..., description="Radio name attribute (same for group)"
)
```
Radio name attribute (same for group).
### label
```
label: str = Field(..., description='Radio label')
```
Radio label.
### value
```
value: str = Field(..., description='Radio value')
```
Radio value.
### checked
```
checked: bool = Field(
default=False, description="Checked state"
)
```
Checked state.
### disabled
```
disabled: bool = Field(
default=False, description="Disabled radio"
)
```
Whether the radio is disabled.
### inline
```
inline: bool = Field(
default=False, description="Display inline"
)
```
Display inline.
### validation_state
```
validation_state: str | None = Field(
default=None,
description="Validation state: valid, invalid",
)
```
Validation state: valid, invalid.
### validation_message
```
validation_message: str | None = Field(
default=None, description="Validation feedback message"
)
```
Validation feedback message.
## Switch
Bootstrap switch component (styled checkbox).
Attributes:
- **`template`** (`str`) – Bootstrap switch component template.
- **`name`** (`str`) – Switch name attribute.
- **`label`** (`str`) – Switch label.
- **`value`** (`str`) – Switch value.
- **`checked`** (`bool`) – Checked state.
- **`disabled`** (`bool`) – Whether the switch is disabled.
- **`validation_state`** (`str | None`) – Validation state: valid, invalid.
- **`validation_message`** (`str | None`) – Validation feedback message.
### template
```
template: str = Field("components/switch.html", frozen=True)
```
Bootstrap switch component template.
### name
```
name: str = Field(..., description='Switch name attribute')
```
Switch name attribute.
### label
```
label: str = Field(..., description='Switch label')
```
Switch label.
### value
```
value: str = Field(default='1', description='Switch value')
```
Switch value.
### checked
```
checked: bool = Field(
default=False, description="Checked state"
)
```
Checked state.
### disabled
```
disabled: bool = Field(
default=False, description="Disabled switch"
)
```
Whether the switch is disabled.
### validation_state
```
validation_state: str | None = Field(
default=None,
description="Validation state: valid, invalid",
)
```
Validation state: valid, invalid.
### validation_message
```
validation_message: str | None = Field(
default=None, description="Validation feedback message"
)
```
Validation feedback message.
## FileInput
Bootstrap file input component.
Attributes:
- **`template`** (`str`) – Bootstrap file input component template.
- **`name`** (`str`) – File input name attribute.
- **`label`** (`str | None`) – File input label.
- **`help_text`** (`str | None`) – Help text below input.
- **`required`** (`bool`) – Whether the file input is required.
- **`disabled`** (`bool`) – Whether the file input is disabled.
- **`multiple`** (`bool`) – Allow multiple files.
- **`accept`** (`str | None`) – Accepted file types (e.g., 'image/\*').
- **`validation_state`** (`str | None`) – Validation state: valid, invalid.
- **`validation_message`** (`str | None`) – Validation feedback message.
### template
```
template: str = Field(
"components/file_input.html", frozen=True
)
```
Bootstrap file input component template.
### name
```
name: str = Field(
..., description="File input name attribute"
)
```
File input name attribute.
### label
```
label: str | None = Field(
default=None, description="File input label"
)
```
File input label.
### help_text
```
help_text: str | None = Field(
default=None, description="Help text below input"
)
```
Help text below input.
### required
```
required: bool = Field(
default=False, description="Required field"
)
```
Whether the file input is required.
### disabled
```
disabled: bool = Field(
default=False, description="Disabled input"
)
```
Whether the file input is disabled.
### multiple
```
multiple: bool = Field(
default=False, description="Allow multiple files"
)
```
Allow multiple files.
### accept
```
accept: str | None = Field(
default=None,
description="Accepted file types (e.g., 'image/*')",
)
```
Accepted file types (e.g., 'image/\*').
### validation_state
```
validation_state: str | None = Field(
default=None,
description="Validation state: valid, invalid",
)
```
Validation state: valid, invalid.
### validation_message
```
validation_message: str | None = Field(
default=None, description="Validation feedback message"
)
```
Validation feedback message.
## Range
Bootstrap range slider component.
Attributes:
- **`template`** (`str`) – Bootstrap range slider component template.
- **`name`** (`str`) – Range name attribute.
- **`label`** (`str | None`) – Range label.
- **`min`** (`float`) – Minimum value.
- **`max`** (`float`) – Maximum value.
- **`step`** (`float`) – Step increment.
- **`value`** (`float`) – Current value.
- **`disabled`** (`bool`) – Whether the range is disabled.
- **`help_text`** (`str | None`) – Help text below range.
- **`validation_state`** (`str | None`) – Validation state: valid, invalid.
- **`validation_message`** (`str | None`) – Validation feedback message.
### template
```
template: str = Field('components/range.html', frozen=True)
```
Bootstrap range slider component template.
### name
```
name: str = Field(..., description='Range name attribute')
```
Range name attribute.
### label
```
label: str | None = Field(
default=None, description="Range label"
)
```
Range label.
### min
```
min: float = Field(default=0, description='Minimum value')
```
Minimum value.
### max
```
max: float = Field(default=100, description="Maximum value")
```
Maximum value.
### step
```
step: float = Field(default=1, description="Step increment")
```
Step increment.
### value
```
value: float = Field(
default=50, description="Current value"
)
```
Current value.
### disabled
```
disabled: bool = Field(
default=False, description="Disabled range"
)
```
Whether the range is disabled.
### help_text
```
help_text: str | None = Field(
default=None, description="Help text below range"
)
```
Help text below range.
### validation_state
```
validation_state: str | None = Field(
default=None,
description="Validation state: valid, invalid",
)
```
Validation state: valid, invalid.
### validation_message
```
validation_message: str | None = Field(
default=None, description="Validation feedback message"
)
```
Validation feedback message.
## ChoicesSelect
Choices.js enhanced select component.
Methods:
- **`prepare_field_params`** – Convert choices to ChoiceOption objects with proper selected state.
Attributes:
- **`id`** (`str`) – Element ID for the select.
- **`template`** (`str`) – Choices.js select component template.
- **`name`** (`str`) – Select name attribute.
- **`label`** (`str | None`) – Select label.
- **`options`** (`list[ChoiceOption | ChoiceGroup]`) – Select options or option groups.
- **`placeholder`** (`str`) – Placeholder text.
- **`search_enabled`** (`bool`) – Enable search functionality.
- **`search_placeholder`** (`str`) – Search placeholder.
- **`multiple`** (`bool`) – Allow multiple selections.
- **`remove_button`** (`bool`) – Show remove button for selected items.
- **`max_item_count`** (`int | None`) – Max items that can be selected (for multiple).
- **`help_text`** (`str | None`) – Help text below select.
- **`required`** (`bool`) – Whether the select is required.
- **`disabled`** (`bool`) – Whether the select is disabled.
- **`validation_state`** (`str | None`) – Validation state: valid, invalid.
- **`validation_message`** (`str | None`) – Validation feedback message.
### id
```
id: str
```
Element ID for the select.
### template
```
template: str = Field(
"components/choices_select.html", frozen=True
)
```
Choices.js select component template.
### name
```
name: str = Field(..., description='Select name attribute')
```
Select name attribute.
### label
```
label: str | None = Field(
default=None, description="Select label"
)
```
Select label.
### options
```
options: list[ChoiceOption | ChoiceGroup] = Field(
default_factory=list
)
```
Select options or option groups.
### placeholder
```
placeholder: str = Field(
default="Select an option",
description="Placeholder text",
)
```
Placeholder text.
### search_enabled
```
search_enabled: bool = Field(
default=True, description="Enable search functionality"
)
```
Enable search functionality.
### search_placeholder
```
search_placeholder: str = Field(
default="Type to search",
description="Search placeholder",
)
```
Search placeholder.
### multiple
```
multiple: bool = Field(
default=False, description="Allow multiple selections"
)
```
Allow multiple selections.
### remove_button
```
remove_button: bool = Field(
default=True,
description="Show remove button for selected items",
)
```
Show remove button for selected items.
### max_item_count
```
max_item_count: int | None = Field(
default=None,
description="Max items that can be selected (for multiple)",
)
```
Max items that can be selected (for multiple).
### help_text
```
help_text: str | None = Field(
default=None, description="Help text below select"
)
```
Help text below select.
### required
```
required: bool = Field(
default=False, description="Required field"
)
```
Whether the select is required.
### disabled
```
disabled: bool = Field(
default=False, description="Disabled select"
)
```
Whether the select is disabled.
### validation_state
```
validation_state: str | None = Field(
default=None,
description="Validation state: valid, invalid",
)
```
Validation state: valid, invalid.
### validation_message
```
validation_message: str | None = Field(
default=None, description="Validation feedback message"
)
```
Validation feedback message.
### prepare_field_params
```
prepare_field_params(
params: dict[str, Any], field_value: Any
) -> dict[str, Any]
```
Convert choices to ChoiceOption objects with proper selected state.
## DatePicker
Flatpickr date picker component.
Methods:
- **`prepare_field_params`** – Convert date/datetime values to ISO string format.
Attributes:
- **`id`** (`str`) – Element ID for the date picker.
- **`template`** (`str`) – Flatpickr date picker component template.
- **`name`** (`str`) – Input name attribute.
- **`label`** (`str | None`) – Input label.
- **`value`** (`str | None`) – Initial date value (YYYY-MM-DD).
- **`placeholder`** (`str | None`) – Placeholder text.
- **`mode`** (`str`) – Selection mode: single, multiple, range.
- **`enable_time`** (`bool`) – Enable time picker.
- **`time_24hr`** (`bool`) – Use 24-hour time format.
- **`date_format`** (`str`) – Date format string.
- **`min_date`** (`str | None`) – Minimum selectable date.
- **`max_date`** (`str | None`) – Maximum selectable date.
- **`disable_dates`** (`list[str]`) – Dates to disable.
- **`inline`** (`bool`) – Display calendar inline.
- **`help_text`** (`str | None`) – Help text below input.
- **`required`** (`bool`) – Whether the date picker is required.
- **`disabled`** (`bool`) – Whether the date picker is disabled.
- **`validation_state`** (`str | None`) – Validation state: valid, invalid.
- **`validation_message`** (`str | None`) – Validation feedback message.
### id
```
id: str
```
Element ID for the date picker.
### template
```
template: str = Field(
"components/datepicker.html", frozen=True
)
```
Flatpickr date picker component template.
### name
```
name: str = Field(..., description='Input name attribute')
```
Input name attribute.
### label
```
label: str | None = Field(
default=None, description="Input label"
)
```
Input label.
### value
```
value: str | None = Field(
default=None,
description="Initial date value (YYYY-MM-DD)",
)
```
Initial date value (YYYY-MM-DD).
### placeholder
```
placeholder: str | None = Field(
default="Select date", description="Placeholder text"
)
```
Placeholder text.
### mode
```
mode: str = Field(
default="single",
description="Selection mode: single, multiple, range",
)
```
Selection mode: single, multiple, range.
### enable_time
```
enable_time: bool = Field(
default=False, description="Enable time picker"
)
```
Enable time picker.
### time_24hr
```
time_24hr: bool = Field(
default=True, description="Use 24-hour time format"
)
```
Use 24-hour time format.
### date_format
```
date_format: str = Field(
default="Y-m-d", description="Date format string"
)
```
Date format string.
### min_date
```
min_date: str | None = Field(
default=None, description="Minimum selectable date"
)
```
Minimum selectable date.
### max_date
```
max_date: str | None = Field(
default=None, description="Maximum selectable date"
)
```
Maximum selectable date.
### disable_dates
```
disable_dates: list[str] = Field(
default_factory=list, description="Dates to disable"
)
```
Dates to disable.
### inline
```
inline: bool = Field(
default=False, description="Display calendar inline"
)
```
Display calendar inline.
### help_text
```
help_text: str | None = Field(
default=None, description="Help text below input"
)
```
Help text below input.
### required
```
required: bool = Field(
default=False, description="Required field"
)
```
Whether the date picker is required.
### disabled
```
disabled: bool = Field(
default=False, description="Disabled input"
)
```
Whether the date picker is disabled.
### validation_state
```
validation_state: str | None = Field(
default=None,
description="Validation state: valid, invalid",
)
```
Validation state: valid, invalid.
### validation_message
```
validation_message: str | None = Field(
default=None, description="Validation feedback message"
)
```
Validation feedback message.
### prepare_field_params
```
prepare_field_params(
params: dict[str, Any], field_value: Any
) -> dict[str, Any]
```
Convert date/datetime values to ISO string format.
## SubmitButton
Bootstrap submit button component for forms.
Methods:
- **`prepare_field_params`** – Submit buttons don't use field values.
Attributes:
- **`template`** (`str`) – Bootstrap submit button component template.
- **`text`** (`str`) – Button text.
- **`button_type`** (`str`) – Button type: submit, button, reset.
- **`name`** (`str | None`) – Button name attribute.
- **`classes`** (`list[str]`) – CSS classes.
- **`disabled`** (`bool`) – Whether the button is disabled.
- **`form`** (`str | None`) – Form element the button is associated with.
- **`formaction`** (`str | None`) – URL where form data is sent.
- **`formenctype`** (`str | None`) – How form data is encoded.
- **`formmethod`** (`str | None`) – HTTP method when button is clicked.
- **`formnovalidate`** (`bool`) – Whether to bypass form validation.
- **`formtarget`** (`str | None`) – Where to display response.
### template
```
template: str = Field(
"components/submit_button.html", frozen=True
)
```
Bootstrap submit button component template.
### text
```
text: str = Field(
default="Submit", description="Button text"
)
```
Button text.
### button_type
```
button_type: str = Field(
default="submit",
description="Button type: submit, button, reset",
)
```
Button type: submit, button, reset.
### name
```
name: str | None = Field(
default=None, description="Button name attribute"
)
```
Button name attribute.
### classes
```
classes: list[str] = Field(
default_factory=lambda: ["btn", "btn-primary"],
description="CSS classes",
)
```
CSS classes.
### disabled
```
disabled: bool = Field(
default=False, description="Disabled button"
)
```
Whether the button is disabled.
### form
```
form: str | None = Field(
default=None,
description="Form element the button is associated with",
)
```
Form element the button is associated with.
### formaction
```
formaction: str | None = Field(
default=None, description="URL where form data is sent"
)
```
URL where form data is sent.
### formenctype
```
formenctype: str | None = Field(
default=None, description="How form data is encoded"
)
```
How form data is encoded.
### formmethod
```
formmethod: str | None = Field(
default=None,
description="HTTP method when button is clicked",
)
```
HTTP method when button is clicked.
### formnovalidate
```
formnovalidate: bool = Field(
default=False,
description="Whether to bypass form validation",
)
```
Whether to bypass form validation.
### formtarget
```
formtarget: str | None = Field(
default=None, description="Where to display response"
)
```
Where to display response.
### prepare_field_params
```
prepare_field_params(
params: dict[str, Any], field_value: Any
) -> dict[str, Any]
```
Submit buttons don't use field values.