jinja2-component-macros


Namejinja2-component-macros JSON
Version 2025.1 PyPI version JSON
download
home_pageNone
SummaryA Jinja2 extension for writing component-like HTML that gets converted into Jinja macro calls
upload_time2025-08-24 13:29:45
maintainerNone
docs_urlNone
authorDan Sloan
requires_python>=3.10
licenseNone
keywords jinja2 templates components html macros web
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # jinja2-component-macros

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

![PyPI version](https://img.shields.io/pypi/v/jinja2-component-macros)
![Python versions](https://img.shields.io/pyversions/jinja2-component-macros)
![License](https://img.shields.io/github/license/luciddan/jinja2-component-macros)

## 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

```jinja
{% 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

```jinja
{% 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

```bash
pip install jinja2-component-macros
```

## Usage

### Basic Setup

```python
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:
```jinja
{# 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:

```jinja
{# 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:

```jinja
{% 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:

```jinja
{# 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:

```jinja
<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:

```jinja
{% 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:

```jinja
{% 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.

```jinja
{% 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`):

```jinja
{% 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](https://docs.astral.sh/uv/) for dependency management and [just](https://github.com/casey/just)
for task running.

```bash
# 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](LICENSE) file for details.
            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "jinja2-component-macros",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": "jinja2, templates, components, html, macros, web",
    "author": "Dan Sloan",
    "author_email": "Dan Sloan <dan@dansloan.dev>",
    "download_url": "https://files.pythonhosted.org/packages/c6/cf/50cd618cd01f6470aa5161a919f8b9916a7f0cb5ec34cdeb5acdb77e5137/jinja2_component_macros-2025.1.tar.gz",
    "platform": null,
    "description": "# jinja2-component-macros\n\nA package to bring component-oriented HTML to Jinja templates, powered by Jinja macros.\n\n![PyPI version](https://img.shields.io/pypi/v/jinja2-component-macros)\n![Python versions](https://img.shields.io/pyversions/jinja2-component-macros)\n![License](https://img.shields.io/github/license/luciddan/jinja2-component-macros)\n\n## Overview\n\nThis package provides a Jinja extension that preprocesses Jinja templates, replacing html 'component' tags with corresponding macro calls.\nTags eligible for replacement are selected based on the \"import\" statements at the start of the template.\n\nI 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.\n\n### With traditional Jinja macros\n\n```jinja\n{% from \"components/cards.html\" import Card, CardHeader %}\n{% from \"components/buttons.html\" import Button %}\n\n{% call Card(class=\"bg-blue\") %}\n  {% call CardHeader() %}Welcome to my card{% endcall %}\n  {{ Button(text=\"Click me\", url=ctx_url, variant=\"primary\") }}\n{% endcall %}\n```\n\n### Using jinja2-component-macros\n\n```jinja\n{% from \"components/cards.html\" import Card, CardHeader %}\n{% from \"components/buttons.html\" import Button %}\n\n<Card class=\"bg-blue\">\n  <CardHeader>Welcome to my card</CardHeader>\n  <Button text=\"Click me\" url='ctx_url' variant=\"primary\" />\n</Card>\n```\n\n## Installation\n\n```bash\npip install jinja2-component-macros\n```\n\n## Usage\n\n### Basic Setup\n\n```python\nfrom jinja2 import Environment\nfrom jinja2_component_macros import ComponentsExtension\n\nenv = Environment(extensions=[ComponentsExtension])\n```\n\n### Creating Components\n\nCreate your components as regular Jinja macros, with whatever parameters you'd like to use.\nA very simple example of a self-closing component:\n```jinja\n{# components.html #}\n{% macro Header(text, class=\"\") %}<h1 class=\"{{ class }}\" {{ kwargs|jcm_attr }}>{{ text }}</h1>{% endmacro %}\n```\n\nNote the above does not use 'caller()', so it will only work as a self-closing component, like `<Header text=\"First Header\"/>`.\nA second version that also allows `<Header>First Header</Header>` might look like:\n\n```jinja\n{# components.html #}\n{% macro Header(text=\"\", class=\"\") %}<h1 class=\"{{ class }}\" {{ kwargs|jcm_attr }}>{{ caller() if caller else text }}</h1>{% endmacro %}\n```\n\nThese examples also use a filter called `jcm_attr`, which is a specialized version of `xmlattr` that can expand the kwargs parameters,\nbut with some extra support for special characters that aren't valid in macro parameter names, for example AlpineJS \"@click\" and \"x-on:load\".\nMeaning you can do things like: `<Header @click=\"open-page=true\">First Header</Header>` and the '@click' attribute will be passed through to \nthe component properly.\nThe 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\nthe kwargs, it looks for a \"_\" key and expands the value of that as additional kwargs parameters.\n\n### Using Components\n\n`jinja2-component-macros` only replaces HTML tags that are listed in `import` statements at the top of a template.\n\nImport your components first, then use them in HTML:\n\n```jinja\n{% from \"components.html\" import Button, Card %}\n\n{# Self-closing components #}\n<Button text=\"Save\" variant=\"primary\" />\n\n{# Container components #}\n<Card title=\"User Profile\" class=\"bg-light\">\n  <p>User information goes here</p>\n  <OtherComponent>This does not get replaced by a macro call because OtherComponent wasn't imported</OtherComponent>\n  <Button text=\"Edit Profile\" variant=\"secondary\" />\n</Card>\n```\n\n## How It Works\n\nUnder the hood, the extension preprocesses the templates, makes a mapping of macro names to convert based on import statements,\nthen scans and converts any matching HTML tags into the appropriate Jinja macro calls:\n\n- **Self-closing tags** (`<Component />`) become `{{ ComponentName() }}`\n- **Container tags** (`<Component>...</Component>`) become `{% call ComponentName() %}...{% endcall %}`\n- **Attributes** are passed as macro parameters\n- **Attributes with invalid characters** (containing `-`, `:`, `@`, `.`) are collected as a dictionary and passed as a special `_` parameter\n\n## Attribute Handling\n\nAttributes 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.\n\n- **Double quotes** (`\"value\"`) are passed as string literal values to macro parameters\n- **Single quotes or unquoted** (`'value'` or `value`) are treated as Jinja Expressions, and are passed unquoted to macro parameters\n\nAs an example:\n\n```jinja\n{# Double quoted attributes... #}\n<Button text=\"Click me\" class=\"btn-primary\" is_active=\"false\" />\n{# ...become... #}\n{{ Button(text=\"Click me\", class=\"btn-primary\", is_active=\"false\") }}\n\n{# Single quoted or unquoted attributes... #}\n{% set button_text=\"some text\" %}\n<Button text='button_text' is_active=false is_valid='false' count=42 />\n{# ...become... #}\n{{ Button(text=button_text, is_active=false, is_valid=false, count=42) }}\n```\n\nEspecially note the different behaviour of \"false\" versus false or 'false'. \nThe former is passed as a string, the latter as the actual boolean value for false. \n\n### Invalid Parameter Names\n\nAttributes with invalid Python parameter names are collected in a special `_` parameter:\n\n```jinja\n<Button x-on:load=123 @click=\"handleClick\" />\n{# ...become... #}\n{{ Button(_={\"x-on:load\": 123, \"@click\": \"handleClick\"}) }}\n```\n\nNote the same rules for quoting apply, so the unquoted 123 is treated as an expression (an integer), not as a string.\n\nAccess these in your macro using the `jcm_attr` helper filter:\n\n```jinja\n{% macro Button(text) %}\n<button{{ kwargs|jcm_attr(autospace=true) }}>{{ text }}</button>\n{% endmacro %}\n```\n\n\n## Helper Functions\n\nThe extension provides two helpful global functions:\n\n### `jcm_attr(autospace=False)`\n\nConverts a dictionary to HTML attribute string:\n\n```jinja\n{% macro Button() %}\n<button {{ kwargs|jcm_attr }}>Click me</button>\n{% endmacro %}\n\n{# Usage: <Button data-id=\"123\" class=\"btn\" /> #}\n{# Output: <button data-id=\"123\" class=\"btn\">Click me</button> #}\n```\n\nAs 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.\n\n```jinja\n{% macro Button() %}\n<button{{ kwargs|jcm_attr(autospace=True) }}>Click me</button>\n{% endmacro %}\n\n{# Usage: <Button /> #}\n{# Output: <button>Click me</button> #}\n```\n\n\n### `classx(*args)`\n\nConditionally joins CSS classes (similar to JavaScript's `clsx`):\n\n```jinja\n{% set button_classes = classx(\n  \"btn\",\n  {\"btn-primary\": variant == \"primary\"},\n  {\"btn-large\": size == \"lg\"},\n  extra_classes\n) %}\n<button class=\"{{ button_classes }}\">{{ text }}</button>\n```\n\n## Known Issues\n\nThere are plenty of things to improve on this, but a couple of significant potential gotchas:\n- There is currently no handling to ignore comment blocks e.g. `{# #}` - HTML tag substitution will be applied even inside a comment.\n\n## Performance\n\nAn area to improve is benchmarking and performance, to look at performance comparisons between this, native macro usage, and other methods of components.\nHowever, 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.\n\n## Development\n\nThis project uses [uv](https://docs.astral.sh/uv/) for dependency management and [just](https://github.com/casey/just)\nfor task running.\n\n```bash\n# Setup development environment\njust bootstrap\n\n# Install dependencies\njust install\n\n# Run tests using tox\njust test\n\n# Run (roughly) the same checks that are run when checking a PR\njust pr-checks\n```\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.",
    "bugtrack_url": null,
    "license": null,
    "summary": "A Jinja2 extension for writing component-like HTML that gets converted into Jinja macro calls",
    "version": "2025.1",
    "project_urls": {
        "Changelog": "https://github.com/luciddan/jinja2-component-macros/blob/main/CHANGELOG.md",
        "Homepage": "https://github.com/luciddan/jinja2-component-macros",
        "Issues": "https://github.com/luciddan/jinja2-component-macros/issues",
        "Repository": "https://github.com/luciddan/jinja2-component-macros"
    },
    "split_keywords": [
        "jinja2",
        " templates",
        " components",
        " html",
        " macros",
        " web"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "e81ee8b2e2045e8ee36a163c23a706e63632dcf47b482a27b829d171a7b26910",
                "md5": "27d0bd3f6ec848a54e84119283136c4e",
                "sha256": "df0032cf49d4912c23cff32d46e702c9c55ea7ca1877d644b0f32b53c37d4f84"
            },
            "downloads": -1,
            "filename": "jinja2_component_macros-2025.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "27d0bd3f6ec848a54e84119283136c4e",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 9051,
            "upload_time": "2025-08-24T13:29:43",
            "upload_time_iso_8601": "2025-08-24T13:29:43.833965Z",
            "url": "https://files.pythonhosted.org/packages/e8/1e/e8b2e2045e8ee36a163c23a706e63632dcf47b482a27b829d171a7b26910/jinja2_component_macros-2025.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "c6cf50cd618cd01f6470aa5161a919f8b9916a7f0cb5ec34cdeb5acdb77e5137",
                "md5": "15fcbfd8e4fc33d725dba6758aa93d59",
                "sha256": "74741e670c197e1323be54172ff57c701b119b6e1ef6086b81f8be4ffc3fd6c7"
            },
            "downloads": -1,
            "filename": "jinja2_component_macros-2025.1.tar.gz",
            "has_sig": false,
            "md5_digest": "15fcbfd8e4fc33d725dba6758aa93d59",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 11917,
            "upload_time": "2025-08-24T13:29:45",
            "upload_time_iso_8601": "2025-08-24T13:29:45.349077Z",
            "url": "https://files.pythonhosted.org/packages/c6/cf/50cd618cd01f6470aa5161a919f8b9916a7f0cb5ec34cdeb5acdb77e5137/jinja2_component_macros-2025.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-08-24 13:29:45",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "luciddan",
    "github_project": "jinja2-component-macros",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "tox": true,
    "lcname": "jinja2-component-macros"
}
        
Elapsed time: 1.41951s