Skip to main content

A Jinja2 extension for writing component-like HTML that gets converted into Jinja macro calls

Project description

jinja2-component-macros

A package to bring component-oriented HTML to Jinja templates, powered by Jinja macros.

PyPI version Python versions License

Overview

This package provides a Jinja extension that preprocesses Jinja templates, replacing html 'component' tags with corresponding macro calls. Tags eligible for replacement are selected based on the "import" statements at the start of the template.

I like this approach because HTML tags are easier to read than 'macro' and 'endmacro' statements, and work well with IDEs even without Jinja-specific support.

With traditional Jinja macros

{% from "components/cards.html" import Card, CardHeader %}
{% from "components/buttons.html" import Button %}

{% call Card(class="bg-blue") %}
  {% call CardHeader() %}Welcome to my card{% endcall %}
  {{ Button(text="Click me", url=ctx_url, variant="primary") }}
{% endcall %}

Using jinja2-component-macros

{% from "components/cards.html" import Card, CardHeader %}
{% from "components/buttons.html" import Button %}

<Card class="bg-blue">
  <CardHeader>Welcome to my card</CardHeader>
  <Button text="Click me" url='ctx_url' variant="primary" />
</Card>

Installation

pip install jinja2-component-macros

Usage

Basic Setup

from jinja2 import Environment
from jinja2_component_macros import ComponentsExtension

env = Environment(extensions=[ComponentsExtension])

Creating Components

Create your components as regular Jinja macros, with whatever parameters you'd like to use. A very simple example of a self-closing component:

