Skip to main content

A lightweight library for flexible Django component building, following Django's principles.

Project description

Django Component Kit

Introducing Django Component Kit, a small and non-intrusive Python library for Django that provides all the necessary tools to build flexible components in a Django-like manner.

By using standard Python decorators and Django custom template tags, you can easily create highly flexible components with IDE-friendly tooling.

Example

Template Tag

# templatetags/mycomponents.py
from django import template
from django.template.loader import get_template
from django_component_kit import component_block_tag

register = template.Library()


@register.tag
@register.tag('endcard')
@component_block_tag(get_template("mycomponents/card.html"))
def card(title: str) -> dict:
    return dict(title=title)

Template

<!-- templates/mycomponents/card.html -->
{% load django_component_kit %}

<div class="card shadow-lg p-3 bg-body rounded">
    <div class="card-header">
        <h> class="card-title">{{ title }}}</h>
    </div>
    <div class="card-body">
        {% render_slot slots.children %}
    </div>
    <div class="card-footer text-muted">
        <div class="links float-end">
            {% render_slot slots.footer %}
        </div>
    </div>
</div>

Usage

<!-- templates/myapp/index.html -->
{% load django_component_kit %}

<div>
  {% card title="Card title" %}
    <p class="card-text">Foo</p>
    {% slot footer %}
      <a href="#" class="card-link">Card link</a>
    {% endslot %}
  {% endcard %}
</div>

Result

<!-- https://myapp.com/index.html -->
<div>
    <div class="card mb-3 shadow-lg p-3 bg-body rounded">
        <div class="card-header">
            <h class="card-title">Card title</h>
        </div>
        <div class="card-body">
            <p class="card-text">Foo</p>
        </div>
        <div class="card-footer text-muted">
            <div class="links float-end">
                <a href="#" class="card-link">Card link</a>
            </div>
        </div>
    </div>
</div>

But Why?

While exploring different frontend frameworks and concepts, I came across HTMX and Alpine.js, which I fell in love with for creating nice frontends in Django. However, the Django Templating System lacked proper support for components, making it difficult to use these frameworks effectively. Existing third-party libraries for components in Django were either outdated or felt more like frameworks than libraries, requiring a steep learning curve.

To address this, I decided to create Django Component Kit, which is based on the unobtrusive and flexible django-web-components library developed by Mihail Cristian Dumitru Xzya. I aimed to integrate it into my component library and enhance its feature set.

During the integration process, I realized that significant modifications were needed to align with my vision. I ended up creating Django Component Kit, which shares some codebase with django-web-components but adopts a different approach to component development.

Getting Started

Installation

pip install django-component-kit

Adding to Your Project

There are two ways to use the library:

Adding as a Django App

This method allows for deeper integration but requires some standard Django configuration.

Add the app to settings.py

# myproject/settings.py
INSTALLED_APPS = [
    ...,
    'django_component_kit',
]

Optional: Adding as a Built-in Component

This method allows you to add the component as a built-in in your project's settings.py without forcing others to install it.

# myproject/settings.py
TEMPLATES = [
    {
        "OPTIONS": {
            "builtins": ["django_component_kit.templatetags.components"],
        },
    },
]

Adding to Your Own Component Library

If you want to use Django Component Kit primarily to build your own components, you can add it to your component library without requiring others to install it.

Add to your component library templatetags/mycomponents.py

# templatetags/mycomponents.py
from django import template
from django_component_kit.tags import do_merge_attrs, do_render_slot, do_slot

register = template.Library()
register.tag("merge_attrs", do_merge_attrs)
register.tag("render_slot", do_render_slot)
register.tag("slot", do_slot)

That's it! Now you can start creating your own components.

Usage

Inline Components

Inline components are simple elements without any children. They allow you to add attributes and are commonly used for form fields or other end elements.

1. Register a function with @component_inline_tag()

Use the @component_inline_tag() decorator to register a function as a custom tag.

# templatetags/mycomponents.py
from django import template
from django.forms import Field
from django.template.loader import get_template
from django_component_kit import component_inline_tag

register = template.Library()


@register.tag
@component_inline_tag(get_template("mycomponents/text_input.html"))
def text_input(field: Field) -> dict:
    """Component for rendering a text input."""
    return dict(field=field)

2. Create a template for the component

Create a template that defines how the component should be rendered. Inline components receive the context provided by the function and the parameters bundled into the attributes variable.

<!-- templates/mycomponents/text_input.html -->
{% load django_component_kit %}

<div class="mb-3">
    <label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
    <input type="text" id="{{ field.id_for_label }}" name="{{ field.name }}" class="form-control">
    <div class="form-text text-muted">{{ field.help_text }}</div>
