From 7e97489633c1a3bdf7776cb31e023ccfc5e5fa97 Mon Sep 17 00:00:00 2001 From: Shaheed Haque Date: Mon, 9 Oct 2023 20:48:01 +0100 Subject: [PATCH 1/4] WIP Jinja support. --- src/reactpy_django/templatetags/jinja.py | 67 ++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/reactpy_django/templatetags/jinja.py diff --git a/src/reactpy_django/templatetags/jinja.py b/src/reactpy_django/templatetags/jinja.py new file mode 100644 index 00000000..a851a4d9 --- /dev/null +++ b/src/reactpy_django/templatetags/jinja.py @@ -0,0 +1,67 @@ +# Copyright © 2023 Innovatie Ltd. All rights reserved. +""" +Jinja support. +""" +import typing as t + +from django.template import RequestContext, loader +from jinja2 import pass_context +from jinja2.ext import Extension +from jinja2.runtime import Context, Undefined + +from .reactpy import component as djt_component +from .. import config + + +class ReactPyExtension(Extension): + """ + Jinja has more expressive power than core Django's templates, and can + directly handle expansions such as: + + {{ component(*args, **kwargs) }} + """ + DJT_TEMPLATE = 'reactpy/component.html' + # + # Therefore, there is no new tag to parse(). + # + tags = {} + + def __init__(self, environment): + super().__init__(environment) + # + # All we need is to add global "component" to the environment. + # + environment.globals["component"] = self._jinja_component + + @pass_context + def _jinja_component(self, __context: Context, dotted_path: str, *args: t.Any, host: str | None = None, + prerender: str = str(config.REACTPY_PRERENDER), **kwargs: t.Any) -> t.Union[t.Any, Undefined]: + """ + This method is used to embed an existing ReactPy component into your + Jinja2 template. + + Args: + dotted_path: String of the fully qualified name of a component. + *args: The positional arguments to provide to the component. + + Keyword Args: + class: The HTML class to apply to the top-level component div. + key: Force the component's root node to use a specific key value. \ + Using key within a template tag is effectively useless. + host: The host to use for the ReactPy connections. If set to `None`, \ + the host will be automatically configured. \ + Example values include: `localhost:8000`, `example.com`, `example.com/subdir` + prerender: Configures whether to pre-render this component, which \ + enables SEO compatibility and reduces perceived latency. + **kwargs: The keyword arguments to provide to the component. + + Returns: + Whatever the components returns. + """ + djt_context = RequestContext(__context.parent['request'], autoescape=__context.eval_ctx.autoescape) + context = djt_component(djt_context, dotted_path, *args, host=host, prerender=prerender, **kwargs) + # + # TODO: can this be usefully cached? + # + result = loader.render_to_string(self.DJT_TEMPLATE, context, __context.parent['request']) + return result From d43263965787005e9569abf989f0fb9e04fd5765 Mon Sep 17 00:00:00 2001 From: Shaheed Haque Date: Thu, 12 Oct 2023 18:39:33 +0100 Subject: [PATCH 2/4] Address second pass review comments. --- src/reactpy_django/templatetags/jinja.py | 29 +++++++----------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/reactpy_django/templatetags/jinja.py b/src/reactpy_django/templatetags/jinja.py index a851a4d9..7f620e78 100644 --- a/src/reactpy_django/templatetags/jinja.py +++ b/src/reactpy_django/templatetags/jinja.py @@ -2,15 +2,12 @@ """ Jinja support. """ -import typing as t - from django.template import RequestContext, loader from jinja2 import pass_context from jinja2.ext import Extension -from jinja2.runtime import Context, Undefined - -from .reactpy import component as djt_component -from .. import config +from jinja2.runtime import Context +from reactpy_django import config +from reactpy_django.templatetags.reactpy import component class ReactPyExtension(Extension): @@ -31,11 +28,10 @@ def __init__(self, environment): # # All we need is to add global "component" to the environment. # - environment.globals["component"] = self._jinja_component + environment.globals["component"] = self.template_tag @pass_context - def _jinja_component(self, __context: Context, dotted_path: str, *args: t.Any, host: str | None = None, - prerender: str = str(config.REACTPY_PRERENDER), **kwargs: t.Any) -> t.Union[t.Any, Undefined]: + def template_tag(self, jinja_context: Context, dotted_path: str, *args, **kwargs) -> str: """ This method is used to embed an existing ReactPy component into your Jinja2 template. @@ -45,23 +41,14 @@ def _jinja_component(self, __context: Context, dotted_path: str, *args: t.Any, h *args: The positional arguments to provide to the component. Keyword Args: - class: The HTML class to apply to the top-level component div. - key: Force the component's root node to use a specific key value. \ - Using key within a template tag is effectively useless. - host: The host to use for the ReactPy connections. If set to `None`, \ - the host will be automatically configured. \ - Example values include: `localhost:8000`, `example.com`, `example.com/subdir` - prerender: Configures whether to pre-render this component, which \ - enables SEO compatibility and reduces perceived latency. **kwargs: The keyword arguments to provide to the component. Returns: Whatever the components returns. """ - djt_context = RequestContext(__context.parent['request'], autoescape=__context.eval_ctx.autoescape) - context = djt_component(djt_context, dotted_path, *args, host=host, prerender=prerender, **kwargs) + django_context = RequestContext(jinja_context.parent['request'], autoescape=jinja_context.eval_ctx.autoescape) + template_context = component(django_context, dotted_path, *args, **kwargs) # # TODO: can this be usefully cached? # - result = loader.render_to_string(self.DJT_TEMPLATE, context, __context.parent['request']) - return result + return loader.render_to_string(self.DJT_TEMPLATE, template_context, jinja_context.parent['request']) From d888b4dd5e7d7e0a22645a6dfe682de3c35303a0 Mon Sep 17 00:00:00 2001 From: Shaheed Haque Date: Sun, 26 Nov 2023 20:12:36 +0000 Subject: [PATCH 3/4] Address lastoutstadnngcomment, and run black. --- src/reactpy_django/templatetags/jinja.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/reactpy_django/templatetags/jinja.py b/src/reactpy_django/templatetags/jinja.py index 7f620e78..2f194a72 100644 --- a/src/reactpy_django/templatetags/jinja.py +++ b/src/reactpy_django/templatetags/jinja.py @@ -9,6 +9,11 @@ from reactpy_django import config from reactpy_django.templatetags.reactpy import component +# +# Point to our non-Django analogue. +# +DJT_TEMPLATE = "reactpy/component.html" + class ReactPyExtension(Extension): """ @@ -17,7 +22,7 @@ class ReactPyExtension(Extension): {{ component(*args, **kwargs) }} """ - DJT_TEMPLATE = 'reactpy/component.html' + # # Therefore, there is no new tag to parse(). # @@ -31,7 +36,9 @@ def __init__(self, environment): environment.globals["component"] = self.template_tag @pass_context - def template_tag(self, jinja_context: Context, dotted_path: str, *args, **kwargs) -> str: + def template_tag( + self, jinja_context: Context, dotted_path: str, *args, **kwargs + ) -> str: """ This method is used to embed an existing ReactPy component into your Jinja2 template. @@ -46,9 +53,14 @@ def template_tag(self, jinja_context: Context, dotted_path: str, *args, **kwargs Returns: Whatever the components returns. """ - django_context = RequestContext(jinja_context.parent['request'], autoescape=jinja_context.eval_ctx.autoescape) + django_context = RequestContext( + jinja_context.parent["request"], + autoescape=jinja_context.eval_ctx.autoescape, + ) template_context = component(django_context, dotted_path, *args, **kwargs) # # TODO: can this be usefully cached? # - return loader.render_to_string(self.DJT_TEMPLATE, template_context, jinja_context.parent['request']) + return loader.render_to_string( + DJT_TEMPLATE, template_context, jinja_context.parent["request"] + ) From 3f56d4e4b5597899f69ef67dc246ff1c10baece4 Mon Sep 17 00:00:00 2001 From: Shaheed Haque Date: Tue, 28 Nov 2023 17:23:30 +0000 Subject: [PATCH 4/4] Address copyright, and use of constant to locate template. --- src/reactpy_django/templatetags/jinja.py | 11 ++--------- src/reactpy_django/templatetags/reactpy.py | 3 ++- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/reactpy_django/templatetags/jinja.py b/src/reactpy_django/templatetags/jinja.py index 2f194a72..38b14b76 100644 --- a/src/reactpy_django/templatetags/jinja.py +++ b/src/reactpy_django/templatetags/jinja.py @@ -1,4 +1,3 @@ -# Copyright © 2023 Innovatie Ltd. All rights reserved. """ Jinja support. """ @@ -6,13 +5,7 @@ from jinja2 import pass_context from jinja2.ext import Extension from jinja2.runtime import Context -from reactpy_django import config -from reactpy_django.templatetags.reactpy import component - -# -# Point to our non-Django analogue. -# -DJT_TEMPLATE = "reactpy/component.html" +from reactpy_django.templatetags.reactpy import COMPONENT_TEMPLATE, component class ReactPyExtension(Extension): @@ -62,5 +55,5 @@ def template_tag( # TODO: can this be usefully cached? # return loader.render_to_string( - DJT_TEMPLATE, template_context, jinja_context.parent["request"] + COMPONENT_TEMPLATE, template_context, jinja_context.parent["request"] ) diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index 209c0ec8..97dd99f9 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -27,11 +27,12 @@ RESOLVED_WEB_MODULES_PATH = reverse("reactpy:web_modules", args=["/"]).strip("/") except NoReverseMatch: RESOLVED_WEB_MODULES_PATH = "" +COMPONENT_TEMPLATE = "reactpy/component.html" register = template.Library() _logger = getLogger(__name__) -@register.inclusion_tag("reactpy/component.html", takes_context=True) +@register.inclusion_tag(COMPONENT_TEMPLATE, takes_context=True) def component( context: template.RequestContext, dotted_path: str,