Skip to content

Jinja2 Setup

Django IncludeContents provides complete feature parity with Django templates through a custom Jinja2 extension. All modern component features work seamlessly with Jinja2.

Full Feature Parity Achieved

Jinja2 support includes all features from Django templates:

  • Component system: Full {% includecontents %} tag support
  • HTML component syntax: <include:component> with full preprocessing
  • JavaScript framework attributes: @click, v-on:, x-on:, :bind syntax
  • Nested attribute syntax: inner.class, button.type, inner.@click
  • Advanced class manipulation: class:not, class="& base", class="additional &"
  • HTML content blocks: <content:name>Content</content:name> syntax
  • Props and validation: Required prop validation and defaults
  • Enum prop validation: Full validation with helpful error messages
  • Named content blocks: Traditional {% contents %} syntax
  • Attrs system: Undefined attribute handling with grouping
  • Context isolation: Components render in isolated contexts
  • Icon system: Full support including HTML syntax <icon:name>
  • WrapIf tag: Not available (use Jinja2 conditionals instead)

Installation

1. Install Django IncludeContents

pip install django-includecontents

2. Configure Jinja2 Backend

Add the Jinja2 backend to your TEMPLATES setting in settings.py:

# settings.py
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.jinja2.Jinja2',
        'DIRS': [
            BASE_DIR / 'jinja2',  # Your Jinja2 templates directory
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'environment': 'myproject.jinja2.environment',
            'extensions': [
                'includecontents.jinja2.IncludeContentsExtension',
            ],
        },
    },
    # You can also keep Django templates alongside Jinja2
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            BASE_DIR / 'templates',
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

3. Create Jinja2 Environment

Create a Jinja2 environment configuration file:

myproject/jinja2.py

from django.contrib.staticfiles.storage import staticfiles_storage
from django.urls import reverse
from jinja2 import Environment


def environment(**options):
    env = Environment(**options)
    env.globals.update({
        'static': staticfiles_storage.url,
        'url': reverse,
    })
    return env

This gives your Jinja2 templates access to Django's static() and url() functions.

4. Add to Installed Apps

Make sure includecontents is in your INSTALLED_APPS:

# settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'includecontents',  # Add this
    'myapp',
]

Directory Structure

Organize your templates with separate directories for Django and Jinja2:

myproject/
├── templates/          # Django templates
│   └── components/
│       └── card.html
├── jinja2/            # Jinja2 templates
│   ├── components/
│   │   └── card.html
│   ├── base.html
│   └── index.html
└── myproject/
    └── jinja2.py      # Environment config

Basic Usage

Template Tag Syntax

Use the {% includecontents %} tag in Jinja2 templates:

jinja2/components/card.html

