Skip to content

Instantly share code, notes, and snippets.

@mariocesar
Last active February 22, 2025 20:20
Show Gist options
  • Save mariocesar/0ad5bfbee43690e5123e9db2307f6db4 to your computer and use it in GitHub Desktop.
Save mariocesar/0ad5bfbee43690e5123e9db2307f6db4 to your computer and use it in GitHub Desktop.
React-style components for Django templates, but only on Django Template. Simple, familiar, and good enough for most projects

Why?

I built this because I wanted React's component patterns in Django templates without switching to a JavaScript frontend. When you use Jinja2 you have macros and that work similar. This will bring the Jinja2 macro experience.

No complex setup, no build process, just practical template components when you need them.

How It Works

Two template tags, that's it:

{% load component_tags %}

{% define "button" %}
  <button class="{{ props.class }}" type="{{ props.type|default:'button' }}">
    {{ props.children }}
  </button>
{% enddefine %}

{% render "button" with class="primary" %}
  Submit
{% endrender %}

Example

{% define "card" %}
  <div class="card {{ props.class }}">
    {% if props.title %}
      <h2>{{ props.title }}</h2>
    {% endif %}
    {{ props.children }}
  </div>
{% enddefine %}

{% render "card" with title="Welcome" class="shadow" %}
  <p>Content goes here</p>
  {% render "button" with class="primary" %}
    Click me
  {% endrender %}
{% endrender %}
from django import template
from django.conf import settings
from django.template import Node, NodeList, TemplateSyntaxError, VariableDoesNotExist
from django.template.base import FilterExpression, Parser, Token, token_kwargs
register = template.Library()
class DefineNode(Node):
"""Store the component template (nodelist) in context.render_context['_components']."""
def __init__(self, name_expr: FilterExpression, nodelist):
self.name_expr = name_expr
self.nodelist = nodelist
def render(self, context):
# Resolve the name at runtime
try:
component_name = self.name_expr.resolve(context)
except VariableDoesNotExist as exc:
msg = f"The component name variable '{self.name_expr.token}' does not exist in the context."
raise TemplateSyntaxError(msg) from exc
if not isinstance(component_name, str) or not component_name:
msg = "The component name must resolve to a non-empty string."
raise TemplateSyntaxError(msg)
with context.push():
if "_components" not in context.render_context:
context.render_context["_components"] = {}
context.render_context["_components"][component_name] = self.nodelist
if settings.DEBUG:
return f"<!-- Component <{component_name} /> defined -->"
return ""
def prepare_tag(name: str, parser: Parser, token: Token) -> tuple[NodeList, list[str]]:
"""Prepare a tag for parsing by extracting the nodelist and tokens."""
nodelist = parser.parse((f"end{name}",))
parser.delete_first_token()
tokens = token.split_contents()
return nodelist, tokens
@register.tag(name="define")
def do_define(parser: Parser, token: Token):
"""
Define tag to store a component template in context.render_context['_components'].
{% define "literal-name" %}
...component body...
{% enddefine %}
"""
nodelist, tokens = prepare_tag("define", parser, token)
if len(tokens) < 2: # noqa: PLR2004
msg = "'define' tag requires a component name, either a quoted string or a variable."
raise TemplateSyntaxError(msg)
if getattr(parser, "define_active", False):
msg = "Nested '{% define %}' blocks are not allowed."
raise TemplateSyntaxError(msg)
old_define_flag = getattr(parser, "define_active", False)
parser.define_active = True
try:
name_expr = parser.compile_filter(tokens[1])
finally:
parser.define_active = old_define_flag
return DefineNode(name_expr, nodelist)
class RenderNode(Node):
"""Render a previously defined component by name."""
def __init__(self, name_expr: FilterExpression, props: dict, nodelist: NodeList):
self.name_expr = name_expr
self.props = props
self.nodelist = nodelist
def render(self, context):
try:
component_name = self.name_expr.resolve(context)
except VariableDoesNotExist as exc:
msg = f"The component name variable '{self.name_expr.token}' does not exist in the context."
raise TemplateSyntaxError(msg) from exc
if not isinstance(component_name, str) or not component_name:
msg = "The component name must resolve to a non-empty string."
raise TemplateSyntaxError(msg)
components = context.render_context.get("_components", {})
component_nodelist = components.get(component_name)
if not component_nodelist:
return ""
resolved_props = {}
for key, filter_expr in self.props.items():
try:
resolved_props[key] = filter_expr.resolve(context)
except VariableDoesNotExist:
resolved_props[key] = ""
# The body inside {% render ... %} is "children"
children_content = self.nodelist.render(context)
with context.push():
context["props"] = resolved_props
context["props"]["children"] = children_content
out = component_nodelist.render(context)
if settings.DEBUG:
props = " ".join(f"{k}={v!r}" for k, v in resolved_props.items() if k != "children")
return "\n".join(
[
f"\n<!-- Render <{component_name} {props!s}> -->\n",
out.strip(),
f"\n<!-- </{component_name}> -->\n",
]
)
return out
@register.tag(name="render")
def do_render(parser: Parser, token: Token):
"""
Render tag to render a previously defined component by name.
{% render "literal-name" [with key="val" another=var] %}
... (this becomes props.children) ...
{% endrender %}
"""
nodelist, tokens = prepare_tag("render", parser, token)
if len(tokens) < 2: # noqa: PLR2004
msg = "'render' tag requires a component name, either a quoted string or a variable."
raise TemplateSyntaxError(msg)
# bits[0] == 'render', not used directly
tokens.pop(0)
# bits[0] == component name, parse the component name as a FilterExpression
name_expr = parser.compile_filter(tokens.pop(0))
props = {}
if tokens and tokens[0] == "with":
tokens.pop(0) # remove 'with'
props = token_kwargs(tokens, parser, support_legacy=False)
return RenderNode(name_expr, props, nodelist)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment