Views & Templates

Chapter 6: Building user interfaces with Jinja2, HTMX, and Bootstrap.

Views are what users actually see—the HTML pages that display your data. sparQ uses Jinja2 templates combined with HTMX for interactivity and Bootstrap for styling.

Template Basics

Templates live in views/templates/myapp/desktop/ within your app folder. They extend the base layout:

{% extends "core/desktop/base.html" %}

{% block content %}
<div class="container py-4">
    <h1>My Tasks</h1>
    <p>You have {{ tasks|length }} tasks.</p>
</div>
{% endblock %}

The {% extends "core/desktop/base.html" %} gives you the full sparQ layout with header, sidebar, and footer.

Displaying Data

Use {{ }} to output variables passed from your controller:

<!-- Simple variable -->
<h1>{{ task.title }}</h1>

<!-- With filter -->
<p>Created {{ task.created_at|timeago }}</p>

<!-- Conditional -->
{% if task.done %}
    <span class="badge bg-success">Done</span>
{% else %}
    <span class="badge bg-warning">Pending</span>
{% endif %}

<!-- Loop -->
{% for task in tasks %}
    <div>{{ task.title }}</div>
{% endfor %}

Global Template Variables

These variables are always available in templates:

Variable Description
current_user The logged-in user (or anonymous)
g.current_module Current module's manifest
g.lang Current language code ("en", "es", etc.)
<!-- Show current user -->
{% if current_user.is_authenticated %}
    Welcome, {{ current_user.name }}!
{% else %}
    <a href="{{ url_for('core.auth.login') }}">Log in</a>
{% endif %}

Template Functions

Translation with _()

<h1>{{ _("Welcome") }}</h1>
<button>{{ _("Save") }}</button>

URLs with url_for()

<a href="{{ url_for('tasks.main.index') }}">All Tasks</a>
<a href="{{ url_for('tasks.main.show', id=task.id) }}">View</a>

Check if Module Exists

{% if module_enabled('billing') %}
    <a href="{{ url_for('billing.invoices.index') }}">Invoices</a>
{% endif %}

HTMX for Interactivity

HTMX lets you build dynamic interfaces without writing JavaScript. Add attributes to HTML elements to make them send AJAX requests:

Click to Load

<button hx-get="/tasks/more"
        hx-target="#task-list"
        hx-swap="beforeend">
    Load More
</button>
<div id="task-list">
    <!-- Tasks appear here -->
</div>

Search with Debounce

<input type="search"
       name="q"
       placeholder="Search tasks..."
       hx-get="/tasks/search"
       hx-trigger="keyup changed delay:300ms"
       hx-target="#results">

<div id="results"></div>

Inline Edit

<!-- Display mode -->
<span hx-get="/tasks/{{ task.id }}/edit-form"
      hx-trigger="click"
      hx-swap="outerHTML"
      class="editable">
    {{ task.title }}
</span>

<!-- Edit mode (returned by /edit-form) -->
<form hx-post="/tasks/{{ task.id }}"
      hx-swap="outerHTML">
    <input name="title" value="{{ task.title }}">
    <button>Save</button>
</form>

Form Submission

<form hx-post="/tasks/create"
      hx-target="#task-list"
      hx-swap="afterbegin"
      hx-on::after-request="this.reset()">
    <input name="title" placeholder="New task..." required>
    <button>Add</button>
</form>
Image: Animation showing HTMX form adding a task without page reload

Bootstrap Components

sparQ includes Bootstrap 5. Use its classes for consistent styling:

Cards

<div class="card">
    <div class="card-header">Task Details</div>
    <div class="card-body">
        <h5 class="card-title">{{ task.title }}</h5>
        <p class="card-text">{{ task.description }}</p>
    </div>
</div>

Tables

<table class="table table-hover">
    <thead>
        <tr>
            <th>Task</th>
            <th>Status</th>
            <th>Actions</th>
        </tr>
    </thead>
    <tbody>
        {% for task in tasks %}
        <tr>
            <td>{{ task.title }}</td>
            <td>
                <span class="badge bg-{{ 'success' if task.done else 'secondary' }}">
                    {{ "Done" if task.done else "Pending" }}
                </span>
            </td>
            <td>
                <a href="{{ url_for('tasks.main.edit', id=task.id) }}"
                   class="btn btn-sm btn-outline-primary">Edit</a>
            </td>
        </tr>
        {% endfor %}
    </tbody>
</table>

Forms

<form method="POST">
    <div class="mb-3">
        <label class="form-label">Task Title</label>
        <input type="text" name="title" class="form-control" required>
    </div>
    <div class="mb-3">
        <label class="form-label">Description</label>
        <textarea name="description" class="form-control" rows="3"></textarea>
    </div>
    <button type="submit" class="btn btn-primary">Save Task</button>
</form>

Alpine.js for Local State

Use Alpine.js for small UI interactions like toggles and dropdowns:

<div x-data="{ open: false }">
    <button @click="open = !open">
        Toggle Details
    </button>
    <div x-show="open" x-transition>
        These are the hidden details.
    </div>
</div>

Template File Structure

Templates are organized in a specific folder structure:

myapp/ └── views/ ├── templates/ │ └── myapp/ # Same name as your app │ ├── desktop/ │ │ ├── index.html │ │ ├── show.html │ │ └── partials/ │ │ └── task_row.html │ └── mobile/ │ └── index.html └── assets/ └── css/ └── myapp.css

The nested templates/myapp/desktop/ structure ensures template names don't collide across apps. When rendering, reference templates as myapp/desktop/index.html.

Device-Aware Templates

Desktop templates must be placed in a desktop/ subfolder. Mobile templates are optional and go in a mobile/ subfolder with the same file name.

Use render_device_template() in your controllers instead of Flask's render_template. It automatically serves the mobile version on mobile devices and falls back to desktop if no mobile template exists:

from system.device.template import render_device_template

# Always pass the desktop path — mobile is resolved automatically
return render_device_template('myapp/desktop/index.html', tasks=tasks)

To test mobile rendering during development, append ?device=mobile to any URL.

Partials for Reusability

Extract repeated HTML into partial templates:

<!-- views/templates/tasks/desktop/partials/task_row.html -->
<tr>
    <td>{{ task.title }}</td>
    <td>{{ task.status }}</td>
</tr>

<!-- Use it in your main template -->
{% for task in tasks %}
    {% include "tasks/desktop/partials/task_row.html" %}
{% endfor %}

Key Takeaways