{# components.html #}
{% macro Header(text, class="") %}<h1 class="{{ class }}" {{ kwargs|jcm_attr }}>{{ text }}</h1>{% endmacro %}

Note the above does not use 'caller()', so it will only work as a self-closing component, like <Header text="First Header"/>. A second version that also allows <Header>First Header</Header> might look like:

{# components.html #}
{% macro Header(text="", class="") %}<h1 class="{{ class }}" {{ kwargs|jcm_attr }}>{{ caller() if caller else text }}</h1>{% endmacro %}

These examples also use a filter called jcm_attr, which is a specialized version of xmlattr that can expand the kwargs parameters, but with some extra support for special characters that aren't valid in macro parameter names, for example AlpineJS "@click" and "x-on:load". Meaning you can do things like: <Header @click="open-page=true">First Header</Header> and the '@click' attribute will be passed through to the component properly. The way this is handled "under the hood" is that any special character parameters are passed as a dict to the '' parameter. When jcm_attr expands the kwargs, it looks for a "" key and expands the value of that as additional kwargs parameters.

Using Components

jinja2-component-macros only replaces HTML tags that are listed in import statements at the top of a template.

Import your components first, then use them in HTML:

{% from "components.html" import Button, Card %}

{# Self-closing components #}
<Button text="Save" variant="primary" />

{# Container components #}
<Card title="User Profile" class="bg-light">
  <p>User information goes here</p>
  <OtherComponent>This does not get replaced by a macro call because OtherComponent wasn't imported</OtherComponent>
  <Button text="Edit Profile" variant="secondary" />
</Card>

How It Works

Under the hood, the extension preprocesses the templates, makes a mapping of macro names to convert based on import statements, then scans and converts any matching HTML tags into the appropriate Jinja macro calls:

  • Self-closing tags (<Component />) become {{ ComponentName() }}
  • Container tags (<Component>...</Component>) become {% call ComponentName() %}...{% endcall %}
  • Attributes are passed as macro parameters
  • Attributes with invalid characters (containing -, :, @, .) are collected as a dictionary and passed as a special _ parameter

Attribute Handling

Attributes are parsed in different ways depending on the type of quotes used - this is distinct from how HTML works, so it should be paid special attention.

  • Double quotes ("value") are passed as string literal values to macro parameters
  • Single quotes or unquoted ('value' or value) are treated as Jinja Expressions, and are passed unquoted to macro parameters

As an example:

{# Double quoted attributes... #}
<Button text="Click me" class="btn-primary" is_active="false" />
{# ...become... #}
{{ Button(text="Click me", class="btn-primary", is_active="false") }}

{# Single quoted or unquoted attributes... #}
{% set button_text="some text" %}
<Button text='button_text' is_active=false is_valid='false' count=42 />
{# ...become... #}
{{ Button(text=button_text, is_active=false, is_valid=false, count=42) }}

Especially note the different behaviour of "false" versus false or 'false'. The former is passed as a string, the latter as the actual boolean value for false.

Invalid Parameter Names

Attributes with invalid Python parameter names are collected in a special _ parameter:

<Button x-on:load=123 @click="handleClick" />
{# ...become... #}
{{ Button(_={"x-on:load": 123, "@click": "handleClick"}) }}

Note the same rules for quoting apply, so the unquoted 123 is treated as an expression (an integer), not as a string.

Access these in your macro using the jcm_attr helper filter:

{% macro Button(text) %}
<button{{ kwargs|jcm_attr(autospace=true) }}>{{ text }}</button>
{% endmacro %}

Helper Functions

The extension provides two helpful global functions:

jcm_attr(autospace=False)

Converts a dictionary to HTML attribute string:

{% macro Button() %}
<button {{ kwargs|jcm_attr }}>Click me</button>
{% endmacro %}

{# Usage: <Button data-id="123" class="btn" /> #}
{# Output: <button data-id="123" class="btn">Click me</button> #}

As with xmlattr, you can use autospace=True to make it add a space only when it returns text - if not, then it returns an empty string.

{% macro Button() %}
<button{{ kwargs|jcm_attr(autospace=True) }}>Click me</button>
{% endmacro %}

{# Usage: <Button /> #}
{# Output: <button>Click me</button> #}

classx(*args)

Conditionally joins CSS classes (similar to JavaScript's clsx):

{% set button_classes = classx(
  "btn",
  {"btn-primary": variant == "primary"},
  {"btn-large": size == "lg"},
  extra_classes
) %}
<button class="{{ button_classes }}">{{ text }}</button>

Known Issues

There are plenty of things to improve on this, but a couple of significant potential gotchas:

  • There is currently no handling to ignore comment blocks e.g. {# #} - HTML tag substitution will be applied even inside a comment.

Performance

An area to improve is benchmarking and performance, to look at performance comparisons between this, native macro usage, and other methods of components. However, because this package does almost all its work in pre-processing, it is expected that it should perform almost the same as using macros directly.

Development

This project uses uv for dependency management and just for task running.

# Setup development environment
just bootstrap

# Install dependencies
just install

# Run tests using tox
just test

# Run (roughly) the same checks that are run when checking a PR
just pr-checks

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jinja2_component_macros-2025.1.tar.gz (11.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jinja2_component_macros-2025.1-py3-none-any.whl (9.1 kB view details)

Uploaded Python 3

File details

Details for the file jinja2_component_macros-2025.1.tar.gz.

File metadata

  • Download URL: jinja2_component_macros-2025.1.tar.gz
  • Upload date:
  • Size: 11.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for jinja2_component_macros-2025.1.tar.gz
Algorithm Hash digest
SHA256 74741e670c197e1323be54172ff57c701b119b6e1ef6086b81f8be4ffc3fd6c7
MD5 15fcbfd8e4fc33d725dba6758aa93d59
BLAKE2b-256 c6cf50cd618cd01f6470aa5161a919f8b9916a7f0cb5ec34cdeb5acdb77e5137

See more details on using hashes here.

Provenance

The following attestation bundles were made for jinja2_component_macros-2025.1.tar.gz:

Publisher: publish.yml on LucidDan/jinja2-component-macros

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file jinja2_component_macros-2025.1-py3-none-any.whl.

File metadata

File hashes

Hashes for jinja2_component_macros-2025.1-py3-none-any.whl
Algorithm Hash digest
SHA256 df0032cf49d4912c23cff32d46e702c9c55ea7ca1877d644b0f32b53c37d4f84
MD5 27d0bd3f6ec848a54e84119283136c4e
BLAKE2b-256 e81ee8b2e2045e8ee36a163c23a706e63632dcf47b482a27b829d171a7b26910

See more details on using hashes here.

Provenance

The following attestation bundles were made for jinja2_component_macros-2025.1-py3-none-any.whl:

Publisher: publish.yml on LucidDan/jinja2-component-macros

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page