Skip to main content

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.
template_name
str
required
The name/path of the template file to render
context
dict
default:"{}"
Dictionary of variables to pass to the template
html
str
The rendered HTML string
# 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.
name
str
required
The route name
params
dict
Route parameters as keyword arguments
url
str
The generated URL path
<!-- 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.
token
str
The CSRF token string
<!-- 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.
user
User | None
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.
messages
dict
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.
path
str
required
The path to the asset (relative to public directory)
versioning
bool
default:"True"
Whether to add version query parameter for cache-busting
url
str
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.
obj
Any
required
The object to dump
output
str
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_date

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>

Formatting Filters

filesizeformat

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_number

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:
  1. User template directory (configured in settings)
  2. 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

  1. 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
  1. CSRF Protection: Always include CSRF tokens in forms
  2. Asset Versioning: Keep versioning enabled in production for proper cache-busting
  3. Error Handling: Catch template exceptions and provide fallback templates
  4. Context Minimization: Only pass necessary data to templates
  5. Filter Usage: Leverage filters for formatting instead of pre-formatting in controllers

Performance Tips

  • 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

Build docs developers (and LLMs) love