|
1 | 1 | """ |
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 |
3 | 34 | """ |
| 35 | + |
| 36 | +from __future__ import annotations |
| 37 | + |
| 38 | +import json |
| 39 | +from logging import getLogger |
| 40 | +from typing import TYPE_CHECKING |
| 41 | + |
4 | 42 | from django.template import RequestContext, loader |
| 43 | +from django.utils.safestring import mark_safe |
5 | 44 | from jinja2 import pass_context |
6 | 45 | from jinja2.ext import Extension |
7 | 46 | 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__) |
9 | 61 |
|
10 | 62 |
|
11 | 63 | 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. |
15 | 65 |
|
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. |
17 | 76 | """ |
18 | 77 |
|
19 | | - # |
20 | | - # Therefore, there is no new tag to parse(). |
21 | | - # |
22 | 78 | tags = {} |
23 | 79 |
|
24 | 80 | def __init__(self, environment): |
25 | 81 | 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 |
30 | 85 |
|
31 | 86 | @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, |
34 | 96 | ) -> 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" %}``. |
38 | 100 |
|
39 | 101 | 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. |
42 | 108 |
|
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. |
45 | 156 |
|
46 | 157 | Returns: |
47 | | - Whatever the components returns. |
| 158 | + Rendered HTML string. |
48 | 159 | """ |
| 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 | + |
49 | 168 | django_context = RequestContext( |
50 | | - jinja_context.parent["request"], |
| 169 | + request, |
51 | 170 | autoescape=jinja_context.eval_ctx.autoescape, |
52 | 171 | ) |
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 | + ) |
57 | 207 | return loader.render_to_string( |
58 | | - COMPONENT_TEMPLATE, template_context, jinja_context.parent["request"] |
| 208 | + PYSCRIPT_SETUP_TEMPLATE, |
| 209 | + template_context, |
59 | 210 | ) |
0 commit comments