# Jinja2 / FastAPI — Referenz Referenz für den Einsatz von CSS-Framework in FastAPI-Apps mit Jinja2-Templates und HTMX. Schnellreferenz für Komponenten und Tokens: [SKILL.md](../SKILL.md) --- ## Setup ```python # main.py from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from starlette.middleware.sessions import SessionMiddleware app = FastAPI() app.add_middleware(SessionMiddleware, secret_key="...") app.mount("/static", StaticFiles(directory="static"), name="static") templates = Jinja2Templates(directory="templates") ``` Statische Dateien werden direkt über den Pfad `/static/...` referenziert — kein `url_for` im Template nötig. --- ## Basis-Template `templates/base.html`: ```html {% block title %}App{% endblock %} {% block head %}{% endblock %}
{% block mobile_title %}App{% endblock %}
{% for msg in request.session.pop('_messages', []) %}

{{ msg.text }}

{% endfor %} {% block content %}{% endblock %}
{% block scripts %}{% endblock %} ``` --- ## Template rendern ```python from fastapi import Request from fastapi.responses import HTMLResponse @router.get("/", response_class=HTMLResponse) async def index(request: Request): return templates.TemplateResponse(request, "index.html", { "items": await get_items() }) ``` `request` wird immer explizit übergeben — Jinja2Templates macht ihn im Template als `request` verfügbar. --- ## Flash Messages FastAPI hat kein eingebautes Flash-System. Einfache Session-Lösung mit `SessionMiddleware`: ```python # Helfer-Funktion def flash(request: Request, text: str, category: str = "info"): messages = request.session.setdefault('_messages', []) messages.append({'text': text, 'category': category}) # In einer Route @router.post("/profil") async def profil_speichern(request: Request): flash(request, 'Profil aktualisiert', 'success') flash(request, 'Ungültige Eingabe', 'danger') flash(request, 'Bitte bestätigen', 'warning') return RedirectResponse("/profil", status_code=303) ``` Im Template werden die Messages über `request.session.pop('_messages', [])` gelesen (siehe Basis-Template oben). --- ## HTMX-Muster ### Button Loading State `hx-disabled-elt="this"` sperrt den Button; `.htmx-indicator` blendet den Spinner ein solange die Anfrage läuft. ```html
``` ### Formular mit Inline-Validation (422) FastAPI antwortet bei Fehlern mit HTTP 422 und gibt das Formular-Partial zurück: ```python from fastapi import Form, status from fastapi.responses import HTMLResponse @router.post("/api/formular", response_class=HTMLResponse) async def formular( request: Request, email: str = Form(...) ): errors = {} if "@" not in email: errors["email"] = "Ungültige E-Mail-Adresse" if errors: return templates.TemplateResponse( request, "_form.html", {"form": {"email": email}, "errors": errors}, status_code=422 ) # Erfolg return templates.TemplateResponse(request, "_form_success.html", {}) ``` ```html
{% if errors and errors.email %} {{ errors.email }} {% endif %}
``` ### Skeleton als Ladeindikator ```html
``` ### Modal via HTMX befüllen ```html ``` ### Toast über Server-Response (HX-Trigger) ```python import json from fastapi.responses import HTMLResponse @router.post("/api/speichern", response_class=HTMLResponse) async def speichern(request: Request): content = templates.TemplateResponse(request, "_partial.html", {}) content.headers['HX-Trigger'] = json.dumps({ 'showToast': {'message': 'Gespeichert', 'type': 'success'} }) return content ``` Listener im `` des Basis-Templates: ```html ``` ### Live-Suche ```html
``` ```python @router.get("/api/suche", response_class=HTMLResponse) async def suche(request: Request, q: str = ""): results = await search_items(q) return templates.TemplateResponse(request, "_ergebnisse.html", {"results": results}) ``` ### Unendliches Nachladen (Intersection Observer) ```html
{% for item in items %} {% endfor %} {% if has_more %}
{% endif %}
``` --- ## Formular-Validation mit Pydantic ```python from pydantic import BaseModel, EmailStr, ValidationError class ProfilForm(BaseModel): name: str email: EmailStr @router.post("/api/profil", response_class=HTMLResponse) async def profil_speichern( request: Request, name: str = Form(...), email: str = Form(...) ): errors = {} try: ProfilForm(name=name, email=email) except ValidationError as e: for err in e.errors(): errors[err['loc'][0]] = err['msg'] if errors: return templates.TemplateResponse( request, "_profil_form.html", {"form": {"name": name, "email": email}, "errors": errors}, status_code=422 ) await update_profil(name=name, email=email) flash(request, 'Profil aktualisiert', 'success') return RedirectResponse("/profil", status_code=303) ``` --- ## Pagination ```python # Route @router.get("/liste", response_class=HTMLResponse) async def liste(request: Request, page: int = 1, per_page: int = 20): total = await count_items() items = await get_items(offset=(page - 1) * per_page, limit=per_page) return templates.TemplateResponse(request, "liste.html", { "items": items, "page": page, "per_page": per_page, "total": total, "pages": (total + per_page - 1) // per_page, }) ``` ```html {% if pages > 1 %} {% endif %} ``` --- ## Dos & Don'ts **Do:** - Farben und Abstände ausschließlich über Tokens: `var(--accent)`, `var(--space-4)` usw. - Semantisches HTML classless einsetzen: `

`, `

`, `` brauchen keine Klassen - `request` immer explizit an `TemplateResponse` übergeben — Starlette macht ihn im Template verfügbar - HTMX-Attribute direkt auf Framework-Elemente — kein Wrapper-`
` nötig - Globale JS-Funktionen nutzen (`openModal`, `showToast`, `sortTable` usw.) **Don't:** - Keine `style`-Attribute, keine `on…`-Attribute, kein Inline-`