Skip to content

Commit 94fd2d4

Browse files
author
User
committed
Add Jinja2 template support for ReactPy components
- Created `ReactPyExtension` Jinja2 extension with `component`, `pyscript_component`, and `pyscript_setup` globals that delegate to the existing Django template tag implementations. - Refactored `reactpy.py` to export template name constants (COMPONENT_TEMPLATE, PYSCRIPT_COMPONENT_TEMPLATE, PYSCRIPT_SETUP_TEMPLATE) so they can be reused by the Jinja2 extension. - Added JINJA_COMPONENT_REGEX to utils.py's RootComponentFinder so that {{ component(...) }} syntax in Jinja2 templates is auto-detected. - Added test infrastructure: settings_jinja.py, jinja_env.py, jinja_views.py, jinja_urls.py, Jinja2 template files, and test_jinja.py. - Made test_app/__init__.py gracefully handle missing `bun` binary by skipping the JS rebuild when artifacts already exist. - Added jinja2 to pyproject.toml hatch-test extra-dependencies. - Updated CHANGELOG.md. (cherry picked from commit 3923cee)
1 parent a01ffa9 commit 94fd2d4

18 files changed

Lines changed: 1284 additions & 589 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Don't forget to remove deprecated code on each major release!
2222
### Added
2323

2424
- Automatically serve ReactPy wheel from Django's static directory when using PyScript.
25+
- Jinja2 template support via `reactpy_django.templatetags.jinja.ReactPyExtension`.
2526

2627
### Changed
2728

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ extra-dependencies = [
9797
"django-bootstrap5",
9898
"decorator",
9999
"uvicorn[standard]",
100+
"jinja2",
100101
]
101102
matrix-name-format = "{variable}-{value}"
102103

Lines changed: 180 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,210 @@
11
"""
2-
Jinja support.
2+
Jinja2 template support for ReactPy-Django.
3+
4+
Provides Jinja2 global functions that mirror the functionality of Django's
5+
``{% component %}``, ``{% pyscript_component %}``, and ``{% pyscript_setup %}``
6+
template tags. These are registered as environment globals so they can be
7+
called directly from Jinja2 templates via ``{{ component(...) }}`` syntax.
8+
9+
To enable, add the extension to your Jinja2 environment configuration:
10+
11+
.. code-block:: python
12+
13+
TEMPLATES = [
14+
{
15+
"BACKEND": "django.template.backends.jinja2.Jinja2",
16+
"DIRS": [...],
17+
"OPTIONS": {
18+
"environment": "myproject.jinja_env.environment",
19+
},
20+
},
21+
]
22+
23+
Then in ``myproject/jinja_env.py``:
24+
25+
.. code-block:: python
26+
27+
from jinja2 import Environment
28+
from reactpy_django.templatetags.jinja import ReactPyExtension
29+
30+
def environment(**options):
31+
env = Environment(**options)
32+
env.add_extension(ReactPyExtension)
33+
return env
334
"""
35+
36+
from __future__ import annotations
37+
38+
import json
39+
from logging import getLogger
40+
from typing import TYPE_CHECKING
41+
442
from django.template import RequestContext, loader
43+
from django.utils.safestring import mark_safe
544
from jinja2 import pass_context
645
from jinja2.ext import Extension
746
from jinja2.runtime import Context
8-
from reactpy_django.templatetags.reactpy import COMPONENT_TEMPLATE, component
47+
48+
from reactpy_django.templatetags.reactpy import (
49+
COMPONENT_TEMPLATE,
50+
PYSCRIPT_COMPONENT_TEMPLATE,
51+
PYSCRIPT_SETUP_TEMPLATE,
52+
component as django_component_tag,
53+
pyscript_component as django_pyscript_component_tag,
54+
pyscript_setup as django_pyscript_setup_tag,
55+
)
56+
57+
if TYPE_CHECKING:
58+
from reactpy.types import Component, VdomDict
59+
60+
_logger = getLogger(__name__)
961

1062