</div>

3. Use the component in a template

Use the component in your template by calling the registered tag and passing the required parameters.

<!-- templates/myapp/index.html -->
<form>
    {% text_input field=form.field %}
    <button type="submit">Submit</button>
</form>

Result

The component will be rendered with the provided attributes:

<!-- https://myapp.com/index.html -->
<form>
    <div class="mb-3">
        <label for="text_1" class="form-label">Foo</label>
        <input type="text" id="text_1" name="Bar" class="form-control">
        <div class="form-text text-muted">Batz</div>
    </div>
    <button type="submit">Submit</button>
</form>

Details

  • The @component_inline_tag() decorator should be placed between the @register.tag decorator and the function definition.
  • The decorator requires a template for rendering the component.
  • All parameters passed to the function will be extracted from the context for further processing.
  • The function must return a dictionary that will be used as the context for rendering the inline component.
  • Parameters passed to the tag will be bundled into the attributes variable in the template context.
  • The register.tag() function provides IDE support for jumping from template tags to functions and viewing component usage.
  • Providing a parameter without a key will treat it as a boolean value. See attributes for more details.

Since: 0.5.0

  • Optionally a context parameter can be added to a tag. This will contain the full context

Block Components

Block components behave similarly to inline components but allow for the inclusion of child elements, making them suitable for more complex components.

1. Register a function with @component_block_tag()

Use the @component_block_tag() decorator to register a function as a custom tag.

# templatetags/mycomponents.py
from django import template
from django.template.loader import get_template
from django_component_kit import component_block_tag

register = template.Library()


@register.tag
@register.tag('endcard')
@component_block_tag(get_template("mycomponents/card.html"))
def card() -> dict:
    """Component for rendering a card."""
    return dict()

2. Create a template for the component

Create a template that defines how the component should be rendered. Block components allow for the use of slots, which are defined using the slot tag.

<!-- templates/mycomponents/card.html -->
{% load django_component_kit %}

<div {% merge_attrs attributes class="card shadow-lg p-3 bg-body rounded" %}>
    {% if slots.header %}
    <div class="card-header">
        {% render_slot slots.header %}
    </div>
    {% endif %}
    <div class="card-body">
        {% render_slot slots.children %}
    </div>
    {% if slots.footer %}
    <div class="card-footer text-muted">
        <div class="links float-end">
            {% render_slot slots.footer %}
        </div>
    </div>
    {% endif %}
</div>

3. Use the component in a template

Use the component in your template by calling the registered tag and defining the slots using the slot tag.

<!-- templates/myapp/index.html -->
{% load django_component_kit %}

<div>
    {% card class='mb-3' %}
    {% slot header %}
    <h> class="card-title">Card title</h>
    {% endslot %}

    <p class="card-text">Bar</p>

    {% slot footer %}
    <a href="#" class="card-link">Card link</a>
    {% endslot %}
    {% endcard %}
</div>

Result

The component will be rendered with the provided attributes and slots:

<!-- https://myapp.com/index.html -->
<div>
    <div class="card mb-3 shadow-lg p-3 bg-body rounded">
        <div class="card-header">
            <h class="card-title">Card title</h>
        </div>
        <div class="card-body">
            <p class="card-text">Foo</p>
        </div>
        <div class="card-footer text-muted">
            <div class="links float-end">
                <a href="#" class="card-link">Card link</a>
            </div>
        </div>
    </div>
</div>

Details

  • All details of inline components apply to block components as well.
  • The tag must end with an endXXX tag, where XXX is the name of the component. Everything enclosed within the tags will be added to the component.
  • The endXXX tag registration is optional but provides autocompletion and linking between the component and its usage.
  • The slots variable is a dictionary that contains all provided slots and a base slot called children, which collects all child elements not inside a slot.
  • Missing variables within the template will not be rendered.
  • Use the render_slot tag to render the slots.
  • Use the {% slot %} tag to define a slot. It must end with an {% endslot %} tag. Everything enclosed within the tags will be added to the slot.
  • Use merge_attrs to merge attributes together.

Since: 0.5.0

  • Optionally a context parameter can be added to a tag. This will contain the full context

Passing Data to Components

You can pass data to components using keyword arguments, which accept hardcoded values or variables:

{% with error_message="Something bad happened!" %}
{% alert type="error" message=error_message %}
{% endwith %}

All attributes will be added to the attributes dictionary, which will be available in the template context:

{
  "attributes": {
    "type": "error",
    "message": "Something bad happened!"
  }
}

You can then access the attributes from the component's template:

<div class="alert alert-{{ attributes.type }}">
    {{ attributes.message }}
</div>

Rendering All Attributes

