Build reusable template components in Django
Project description
django-web-components
A simple way to create reusable template components in Django.
Example
You have to first register your component
from django_web_components import component
@component.register("card")
class Card(component.Component):
template_name = "components/card.html"
The component's template:
# components/card.html
{% load components %}
<div class="card">
<div class="card-header">
{% render_slot slots.header %}
</div>
<div class="card-body">
<h5 class="card-title">
{% render_slot slots.title %}
</h5>
{% render_slot slots.inner_block %}
</div>
</div>
You can now render this component with:
{% load components %}
{% card %}
{% slot header %} Featured {% endslot %}
{% slot title %} Card title {% endslot %}
<p>Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
{% endcard %}
Which will result in the following HTML being rendered:
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">
Card title
</h5>
<p>Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
Installation
pip install django-web-components
Then add django_web_components
to your INSTALLED_APPS
.
INSTALLED_APPS = [
...,
"django_web_components",
]
Optional
To avoid having to use {% load components %}
in each template, you may add the tags to the builtins
list inside your
settings.
TEMPLATES = [
{
...,
"OPTIONS": {
"context_processors": [
...
],
"builtins": [
"django_web_components.templatetags.components",
],
},
},
]
Python / Django compatibility
The library supports Python 3.8+ and Django 3.2+.
Python version | Django version |
---|---|
3.11 |
4.1 |
3.10 |
4.1 , 4.0 , 3.2 |
3.9 |
4.1 , 4.0 , 3.2 |
3.8 |
4.1 , 4.0 , 3.2 |
Components
There are two approaches to writing components: class based components and function based components.
Class based components
from django_web_components import component
@component.register("alert")
class Alert(component.Component):
# You may also override the get_template_name() method instead
template_name = "components/alert.html"
# Extra context data will be passed to the template context
def get_context_data(self, **kwargs) -> dict:
return {
"dismissible": False,
}
The component will be rendered by calling the render(context)
method, which by default will load the template file and render it.
For tiny components, it may feel cumbersome to manage both the component class and the component's template. For this reason, you may define the template directly from the render
method:
from django_web_components import component
from django_web_components.template import CachedTemplate
@component.register("alert")
class Alert(component.Component):
def render(self, context) -> str:
return CachedTemplate(
"""
<div class="alert alert-primary" role="alert">
{% render_slot slots.inner_block %}
</div>
""",
name="alert",
).render(context)
Function based components
A component may also be defined as a single function that accepts a context
and returns a string:
from django_web_components import component
from django_web_components.template import CachedTemplate
@component.register
def alert(context):
return CachedTemplate(
"""
<div class="alert alert-primary" role="alert">
{% render_slot slots.inner_block %}
</div>
""",
name="alert",
).render(context)
The examples in this guide will mostly use function based components, since it's easier to exemplify as the component code and template are in the same place, but you are free to choose whichever method you prefer.
Template files vs template strings
The library uses the regular Django templates, which allows you to either load templates from files, or create Template objects directly using template strings. Both methods are supported, and both have advantages and disadvantages:
- Template files
- You get formatting support and syntax highlighting from your editor
- You get caching by default
- Harder to manage / reason about since your code is split from the template
- Template strings
- Easier to manage / reason about since your component's code and template are in the same place
- You lose formatting support and syntax highlighting since the template is just a string
- You lose caching
Regarding caching, the library provides a CachedTemplate
, which will cache and reuse the Template
object as long as you provide a name
for it, which will be used as the cache key:
from django_web_components import component
from django_web_components.template import CachedTemplate
@component.register
def alert(context):
return CachedTemplate(
"""
<div class="alert alert-primary" role="alert">
{% render_slot slots.inner_block %}
</div>
""",
name="alert",
).render(context)
So in reality, the caching should not be an issue when using template strings, since CachedTemplate
is just as fast as using the cached loader with template files.
Regarding formatting support and syntax highlighting, there is no good solution for template strings. PyCharm supports language injection which allows you to add a # language=html
comment before the template string and get syntax highlighting, however, it only highlights HTML and not the Django tags, and you are still missing support for formatting. Maybe the editors will add better support for this in the future, but for the moment you will be missing syntax highlighting and formatting if you go this route. There is an open conversation about this on the django-components
repo, credits to EmilStenstrom for moving the conversation forward with the VSCode team.
In the end, it's a tradeoff. Use the method that makes the most sense for you.
Registering your components
Just like signals, the components can live anywhere, but you need to make sure Django picks them up on startup. The easiest way to do this is to define your components in a components.py
submodule of the application they relate to, and then connect them inside the ready()
method of your application configuration class.
from django.apps import AppConfig
from django_web_components import component
class MyAppConfig(AppConfig):
...
def ready(self):
# Implicitly register components decorated with @component.register
from . import components # noqa
# OR explicitly register a component
component.register("card", components.Card)
You may also unregister
an existing component, or get a component from the registry:
from django_web_components import component
# Unregister a component
component.registry.unregister("card")
# Get a component
component.registry.get("card")
# Remove all components
component.registry.clear()
# Get all components as a dict of name: component
component.registry.all()
Rendering components
Each registered component will have two tags available for use in your templates:
- A block tag, e.g.
{% card %} ... {% endcard %}
- An inline tag, e.g.
{% #user_profile %}
. This can be useful for components that don't necessarily require a body
Component tag formatter
By default, components will be registered using the following tags:
- Block start tag:
{% <component_name> %}
- Block end tag:
{% end<component_name> %}
- Inline tag:
{% #<component_name> %}
This behavior may be changed by providing a custom tag formatter in your settings. For example, to change the block tags to {% #card %} ... {% /card %}
, and the inline tag to {% card %}
(similar to slippers), you can use the following formatter:
class ComponentTagFormatter:
def format_block_start_tag(self, name):
return f"#{name}"
def format_block_end_tag(self, name):
return f"/{name}"
def format_inline_tag(self, name):
return name
# inside your settings
WEB_COMPONENTS = {
"DEFAULT_COMPONENT_TAG_FORMATTER": "path.to.your.ComponentTagFormatter",
}
Passing data to components
You may pass data to components using keyword arguments, which accept either hardcoded values or variables:
{% with error_message="Something bad happened!" %}
{% #alert type="error" message=error_message %}
{% endwith %}
All attributes will be added in an attributes
dict which will be available in the template context:
{
"attributes": {
"type": "error",
"message": "Something bad happened!"
}
}
You can then access it from your component's template:
<div class="alert alert-{{ attributes.type }}">
{{ attributes.message }}
</div>
Rendering all attributes
You may also render all attributes directly using {{ attributes }}
. For example, if you have the following component
{% alert id="alerts" class="font-bold" %} ... {% endalert %}
You may render all attributes using
<div {{ attributes }}>
<!-- Component content -->
</div>
Which will result in the following HTML being rendered:
<div id="alerts" class="font-bold">
<!-- Component content -->
</div>
Attributes with special characters
You can also pass attributes with special characters ([@:_-.]
), as well as attributes with no value:
{% button @click="handleClick" data-id="123" required %} ... {% endbutton %}
Which will result in the follow dict 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
Sometimes you may need to specify default values for attributes, or merge additional values into some of the component's attributes. The library provides a merge_attrs
tag that helps with this:
<div {% merge_attrs attributes class="alert" role="alert" %}>
<!-- Component content -->
</div>
If we assume this component is utilized like so:
{% alert class="mb-4" %} ... {% endalert %}
The final rendered HTML of the component will appear like the following:
<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. However, unlike the class
attribute, these attributes will not be merged with injected attribute values. Instead, they will be overwritten. For example, a button
component's implementation may look like the following:
<button {% merge_attrs attributes type="button" %}>
{% render_slot slots.inner_block %}
</button>
To render the button component with a custom type
, it may be specified when consuming the component. If no type is specified, the 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 may also 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 utilized like so:
{% 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
dict inside the context. However, this may not always be what we want. For example, imagine we want to have an alert
component that can be dismissed, while at the same time being able to pass extra attributes to the root element, like an id
or class
. Ideally we would want to be able to render a component like this:
{% alert id="alerts" dismissible %} Something went wrong! {% endalert %}
A naive way to implement this component would be something like the following:
<div {{ attributes }}>
{% render_slot slots.inner_block %}
{% if attributes.dismissible %}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
{% endif %}
</div>
However, this would result in the dismissible
attribute being included in the root element, which is not what we want:
<div id="alerts" dismissible>
Something went wrong!
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
Ideally we would want the dismissible
attribute to be separated from the attributes
since we only want to use it in logic, but not necessarily render it to the component.
To achieve this, you can manipulate the context from your component in order to provide a better API for using the components. There are several ways to do this, choose the method that makes the most sense to you, for example:
- You can override
get_context_data
and remove thedismissible
attribute fromattributes
and return it in the context instead
from django_web_components import component
@component.register("alert")
class Alert(component.Component):
template_name = "components/alert.html"
def get_context_data(self, **kwargs):
dismissible = self.attributes.pop("dismissible", False)
return {
"dismissible": dismissible,
}
- You can override the
render
method and manipulate the context there
from django_web_components import component
@component.register("alert")
class Alert(component.Component):
template_name = "components/alert.html"
def render(self, context):
context["dismissible"] = context["attributes"].pop("dismissible", False)
return super().render(context)
Both of the above solutions will work, and you can do the same for function based components. The component's template can then look like this:
<div {{ attributes }}>
{% render_slot slots.inner_block %}
{% if dismissible %}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
{% endif %}
</div>
Which should result in the correct HTML being rendered:
<div id="alerts">
Something went wrong!
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
Slots
You will often need to pass additional content to your components via "slots". A slots
context variable is passed to your components, which consists of a dict with the slot name as the key and the slot as the value. You may then render the slots inside your components using the render_slot
tag.
The default slot
To explore this concept, let's imagine we want to pass some content to an alert
component:
{% alert %}
<strong>Whoops!</strong> Something went wrong!
{% endalert %}
By default, that content will be made available to your component in the default slot which is called inner_block
. You can then render this slot using the render_slot
tag inside your component:
{% load components %}
<div class="alert alert-danger">
{% render_slot slots.inner_block %}
</div>
Which should result in the following HTML being rendered:
<div class="alert alert-danger">
<strong>Whoops!</strong> Something went wrong!
</div>
You may also rename the default slot by specifying it in your settings:
# inside your settings
WEB_COMPONENTS = {
"DEFAULT_SLOT_NAME": "inner_block",
}
Named slots
Sometimes a component may need to render multiple different slots in different locations within the component. Let's modify our alert component to allow for the injection of a "title" slot:
{% load components %}
<div class="alert alert-danger">
<span class="alert-title">
{% render_slot slots.title %}
</span>
{% render_slot slots.inner_block %}
</div>
You may define the content of the named slot using the slot
tag. Any content not within an explicit slot
tag will be added to the default inner_block
slot:
{% load components %}
{% alert %}
{% slot title %} Server error {% endslot %}
<strong>Whoops!</strong> Something went wrong!
{% endalert %}
The rendered HTML in this example would be:
<div class="alert alert-danger">
<span class="alert-title">
Server error
</span>
<strong>Whoops!</strong> Something went wrong!
</div>
Duplicate named slots
You may define the same named slot multiple times:
{% unordered_list %}
{% slot item %} First item {% endslot %}
{% slot item %} Second item {% endslot %}
{% slot item %} Third item {% endslot %}
{% endunordered_list %}
You can then iterate over the slot inside your component:
<ul>
{% for item in slots.item %}
<li>{% render_slot item %}</li>
{% endfor %}
</ul>
Which will result in the following HTML:
<ul>
<li>First item</li>
<li>Second item</li>
<li>Third item</li>
</ul>
Scoped slots
The slot content will also have access to the component's context. To explore this concept, imagine a list component that accepts an entries
attribute representing a list of things, which it will then iterate over and render the inner_block
slot for each entry.
from django_web_components import component
from django_web_components.template import CachedTemplate
@component.register
def unordered_list(context):
context["entries"] = context["attributes"].pop("entries", [])
return CachedTemplate(
"""
<ul>
{% for entry in entries %}
<li>
{% render_slot slots.inner_block %}
</li>
{% endfor %}
</ul>
""",
name="unordered_list",
).render(context)
We can then render the component as follows:
{% unordered_list entries=entries %}
I like {{ entry }}!
{% endunordered_list %}
In this example, the entry
variable comes from the component's context. If we assume that entries = ["apples", "bananas", "cherries"]
, the resulting HTML will be:
<ul>
<li>I like apples!</li>
<li>I like bananas!</li>
<li>I like cherries!</li>
</ul>
You may also explicitly pass a second argument to render_slot
:
<ul>
{% for entry in entries %}
<li>
<!-- We are passing the `entry` as the second argument to render_slot -->
{% render_slot slots.inner_block entry %}
</li>
{% endfor %}
</ul>
When invoking the component, you can use the special attribute :let
to take the value that was passed to render_slot
and bind it to a variable:
{% unordered_list :let="fruit" entries=entries %}
I like {{ fruit }}!
{% endunordered_list %}
This would render the same HTML as above.
Slot attributes
Similar to the components, you may assign additional attributes to slots. Below is a table component illustrating multiple named slots with attributes:
from django_web_components import component
from django_web_components.template import CachedTemplate
@component.register
def table(context):
context["rows"] = context["attributes"].pop("rows", [])
return CachedTemplate(
"""
<table>
<tr>
{% for col in slots.column %}
<th>{{ col.attributes.label }}</th>
{% endfor %}
</tr>
{% for row in rows %}
<tr>
{% for col in slots.column %}
<td>
{% render_slot col row %}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
""",
name="table",
).render(context)
You can invoke the component like so:
{% table rows=rows %}
{% slot column :let="user" label="Name" %}
{{ user.name }}
{% endslot %}
{% slot column :let="user" label="Age" %}
{{ user.age }}
{% endslot %}
{% endtable %}
If we assume that rows = [{ "name": "Jane", "age": "34" }, { "name": "Bob", "age": "51" }]
, the following HTML will be rendered:
<table>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr>
<td>Jane</td>
<td>34</td>
</tr>
<tr>
<td>Bob</td>
<td>51</td>
</tr>
</table>
Nested components
You may also nest components to achieve more complicated elements. Here is an example of how you might implement an Accordion component using Bootstrap:
from django_web_components import component
from django_web_components.template import CachedTemplate
import uuid
@component.register
def accordion(context):
context["accordion_id"] = context["attributes"].pop("id", str(uuid.uuid4()))
context["always_open"] = context["attributes"].pop("always_open", False)
return CachedTemplate(
"""
<div class="accordion" id="{{ accordion_id }}">
{% render_slot slots.inner_block %}
</div>
""",
name="accordion",
).render(context)
@component.register
def accordion_item(context):
context["id"] = context["attributes"].pop("id", str(uuid.uuid4()))
context["open"] = context["attributes"].pop("open", False)
return CachedTemplate(
"""
<div class="accordion-item" id="{{ id }}">
<h2 class="accordion-header" id="{{ id }}-header">
<button
class="accordion-button {% if not open %}collapsed{% endif %}"
type="button"
data-bs-toggle="collapse"
data-bs-target="#{{ id }}-collapse"
aria-expanded="{% if open %}true{% else %}false{% endif %}"
aria-controls="{{ id }}-collapse"
>
{% render_slot slots.title %}
</button>
</h2>
<div
id="{{ id }}-collapse"
class="accordion-collapse collapse {% if open %}show{% endif %}"
aria-labelledby="{{ id }}-header"
{% if accordion_id and not always_open %}
data-bs-parent="#{{ accordion_id }}"
{% endif %}}
>
<div class="accordion-body">
{% render_slot slots.body %}
</div>
</div>
</div>
""",
name="accordion_item",
).render(context)
You can then use them as follows:
{% accordion %}
{% accordion_item open %}
{% slot title %} Accordion Item #1 {% endslot %}
{% slot body %}
<strong>This is the first item's accordion body.</strong> It is shown by default.
{% endslot %}
{% endaccordion_item %}
{% accordion_item %}
{% slot title %} Accordion Item #2 {% endslot %}
{% slot body %}
<strong>This is the second item's accordion body.</strong> It is hidden by default.
{% endslot %}
{% endaccordion_item %}
{% accordion_item %}
{% slot title %} Accordion Item #3 {% endslot %}
{% slot body %}
<strong>This is the third item's accordion body.</strong> It is hidden by default.
{% endslot %}
{% endaccordion_item %}
{% endaccordion %}
Setup for development and running the tests
The project uses poetry
to manage the dependencies. Check out the documentation on how to install poetry here: https://python-poetry.org/docs/#installation
Install the dependencies
poetry install
Activate the environment
poetry shell
Now you can run the tests
python runtests.py
Motivation / Inspiration / Resources
The project came to be after seeing how other languages / frameworks deal with components, and wanting to bring some of those ideas back to Django.
- django-components - The existing
django-components
library is already great and supports most of the features that this project has, but I thought the syntax could be improved a bit to feel less verbose, and add a few extra things that seemed necessary, like support for function based components and template strings, and working with attributes - Phoenix Components - I really liked the simplicity of Phoenix and how they deal with components, and this project is heavily inspired by it. In fact, some of the examples above are straight-up copied from there (like the table example).
- Laravel Blade Components - The initial implementation was actually very different and was relying on HTML parsing to turn the HTML into template Nodes, and was heavily inspired by Laravel. This had the benefit of having a nicer syntax (e.g. rendering the components looked a lot like normal HTML
<x-alert type="error">Server Error</x-alert>
), but the solution was a lot more complicated and I came to the conclusion that using a similar approach todjango-components
made a lot more sense in Django - Vue Components
- slippers
- django-widget-tweaks
- How EEx Turns Your Template Into HTML
Component libraries
Many other languages / frameworks are using the same concepts for building components (slots, attributes), so a lot of the knowledge is transferable, and there is already a great deal of existing component libraries out there (e.g. using Bootstrap, Tailwind, Material design, etc.). I highly recommend looking at some of them to get inspired on how to build / structure your components. Here are some examples:
- https://bootstrap-vue.org/docs/components/alert
- https://coreui.io/bootstrap-vue/components/alert.html
- https://laravel-bootstrap-components.com/components/alerts
- https://flowbite.com/docs/components/alerts/
- https://www.creative-tim.com/vuematerial/components/button
- https://phoenix-ui.fly.dev/components/alert
- https://storybook.phenixgroupe.com/components/message
- https://surface-ui.org/samplecomponents/Button
- https://react-bootstrap.github.io/components/alerts/
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file django_web_components-0.2.0.tar.gz
.
File metadata
- Download URL: django_web_components-0.2.0.tar.gz
- Upload date:
- Size: 23.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.11.2
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 71e083bb5b7b790f834c4b2a1b7d31be8f588ac21a43ba1e14df901b70fa2147 |
|
MD5 | cd370b17fed5d6a3b308a4339192fdd2 |
|
BLAKE2b-256 | ff1ac29156353604f4a7f1db1cde8c1cda745eaa86646bb0e957a9ae6b410c70 |
File details
Details for the file django_web_components-0.2.0-py3-none-any.whl
.
File metadata
- Download URL: django_web_components-0.2.0-py3-none-any.whl
- Upload date:
- Size: 19.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.11.2
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 1a2e06767f4b10872f512954287a32e85e96109e918031de78fc8bde796b28d9 |
|
MD5 | 039f7555734e321ffa4854a73dabe881 |
|
BLAKE2b-256 | b45d3e426c3f76597f301a2ac14528c3d279af09267022ef0529222614dd4c54 |