1163
class ReactPyExtension(Extension):
12-
"""
13-
Jinja has more expressive power than core Django's templates, and can
14-
directly handle expansions such as:
64+
"""A Jinja2 extension that adds ReactPy component rendering functions.
1565
16-
{{ component(*args, **kwargs) }}
66+
This extension registers the following globals into the Jinja2 environment:
67+
68+
* ``component`` - Renders a server-side ReactPy component.
69+
* ``pyscript_component`` - Renders a client-side PyScript component.
70+
* ``pyscript_setup`` - Renders PyScript setup configuration.
71+
72+
Unlike Django's template tags, which require ``{% load reactpy %}`` and use
73+
``{% component %}`` syntax, Jinja2 uses ``{{ component(...) }}`` function calls.
74+
This is because Jinja2 has more expressive power and can directly handle
75+
function expansions.
1776
"""
1877

19-
#
20-
# Therefore, there is no new tag to parse().
21-
#
2278
tags = {}
2379

2480
def __init__(self, environment):
2581
super().__init__(environment)
26-
#
27-
# All we need is to add global "component" to the environment.
28-
#
29-
environment.globals["component"] = self.template_tag
82+
environment.globals["component"] = self._component
83+
environment.globals["pyscript_component"] = self._pyscript_component
84+
environment.globals["pyscript_setup"] = self._pyscript_setup
3085

3186
@pass_context
32-
def template_tag(
33-
self, jinja_context: Context, dotted_path: str, *args, **kwargs
87+
def _component(
88+
self,
89+
jinja_context: Context,
90+
dotted_path: str,
91+
*args,
92+
host: str | None = None,
93+
prerender: str = "",
94+
offline: str = "",
95+
**kwargs,
3496
) -> str:
35-
"""
36-
This method is used to embed an existing ReactPy component into your
37-
Jinja2 template.
97+
"""Render a server-side ReactPy component.
98+
99+
This is the Jinja2 equivalent of ``{% component "path.to.Component" %}``.
38100
39101
Args:
40-
dotted_path: String of the fully qualified name of a component.
41-
*args: The positional arguments to provide to the component.
102+
dotted_path: The dotted path to the component to render.
103+
*args: Positional arguments to pass to the component.
104+
host: The host to use for ReactPy connections.
105+
prerender: If ``"true"`` the component will be pre-rendered server-side.
106+
offline: Dotted path to an offline fallback component.
107+
**kwargs: Keyword arguments to pass to the component.
42108
43-
Keyword Args:
44-
**kwargs: The keyword arguments to provide to the component.
109+
Returns:
110+
Rendered HTML string.
111+
"""
112+
request = jinja_context.parent.get("request")
113+
if request is None:
114+
_logger.exception(
115+
"Cannot render a ReactPy component in a Jinja2 template without a "
116+
"request object. Ensure the 'django.template.context_processors.request' "
117+
"context processor is enabled for your Jinja2 backend."
118+
)
119+
return ""
120+
121+
django_context = RequestContext(
122+
request,
123+
autoescape=jinja_context.eval_ctx.autoescape,
124+
)
125+
template_context = django_component_tag(
126+
django_context,
127+
dotted_path,
128+
*args,
129+
host=host,
130+
prerender=prerender,
131+
offline=offline,
132+
**kwargs,
133+
)
134+
return loader.render_to_string(
135+
COMPONENT_TEMPLATE,
136+
template_context,
137+
request,
138+
)
139+
140+
@pass_context
141+
def _pyscript_component(
142+
self,
143+
jinja_context: Context,
144+
*file_paths: str,
145+
initial: str | VdomDict | Component = "",
146+
root: str = "root",
147+
) -> str:
148+
"""Render a client-side PyScript component.
149+
150+
This is the Jinja2 equivalent of ``{% pyscript_component "path/to/file.py" %}``.
151+
152+
Args:
153+
file_paths: File paths to client-side component Python files.
154+
initial: Initial HTML displayed before the PyScript component loads.
155+
root: The name of the root component function.
45156
46157
Returns:
47-
Whatever the components returns.
158+
Rendered HTML string.
48159
"""
160+
request = jinja_context.parent.get("request")
161+
if request is None:
162+
_logger.exception(
163+
"Cannot render a PyScript component in a Jinja2 template without a "
164+
"request object."
165+
)
166+
return ""
167+
49168
django_context = RequestContext(
50-
jinja_context.parent["request"],
169+
request,
51170
autoescape=jinja_context.eval_ctx.autoescape,
52171
)
53-
template_context = component(django_context, dotted_path, *args, **kwargs)
54-
#
55-
# TODO: can this be usefully cached?
56-
#
172+
template_context = django_pyscript_component_tag(
173+
django_context,
174+
*file_paths,
175+
initial=initial,
176+
root=root,
177+
)
178+
return loader.render_to_string(
179+
PYSCRIPT_COMPONENT_TEMPLATE,
180+
template_context,
181+
request,
182+
)
183+
184+
def _pyscript_setup(
185+
self,
186+
*extra_py: str,
187+
extra_js: str | dict = "",
188+
config: str | dict = "",
189+
) -> str:
190+
"""Render PyScript setup configuration.
191+
192+
This is the Jinja2 equivalent of ``{% pyscript_setup %}``.
193+
194+
Args:
195+
extra_py: Additional Python dependencies.
196+
extra_js: Additional JavaScript modules.
197+
config: PyScript configuration overrides.
198+
199+
Returns:
200+
Rendered HTML string.
201+
"""
202+
template_context = django_pyscript_setup_tag(
203+
*extra_py,
204+
extra_js=extra_js,
205+
config=config,
206+
)
57207
return loader.render_to_string(
58-
COMPONENT_TEMPLATE, template_context, jinja_context.parent["request"]
208+
PYSCRIPT_SETUP_TEMPLATE,
209+
template_context,
59210
)

src/reactpy_django/templatetags/reactpy.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
_logger.exception("Could not resolve the 'web_modules' URL path!")
4747

4848
COMPONENT_TEMPLATE = "reactpy/component.html"
49+
PYSCRIPT_COMPONENT_TEMPLATE = "reactpy/pyscript_component.html"
50+
PYSCRIPT_SETUP_TEMPLATE = "reactpy/pyscript_setup.html"
4951

5052

5153
@register.inclusion_tag(COMPONENT_TEMPLATE, takes_context=True)
@@ -192,7 +194,7 @@ def component(
192194
}
193195

194196

195-
@register.inclusion_tag("reactpy/pyscript_component.html", takes_context=True)
197+
@register.inclusion_tag(PYSCRIPT_COMPONENT_TEMPLATE, takes_context=True)
196198
def pyscript_component(
197199
context: template.RequestContext,
198200
*file_paths: str,
@@ -226,7 +228,7 @@ def pyscript_component(
226228
}
227229

228230

229-
@register.inclusion_tag("reactpy/pyscript_setup.html")
231+
@register.inclusion_tag(PYSCRIPT_SETUP_TEMPLATE)
230232
def pyscript_setup(
231233
*extra_py: str,
232234
extra_js: str | dict = "",

0 commit comments

Comments
 (0)