{# props title, subtitle="" #}
<div class="card">
    <h2>{{ title }}</h2>
    {% if subtitle %}
        <p class="subtitle">{{ subtitle }}</p>
    {% endif %}
    <div class="content">
        {{ contents }}
    </div>
</div>

jinja2/index.html

<!DOCTYPE html>
<html>
<head>
    <title>My Site</title>
</head>
<body>
    {% includecontents "components/card.html" title="Welcome" subtitle="Getting started" %}
        <p>This is your first Jinja2 component!</p>
    {% endincludecontents %}
</body>
</html>

HTML Component Syntax

The extension automatically preprocesses HTML component syntax:

jinja2/index.html

<!DOCTYPE html>
<html>
<head>
    <title>My Site</title>
</head>
<body>
    <include:card title="Welcome" subtitle="Getting started">
        <p>This is your first Jinja2 component with HTML syntax!</p>
    </include:card>
</body>
</html>

Named Content Blocks

Support for named content blocks works identically to Django:

jinja2/components/layout.html

<div class="layout">
    <header>
        {{ contents.header }}
    </header>
    <main>
        {{ contents }}
    </main>
    {% if contents.footer %}
        <footer>
            {{ contents.footer }}
        </footer>
    {% endif %}
</div>

Usage:

<include:layout>
    <content:header>
        <h1>Page Title</h1>
    </content:header>

    <p>Main page content here.</p>

    <content:footer>
        <p>&copy; 2024 My Company</p>
    </content:footer>
</include:layout>

Modern JavaScript Framework Integration

The Jinja2 extension now supports all modern JavaScript framework attributes:

Vue.js Integration

<!-- Vue.js event handlers -->
<include:button @click="handleClick()" @keyup.enter="submitForm()">
    Click me
</include:button>

<!-- Vue.js directives -->
<include:card
    v-on:submit="onSubmit"
    v-model="inputValue"
    v-bind:class="{ 'active': isActive }"
>
    Card content
</include:card>

<!-- Vue.js bind shorthand -->
<include:component :class="dynamicClasses" :disabled="isDisabled">
    Dynamic component
</include:component>

Alpine.js Integration

<!-- Alpine.js event handlers -->
<include:button x-on:click="open = !open" x-data="{ open: false }">
    Toggle
</include:button>

<!-- Alpine.js directives -->
<include:modal x-show="showModal" x-transition>
    Modal content
</include:modal>

Nested Attributes

<!-- Pass attributes to nested elements -->
<include:form-with-button
    inner.class="form-control"
    button.type="submit"
    button.@click="handleSubmit()"
>
    Form content
</include:form-with-button>

Advanced Class Manipulation

<!-- Conditional classes -->
<include:card class:not="disabled ? 'active'" variant="primary">
    Conditional styling
</include:card>

<!-- Class append/prepend -->
<include:button class="& btn-primary" size="large">
    Base classes with extensions
</include:button>

<include:alert class="custom-alert &" type="warning">
    Custom classes with base
</include:alert>

Props and Attributes

The full props system works in Jinja2, including enum validation:

jinja2/components/button.html

{# props variant=primary,secondary,danger size=small,medium,large disabled=false #}
<button
    class="btn btn-{{ variant }} btn-{{ size }}"
    {% if disabled %}disabled{% endif %}
    {{ attrs }}
>
    {{ contents }}
</button>

Usage:

<include:button
    variant="success"
    size="large"
    onclick="alert('Clicked!')"
    data-id="123"
>
    Save Changes
</include:button>

Enum Validation: If you use an invalid enum value, you'll get a helpful error message:

<!-- This will raise a TemplateRuntimeError -->
<include:button variant="invalid">Button</include:button>

Error message:

Invalid value "invalid" for attribute "variant" in component 'components/button.html'.
Allowed values: 'primary', 'secondary', 'danger'. Did you mean "primary"?
Example: <include:button variant="primary">

Icon System

Icons work seamlessly with Jinja2. First configure icons in settings:

# settings.py
STATICFILES_FINDERS = [
    'includecontents.icons.finders.IconSpriteFinder',  # Must be first
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]

INCLUDECONTENTS_ICONS = {
    'icons': [
        'mdi:home',
        'mdi:user',
        'tabler:star',
        'icons/logo.svg',  # Local SVG files
    ]
}

Then use icons in Jinja2 templates:

<!-- HTML syntax (works automatically) -->
<icon:home class="nav-icon" />
<icon:user class="avatar" />

<!-- Template function syntax (also available) -->
{{ icon('home', class='nav-icon') }}
{{ icon('user', class='avatar') }}

<!-- Mixed usage -->
<icon:star class="rating-icon" />
<div>Rating: {{ icon('star', class='small') }}</div>

Icon Function

The icon() function is automatically available in Jinja2 templates when using the IncludeContentsExtension. No manual environment setup required.

Context and Variables

Django Context Processors

Context Processors Discouraged

Django's documentation discourages using context processors with Jinja2. Since Jinja2 can call functions directly (unlike Django templates), it's better to add functions to the global environment instead.

Recommended approach:

myproject/jinja2.py

from django.contrib.staticfiles.storage import staticfiles_storage
from django.urls import reverse
from jinja2 import Environment


def environment(**options):
    env = Environment(**options)
    env.globals.update({
        'static': staticfiles_storage.url,
        'url': reverse,
        # Add functions here instead of using context processors
    })
    return env

CSRF Token Handling

Automatic CSRF Protection

Django's Jinja2 backend automatically provides CSRF token variables in all templates when rendered with a request context. No manual configuration needed!

Django automatically adds these variables to your Jinja2 template context:

  • {{ csrf_token }} - The CSRF token value
  • {{ csrf_input }} - Complete hidden input field (recommended)

In your components:

{# components/form.html #}
<form method="post" action="{{ action }}">
    {{ csrf_input }}  <!-- Automatic CSRF protection -->
    {{ contents }}
    <button type="submit">Submit</button>
</form>

Usage:

<include:form action="/contact/">
    <input name="email" placeholder="Email" required>
    <textarea name="message" placeholder="Message" required></textarea>
</include:form>

Component Context Isolation:

CSRF tokens are automatically preserved in components even with context isolation enabled:

{# components/secure-form.html #}
{# props title #}
<div class="form-wrapper">
    <h2>{{ title }}</h2>
    <form method="post">
        {{ csrf_input }}  <!-- Always available -->
        {{ contents }}
    </form>
</div>

The IncludeContents extension automatically preserves these essential Django variables:

  • request - Current HTTP request
  • csrf_token - CSRF token value
  • csrf_input - CSRF input field
  • user - Current authenticated user
  • perms - User permissions
  • messages - Django messages framework
  • LANGUAGES & LANGUAGE_CODE - Internationalization

Passing Variables to Components

Variables are passed the same way as Django:

<!-- From view context -->
<include:user-card user="{{ user }}" />

<!-- Template variables -->
{% set current_user = request.user %}
<include:user-card user="{{ current_user }}" />

<!-- With filters -->
<include:product-list products="{{ products|list }}" />

Differences from Django Templates

Syntax Differences

Feature Django Jinja2
Comments {# comment #} {# comment #}
Variables {{ variable }} {{ variable }}
Tags {% tag %} {% tag %}
Loops {% for %}...{% endfor %} {% for %}...{% endfor %}
Conditionals {% if %}...{% endif %} {% if %}...{% endif %}
Template loading {% load tag_library %} Extensions in settings

Jinja2 Advantages

<!-- Inline expressions -->
<include:card title="{{ 'Welcome ' + user.name }}" />

<!-- List/dict literals -->
<include:data items="{{ [1, 2, 3] }}" config="{{ {'theme': 'dark'} }}" />

<!-- Method calls -->
<include:user-info name="{{ user.get_full_name() }}" />

<!-- Advanced filters -->
<include:article content="{{ article.body|markdown|safe }}" />

Template Tag Syntax Limitations

Advanced Attributes Require HTML Syntax

The traditional {% includecontents %} tag syntax in Jinja2 has significant limitations due to Jinja2's parser restrictions. Modern web development attributes only work with HTML component syntax.

Limited: Traditional Tag Syntax

<!-- These WILL FAIL with parser errors -->
{% includecontents "button" @click="handler()" %}{% endincludecontents %}
{% includecontents "card" data-id="123" %}{% endincludecontents %}
{% includecontents "form" v-on:submit="onSubmit" %}{% endincludecontents %}
{% includecontents "modal" x-show="open" %}{% endincludecontents %}
{% includecontents "component" inner.class="test" %}{% endincludecontents %}
{% includecontents "button" class:active="true" %}{% endincludecontents %}

<!-- Only basic Python identifiers work -->
{% includecontents "card" title="Hello" variant="primary" %}{% endincludecontents %}

Error: expected token 'name', got '-' or unexpected char '@'

<!-- All modern attributes work perfectly -->
<include:button @click="handler()">Click me</include:button>
<include:card data-id="123">Content</include:card>
<include:form v-on:submit="onSubmit">Form</include:form>
<include:modal x-show="open">Modal</include:modal>
<include:component inner.class="test">Component</include:component>
<include:button class:active="true">Toggle</include:button>

Alternative: Dictionary Syntax

For advanced attributes with traditional syntax, use dictionary unpacking:

{% includecontents "button" {"@click": "handler()", "data-id": "123"} %}
    Click me
{% endincludecontents %}

Recommendation: Use HTML component syntax (<include:>) for all modern web development features. Reserve traditional syntax for simple attributes only.

WrapIf Alternative

Since {% wrapif %} isn't available in Jinja2, use conditional macros:

<!-- Jinja2 macro approach -->
{% macro wrap_if(condition, tag, attrs='', class='') %}
  {% if condition %}
    <{{ tag }}{% if attrs %} {{ attrs }}{% endif %}{% if class %} class="{{ class }}"{% endif %}>
  {% endif %}
  {{ caller() }}
  {% if condition %}</{{ tag }}>{% endif %}
{% endmacro %}

<!-- Usage -->
{% call wrap_if(user.is_authenticated, 'div', class='authenticated') %}
    Welcome back, {{ user.name }}!
{% endcall %}

Testing Your Setup

Create a simple test to verify everything works:

jinja2/test.html

<!DOCTYPE html>
<html>
<head>
    <title>Test Components</title>
</head>
<body>
    <h1>Component Test</h1>

    <!-- Test template tag syntax -->
    {% includecontents "components/card.html" title="Template Tag" %}
        <p>This uses template tag syntax.</p>
    {% endincludecontents %}

    <!-- Test HTML syntax -->
    <include:card title="HTML Syntax">
        <p>This uses HTML component syntax.</p>
    </include:card>

    <!-- Test named contents -->
    <include:card title="Named Contents">
        <p>Main content</p>
        <content:footer>
            <small>Footer content</small>
        </content:footer>
    </include:card>

    <!-- Test JavaScript framework attributes -->
    <include:button @click="alert('Clicked!')" v-on:mouseover="showTooltip()">
        Vue.js Button
    </include:button>

    <!-- Test Alpine.js attributes -->
    <include:toggle x-on:click="open = !open" x-data="{ open: false }">
        Alpine.js Toggle
    </include:toggle>

    <!-- Test nested attributes -->
    <include:form inner.class="form-control" button.type="submit">
        Form with nested attributes
    </include:form>

    <!-- Test advanced class manipulation -->
    <include:alert class="custom-alert &" class:not="dismissed ? 'visible'">
        Alert with class manipulation
    </include:alert>
</body>
</html>

views.py

from django.shortcuts import render

def test_components(request):
    return render(request, 'test.html', {
        'user': request.user,
    })

Troubleshooting

Common Issues

Extension not loading:

# Check that extension is properly configured
TEMPLATES = [{
    'BACKEND': 'django.template.backends.jinja2.Jinja2',
    'OPTIONS': {
        'extensions': [
            'includecontents.jinja2.IncludeContentsExtension',  # Must be exact
        ],
    },
}]

HTML syntax not working:

# Ensure you're using Jinja2 backend, not Django backend
# HTML syntax requires preprocessing which only works with Jinja2

Components not found:

# Check template directory structure
jinja2/
└── components/
    └── card.html  # Should be here

Context variables missing:

# Configure environment properly in jinja2.py
def environment(**options):
    env = Environment(**options)
    env.globals.update({
        'static': staticfiles_storage.url,
        'url': reverse,
        # Add other global functions here
    })
    return env

Next Steps