Overview
The TemplateRenderer is Framefox’s template rendering engine built on Jinja2. It provides a powerful system for rendering HTML templates with built-in support for forms, security features, asset management, and custom filters.
Key Features
- Jinja2 Integration: Full Jinja2 template engine with strict undefined checking
- Automatic Context Management: Request, user, and flash messages automatically available
- Security: Built-in CSRF token generation and management
- Asset Versioning: Automatic cache-busting for static assets
- Custom Filters: Rich set of built-in filters for data formatting
- Template Profiling: Performance monitoring integration
- Form Extensions: Advanced form rendering capabilities
Initialization
from framefox.core.templates.template_renderer import TemplateRenderer
# Initialize renderer
renderer = TemplateRenderer()
The renderer automatically:
- Configures Jinja2 with user and framework template directories
- Registers global functions (url_for, csrf_token, etc.)
- Registers custom filters
- Sets up form extensions
- Integrates with the service container
Core Methods
render()
Render a template with the provided context.
The name/path of the template file to render
Dictionary of variables to pass to the template
# Basic rendering
html = renderer.render('home.html', {
'title': 'Welcome',
'user': current_user
})
# With nested templates
html = renderer.render('layouts/dashboard.html', {
'stats': dashboard_stats,
'notifications': user_notifications
})
# Request is automatically added to context
html = renderer.render('profile.html')
# Template has access to {{ request.method }}, {{ request.path }}, etc.
Automatic Context Variables:
Even if not explicitly passed, these are always available:
request - Current request object (if available)
- All registered global functions
Error Handling:
Raises exceptions for:
TemplateNotFound - Template file doesn’t exist
TemplateSyntaxError - Invalid Jinja2 syntax
UndefinedError - Referenced undefined variable (strict mode)
from jinja2.exceptions import TemplateNotFound
try:
html = renderer.render('missing.html')
except TemplateNotFound:
# Handle missing template
html = renderer.render('errors/404.html')
Global Template Functions
These functions are automatically available in all templates without explicit context passing.
url_for()
Generate URLs for named routes.
Route parameters as keyword arguments
<!-- In templates -->
<a href="{{ url_for('user_profile', user_id=42) }}">View Profile</a>
<form action="{{ url_for('submit_form') }}" method="post">
<!-- form fields -->
</form>
<!-- Dynamic navigation -->
<nav>
<a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('about') }}">About</a>
</nav>
csrf_token()
Generate and retrieve a CSRF token for form protection.
<!-- In forms -->
<form method="post" action="{{ url_for('update_profile') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="text" name="username">
<button type="submit">Update</button>
</form>
<!-- Or with macro -->
{{ csrf_input() }}
Behavior:
- Generates a new token if one doesn’t exist
- Stores token in session
- Automatically saved to session storage
current_user()
Get the currently authenticated user.
The current user object, or None if not authenticated
<!-- Conditional rendering based on auth -->
{% if current_user() %}
<p>Welcome, {{ current_user().username }}!</p>
<a href="{{ url_for('logout') }}">Logout</a>
{% else %}
<a href="{{ url_for('login') }}">Login</a>
{% endif %}
<!-- Access user properties -->
{% set user = current_user() %}
{% if user and user.is_admin %}
<a href="{{ url_for('admin_panel') }}">Admin</a>
{% endif %}
get_flash_messages()
Retrieve flash messages from the session.
Dictionary of flash messages organized by category
<!-- Display flash messages -->
{% set messages = get_flash_messages() %}
{% for category, message_list in messages.items() %}
{% for message in message_list %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endfor %}
<!-- Check for specific category -->
{% if get_flash_messages().success %}
<div class="success-banner">
{% for msg in get_flash_messages().success %}
{{ msg }}
{% endfor %}
</div>
{% endif %}
asset()
Generate versioned URLs for static assets.
The path to the asset (relative to public directory)
Whether to add version query parameter for cache-busting
The complete asset URL with version parameter
<!-- CSS and JS with automatic cache-busting -->
<link rel="stylesheet" href="{{ asset('css/style.css') }}">
<script src="{{ asset('js/app.js') }}"></script>
<!-- Images -->
<img src="{{ asset('images/logo.png') }}" alt="Logo">
<!-- Without versioning -->
<link rel="stylesheet" href="{{ asset('css/print.css', versioning=False) }}">
Versioning Strategy:
- Uses file modification time to generate MD5 hash
- Adds
?v=<hash> query parameter
- Falls back to timestamp if file doesn’t exist
- Ensures browsers cache-bust when files change
request
Access the current request object.
<!-- Request properties -->
<p>Method: {{ request.method }}</p>
<p>Path: {{ request.path }}</p>
<p>Query: {{ request.query_params }}</p>
<!-- Conditional rendering based on request -->
{% if request.path.startswith('/admin') %}
<nav><!-- Admin navigation --></nav>
{% endif %}
<!-- Check request method -->
{% if request.method == 'POST' %}
<p>Form submitted</p>
{% endif %}
dump()
Debug helper to pretty-print objects.
Pretty-formatted string representation
<!-- Debug output -->
<pre>{{ dump(user) }}</pre>
<pre>{{ dump(request.headers) }}</pre>
<!-- Debug complex objects -->
{% set debug_info = {
'user': current_user(),
'request': request,
'messages': get_flash_messages()
} %}
<pre>{{ dump(debug_info) }}</pre>
Custom Filters
Framefox provides a rich set of custom Jinja2 filters for data formatting and manipulation.
Date & Time Filters
date
Format dates with custom format strings.
<!-- Default format: dd/mm/yyyy -->
{{ user.created_at|date }}
<!-- Custom format -->
{{ user.created_at|date("%B %d, %Y") }}
<!-- Output: December 25, 2023 -->
<!-- Works with timestamps -->
{{ 1640995200|date("%Y-%m-%d") }}
<!-- Output: 2022-01-01 -->
Format dates with time (default: dd/mm/yyyy HH:MM).
{{ order.placed_at|format_date }}
<!-- Output: 15/03/2024 14:30 -->
{{ order.placed_at|format_date("%d %b %Y at %I:%M %p") }}
<!-- Output: 15 Mar 2024 at 02:30 PM -->
time
Format millisecond values as human-readable time.
<!-- Render duration -->
{{ render_time|time }}
<!-- Output: 1s 250ms -->
{{ long_duration|time(include_ms=False) }}
<!-- Output: 2h 15min 30s -->
<!-- Processing time -->
<p>Processed in {{ processing_ms|time }}</p>
Format byte sizes in human-readable format.
{{ file.size|filesizeformat }}
<!-- 1024 → "1.00 KB" -->
<!-- 1048576 → "1.00 MB" -->
<!-- 1073741824 → "1.00 GB" -->
<p>File size: {{ upload.size|filesizeformat }}</p>
Format numbers with custom separators.
<!-- Default: 2 decimals, comma separator -->
{{ price|format_number }}
<!-- 1234.56 → "1 234,56" -->
<!-- Custom configuration -->
{{ amount|format_number(decimal_places=0, decimal_separator=".", thousand_separator=",") }}
<!-- 1234567 → "1,234,567" -->
json_encode
Encode values as JSON strings.
<!-- Pass data to JavaScript -->
<script>
const userData = {{ user|json_encode|safe }};
const config = {{ app_config|json_encode|safe }};
</script>
<!-- Data attributes -->
<div data-config="{{ options|json_encode }}"></div>
Data Filters
relation_display
Display ORM relationships in human-readable format.
<!-- Single relation -->
{{ user.company|relation_display }}
<!-- Shows company name if available -->
<!-- Collection -->
{{ post.tags|relation_display }}
<!-- "Technology, Python, Web" or "5 items" -->
<!-- None values -->
{{ user.manager|relation_display }}
<!-- Output: "None" -->
min / max
Get minimum or maximum values.
<!-- From list -->
{{ prices|min }}
{{ prices|max }}
<!-- Compare with value -->
{{ user.age|min(18) }} <!-- Ensures at least 18 -->
{{ score|max(100) }} <!-- Caps at 100 -->
String Filters
split
Split strings into lists.
{% set tags = "python,web,framework"|split(",") %}
{% for tag in tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
<!-- Custom delimiter -->
{% set words = "hello world test"|split(" ") %}
slice
Extract substring slices.
{{ long_text|slice(0, 100) }}
<!-- First 100 characters -->
{{ description|slice(10) }}
<!-- From character 10 to end -->
lower
Convert strings to lowercase.
{{ username|lower }}
{{ email|lower }}
last
Get the last item from a list.
{{ breadcrumbs|last }}
{{ user.recent_orders|last }}
Template Context
Understanding what’s available in your templates:
<!-- Explicitly passed context -->
{# In Python: renderer.render('page.html', {'title': 'Hello', 'items': [...]}) #}
<h1>{{ title }}</h1>
{% for item in items %}
{{ item }}
{% endfor %}
<!-- Automatically available globals -->
{{ url_for('home') }}
{{ csrf_token() }}
{{ current_user() }}
{{ get_flash_messages() }}
{{ asset('css/style.css') }}
{{ request.path }}
<!-- Custom filters -->
{{ date_value|date }}
{{ file_size|filesizeformat }}
{{ data|json_encode }}
Advanced Usage
Custom Template Extensions
The renderer is configured with FormExtension for advanced form rendering:
<!-- Form rendering with extension -->
{{ form_start(form) }}
{{ form_row(form.username) }}
{{ form_row(form.email) }}
{{ form_row(form.password) }}
{{ form_end(form) }}
Template Profiling
The renderer automatically integrates with Framefox’s profiler:
# Profiling is automatic when profiler is enabled
html = renderer.render('complex_template.html', context)
# Memory usage and render time are tracked
Multiple Template Directories
The renderer searches templates in order:
- User template directory (configured in settings)
- Framework template directory (built-in templates)
# In settings
template_dir = "./templates"
# Renderer will search:
# 1. ./templates/page.html
# 2. framefox/core/templates/views/page.html
Complete Example
Here’s a comprehensive example bringing it all together:
# Controller
from framefox.core.templates.template_renderer import TemplateRenderer
class ProductController:
def __init__(self, renderer: TemplateRenderer):
self.renderer = renderer
def show_product(self, product_id: int):
product = self.product_repo.find(product_id)
related = self.product_repo.find_related(product_id)
return self.renderer.render('products/show.html', {
'product': product,
'related_products': related,
'page_title': f'{product.name} - Our Store'
})
<!-- templates/products/show.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{ page_title }}</title>
<link rel="stylesheet" href="{{ asset('css/products.css') }}">
</head>
<body>
<!-- Flash messages -->
{% set messages = get_flash_messages() %}
{% for category, msgs in messages.items() %}
{% for msg in msgs %}
<div class="alert-{{ category }}">{{ msg }}</div>
{% endfor %}
{% endfor %}
<!-- Navigation -->
<nav>
<a href="{{ url_for('home') }}">Home</a>
{% if current_user() %}
<a href="{{ url_for('cart') }}">Cart</a>
<span>{{ current_user().email }}</span>
{% else %}
<a href="{{ url_for('login') }}">Login</a>
{% endif %}
</nav>
<!-- Product details -->
<article>
<h1>{{ product.name }}</h1>
<img src="{{ asset(product.image_path) }}" alt="{{ product.name }}">
<div class="meta">
<span>Added: {{ product.created_at|date("%B %d, %Y") }}</span>
<span>Category: {{ product.category|relation_display }}</span>
</div>
<div class="price">
${{ product.price|format_number(decimal_places=2, decimal_separator=".", thousand_separator=",") }}
</div>
<div class="description">
{{ product.description }}
</div>
<!-- Add to cart form -->
<form method="post" action="{{ url_for('add_to_cart') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="product_id" value="{{ product.id }}">
<input type="number" name="quantity" value="1" min="1">
<button type="submit">Add to Cart</button>
</form>
</article>
<!-- Related products -->
<section>
<h2>Related Products</h2>
{% for related in related_products|slice(0, 4) %}
<a href="{{ url_for('product_show', product_id=related.id) }}">
<img src="{{ asset(related.image_path) }}" alt="{{ related.name }}">
<span>{{ related.name }}</span>
</a>
{% endfor %}
</section>
<!-- Debug info (development only) -->
{% if request.query_params.get('debug') %}
<pre>{{ dump(product) }}</pre>
{% endif %}
<script src="{{ asset('js/products.js') }}"></script>
</body>
</html>
Best Practices
- Template Organization: Use subdirectories to organize templates by feature
templates/
├── layouts/
│ ├── base.html
│ └── admin.html
├── products/
│ ├── list.html
│ └── show.html
└── users/
├── profile.html
└── settings.html
-
CSRF Protection: Always include CSRF tokens in forms
-
Asset Versioning: Keep versioning enabled in production for proper cache-busting
-
Error Handling: Catch template exceptions and provide fallback templates
-
Context Minimization: Only pass necessary data to templates
-
Filter Usage: Leverage filters for formatting instead of pre-formatting in controllers
- Template compilation is cached by Jinja2
- Asset version hashes are computed once per file modification
- Request context is automatically added only when needed
- Profiling has minimal overhead when disabled