You can also render all attributes directly using {{ attributes }}. For example, if you have the following component:

{% alert id="alerts" class="font-bold" %} ... {% endalert %}

You can render all attributes using:

<div {{ attributes }}>
    <!-- Component content -->
</div>

This will render the following HTML:

<div id="alerts" class="font-bold">
    <!-- Component content -->
</div>

Attributes with Special Characters

You can pass attributes with special characters ([@:_-.]) as well as attributes with no value:

{% button @click="handleClick" data-id="123" required %} ... {% endbutton %}

This will result in the following dictionary available in the context:

{
    "attributes": {
        "@click": "handleClick",
        "data-id": "123",
        "required": True,
    }
}

And will be rendered by {{ attributes }} as @click="handleClick" data-id="123" required.

Default/Merged Attributes

You can specify default values for attributes or merge additional values into some of the component's attributes using the merge_attrs tag:

<div {% merge_attrs attributes class="alert" role="alert" %}>
    <!-- Component content -->
</div>

If we assume this component is used like this:

{% alert class="mb-4" %} ... {% endalert %}

The final rendered HTML of the component will be:

<div class="alert mb-4" role="alert">
    <!-- Component content -->
</div>

Non-Class Attribute Merging

When merging attributes that are not class attributes, the values provided to the merge_attrs tag will be considered the "default" values of the attribute. These attributes will not be merged with injected attribute values but will be overwritten. For example, a button component's implementation may look like this:

<button {% merge_attrs attributes type="button" %}>
    {% render_slot slots.inner_block %}
</button>

To render the button component with a custom type, you can specify it when consuming the component. If no type is specified, the default button type will be used:

{% button type="submit" %} Submit {% endbutton %}

The rendered HTML of the button component in this example would be:

<button type="submit">
    Submit
</button>

Appendable Attributes

You can treat other attributes as "appendable" by using the += operator:

<div {% merge_attrs attributes data-value+="some-value" %}>
    <!-- Component content -->
</div>

If we assume this component is used like this:

{% alert data-value="foo" %} ... {% endalert %}

The rendered HTML will be:

<div data-value="foo some-value">
    <!-- Component content -->
</div>

Manipulating the Attributes

By default, all attributes are added to an attributes dictionary inside the context. However, in some cases, you may want to separate certain attributes from the attributes dictionary. For example, you may want to render an alert component with a custom id or class attribute, while still being able to use additional attributes internally.

To achieve this, you can manipulate the context in your component's function to provide a better API for using the component:

# templatetags/mycomponents.py
from django import template
from django.template.loader import get_template
from django_component_kit import component_block_tag

register = template.Library()


@register.tag
@register.tag("endalert")
@component_block_tag(get_template("mycomponents/alert.html"))
def alert(dismissible: bool) -> dict:
    """Component for rendering an alert."""
    return {"dismissible": dismissible}

The component's template can then be modified to handle the new structure:

<!-- templates/mycomponents/alert.html -->
{% load django_component_kit %}

<div {% merge_attrs attributes %}>
    {% render_slot slots.children %}
    {% if dismissible %}
    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
    {% endif %}
</div>

This allows you to render the component like this:

{% alert id="my-alert" class="my-class" dismissible %}
This is an alert.
{% endalert %}

The rendered HTML will be:

<div id="my-alert" class="my-class">
    This is an alert.
    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>

Slots

Slots allow you to pass additional content to your components. The slots context variable is passed to your components and consists of a dictionary with the slot name as the key and the slot content as the value. You can render the slots inside your components using the render_slot tag.

The Default Slot

By default, any content not enclosed within a named slot will be added to the default slot, which is called children. You can render this slot using the render_slot tag inside your component.

Named Slots

Sometimes a component may need to render multiple named slots in different locations. You can define named slots using the slot tag and render them using the render_slot tag.

Scoped Slots

Slots also have access to the component's context, allowing you to pass variables to the slot content. You can use the :let attribute to bind the value passed to render_slot to a variable within the slot content.

Nested Components

You can nest components to create more complex elements. This allows you to build reusable and modular components.

Partials

Since: 0.2.0

Partials allow you to render only a part of a template. This can be useful, when you have a complex component which consists of multiple subcomponents. Like a card with actions, which are used to manipulate the content of the card. Usually you would use one template for the card and then one for each kind of action component. This is fine, until you have javascript, HTMX or AlpineJS logic inside of these components, which must reference each other. That's indeed possible, but the DX is bad, as you don't know what functions or attributes you can use and what they do. In this case partials are coming in for the rescue. Just put all subcomponents into a partial inside the main component. With this, even IDE support is present for seeing where which function is being called.

Hint: The partials are cached, so you can use a lot of them without performance impact.

Since: 0.3.0 When a template is rendered as a partial, the 'is_partial' attribute is set. Partials can be rendered directly via the PartialResponse class.

Usage

You can directly write the partial inside the main component at any point in the template. Only if the inline argument is set to True, the partial will be rendered inside the main component. If not, it will be hidden. The example uses AlpineJS for demonstration purposes:

<!-- templates/mycomponents/card.html -->
{% load django_component_kit %}

<div class="card" x-data="{ color: text-success }">
  <div class="card-header">
    {% render_slot slots.header %}
    {% partial "toggle_color_action" %}
      <button
              type="button"
              class="btn-close"
              aria-label="Toggle"
              @click="color=color==='text-success' ? 'text-danger' : 'text-success'"
      >
        Toggle color
      </button>
    {% endpartial %}
  </div>
  <div class="card-body" :class="color">
    {% render_slot slots.children %}
  </div>
</div>
# templatetags/mycomponents.py
from django import template
from django.template.loader import get_template
from django_component_kit import component_block_tag, component_inline_tag

register = template.Library()

@register.tag
@register.tag('endcard')
@component_block_tag(get_template("mycomponents/card.html"))
def card() -> dict:
    return dict()


@register.tag
@component_inline_tag(get_template("mycomponents/card.html"), partial="toggle_color_action")
def toggle_color_action() -> dict:
    return dict()
<!-- templates/index.html -->
{% load django_component_kit %}

{% card %}
  {% slot header %}
    {% toggle_color_action %}
  {% endslot %}
  <p>This is a text</p>
{% endcard %}

Result

<div class="card" x-data="{ color: text-success }">
  <div class="card-header">
    <button
            type="button"
            class="btn-close"
            aria-label="Toggle"
            @click="color=color==='text-success' ? 'text-danger' : 'text-success'"
    >
      Toggle color
    </button>
  </div>
  <div class="card-body" :class="color">
    <p>This is a text</p>
  </div>
</div>

Assets

Since: 0.3.0

When registering a component, you can assign JS oder CSS assets to it. You can do this using the js or css arguments in the component_inline_tag or component_block_tag. They will be ordered in the order they are registered, CSS in front of JS. Duplicates are removed. You can then use the assets via the assets tag.

Hint: If you want to modify the assets, you can import the assets object and modify it before usage.

Usage

# templatetags/mycomponents.py
from django import template
from django.template.loader import get_template
from django_component_kit import component_inline_tag

register = template.Library()

@register.tag
@component_inline_tag(get_template("mycomponents/card.html"), js=["myapp/myjs.js"])
def card() -> dict:
    return dict()
<!-- templates/index.html -->
{% assets %}

Result

<script src="static/myapp/myjs.js" defer></script>

Contribution

If you have any questions, suggestions, or feedback, feel free to open an issue.

Development and Testing

To set up the development environment and run the tests, follow these steps:

  1. Install the dependencies using poetry install.
  2. Activate the environment using poetry shell.
  3. Run the tests using poe test.
  4. Run the linting using poe lint.

What's Next?

The current goal is to stabilize Django Component Kit and ensure it works as expected in different applications and use cases. Future plans include adding more features to the library, but there is no specific roadmap at the moment. One potential addition could be support for assets, although the need for this is diminished with the popularity of Tailwind, HTMX, and Alpine.js.

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

django_component_kit-0.5.1.tar.gz (20.4 kB view details)

Uploaded Source

Built Distribution

django_component_kit-0.5.1-py3-none-any.whl (19.3 kB view details)

Uploaded Python 3

File details

Details for the file django_component_kit-0.5.1.tar.gz.

File metadata

  • Download URL: django_component_kit-0.5.1.tar.gz
  • Upload date:
  • Size: 20.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.8

File hashes

Hashes for django_component_kit-0.5.1.tar.gz
Algorithm Hash digest
SHA256 3500d860cd1a82a607a7e239c9678c968bd51a49b4ba150c77e2b4c15d04572a
MD5 ab9365a24940c6772dd626f4cc5550fa
BLAKE2b-256 a75927a84a8e8323305ed4346d3312ae04487c00596c224f931da15ca7869e66

See more details on using hashes here.

File details

Details for the file django_component_kit-0.5.1-py3-none-any.whl.

File metadata

File hashes

Hashes for django_component_kit-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 12ed33b0688779a2087c2aa2f4139104b8cedf956708aa9d67da79705dd620ba
MD5 2facc81cec16fc7a8c576441e1f7d7e0
BLAKE2b-256 f202d02d65a383ad817681fa39ab9495d0d887a4d5d0801e83c06918c60cbe13

See more details on using hashes here.

Supported by

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