Support passing extra context variables via the {% component %} tag

pull/10902/head
Matt Westcott 2023-09-16 00:15:10 +01:00 zatwierdzone przez LB (Ben Johnston)
rodzic dd95b8b0d2
commit 6009903c55
5 zmienionych plików z 169 dodań i 15 usunięć

Wyświetl plik

@ -28,6 +28,7 @@ Changelog
* Extract generic breadcrumbs functionality from page breadcrumbs (Sage Abdullah)
* Add support for `placement` in the `human_readable_date` tooltip template tag (Rohit Sharma)
* Add breadcrumbs to generic model views (Sage Abdullah)
* Support passing extra context variables via the `{% component %}` tag (Matt Westcott)
* Fix: Ensure that StreamField's `FieldBlock`s correctly set the `required` and `aria-describedby` attributes (Storm Heg)
* Fix: Avoid an error when the moderation panel (admin dashboard) contains both snippets and private pages (Matt Westcott)
* Fix: When deleting collections, ensure the collection name is correctly shown in the success message (LB (Ben) Johnston)

Wyświetl plik

@ -115,6 +115,26 @@ the `my_app/welcome.html` template could render the panels as follows:
{% endfor %}
```
You can pass additional context variables to the component using the keyword `with`:
```html+django
{% component panel with username=request.user.username %}
```
To render the component with only the variables provided (and no others from the calling template's context), use `only`:
```html+django
{% component panel with username=request.user.username only %}
```
To store the component's rendered output in a variable rather than outputting it immediately, use `as` followed by the variable name:
```html+django
{% component panel as panel_html %}
{{ panel_html }}
```
Note that it is your template's responsibility to output any media declarations defined on the components. For a Wagtail admin view, this is best done by constructing a media object for the whole page within the view, passing this to the template, and outputting it via the base template's `extra_js` and `extra_css` blocks:
```python

Wyświetl plik

@ -40,6 +40,7 @@ depth: 1
* Extract generic breadcrumbs functionality from page breadcrumbs (Sage Abdullah)
* Add support for `placement` in `human_readable_date` the tooltip template tag (Rohit Sharma)
* Add breadcrumbs to generic model views (Sage Abdullah)
* Support passing extra context variables via the `{% component %}` tag (Matt Westcott)
### Bug fixes

Wyświetl plik

@ -19,7 +19,7 @@ from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
from django.utils import timezone
from django.utils.encoding import force_str
from django.utils.html import avoid_wrapping, json_script
from django.utils.html import avoid_wrapping, conditional_escape, json_script
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from django.utils.timesince import timesince
@ -980,21 +980,107 @@ def resolve_url(url):
return ""
@register.simple_tag(takes_context=True)
def component(context, obj, fallback_render_method=False):
# Render a component by calling its render_html method, passing request and context from the
# calling template.
# If fallback_render_method is true, objects without a render_html method will have render()
# called instead (with no arguments) - this is to provide deprecation path for things that have
# been newly upgraded to use the component pattern.
class ComponentNode(template.Node):
def __init__(
self,
component,
extra_context=None,
isolated_context=False,
fallback_render_method=None,
target_var=None,
):
self.component = component
self.extra_context = extra_context or {}
self.isolated_context = isolated_context
self.fallback_render_method = fallback_render_method
self.target_var = target_var
has_render_html_method = hasattr(obj, "render_html")
if fallback_render_method and not has_render_html_method and hasattr(obj, "render"):
return obj.render()
elif not has_render_html_method:
raise ValueError(f"Cannot render {obj!r} as a component")
def render(self, context: Context) -> str:
# Render a component by calling its render_html method, passing request and context from the
# calling template.
# If fallback_render_method is true, objects without a render_html method will have render()
# called instead (with no arguments) - this is to provide deprecation path for things that have
# been newly upgraded to use the component pattern.
return obj.render_html(context)
component = self.component.resolve(context)
if self.fallback_render_method:
fallback_render_method = self.fallback_render_method.resolve(context)
else:
fallback_render_method = False
values = {
name: var.resolve(context) for name, var in self.extra_context.items()
}
if hasattr(component, "render_html"):
if self.isolated_context:
html = component.render_html(context.new(values))
else:
with context.push(**values):
html = component.render_html(context)
elif fallback_render_method and hasattr(component, "render"):
html = component.render()
else:
raise ValueError(f"Cannot render {component!r} as a component")
if self.target_var:
context[self.target_var] = html
return ""
else:
if context.autoescape:
html = conditional_escape(html)
return html
@register.tag(name="component")
def component(parser, token):
bits = token.split_contents()[1:]
if not bits:
raise template.TemplateSyntaxError(
"'component' tag requires at least one argument, the component object"
)
component = parser.compile_filter(bits.pop(0))
# the only valid keyword argument immediately following the component
# is fallback_render_method
flags = token_kwargs(bits, parser)
fallback_render_method = flags.pop("fallback_render_method", None)
if flags:
raise template.TemplateSyntaxError(
"'component' tag only accepts 'fallback_render_method' as a keyword argument"
)
extra_context = {}
isolated_context = False
target_var = None
while bits:
bit = bits.pop(0)
if bit == "with":
extra_context = token_kwargs(bits, parser)
elif bit == "only":
isolated_context = True
elif bit == "as":
try:
target_var = bits.pop(0)
except IndexError:
raise template.TemplateSyntaxError(
"'component' tag with 'as' must be followed by a variable name"
)
else:
raise template.TemplateSyntaxError(
"'component' tag received an unknown argument: %r" % bit
)
return ComponentNode(
component,
extra_context=extra_context,
isolated_context=isolated_context,
fallback_render_method=fallback_render_method,
target_var=target_var,
)
class FragmentNode(template.Node):

Wyświetl plik

@ -228,7 +228,7 @@ class TestComponentTag(SimpleTestCase):
class MyComponent(Component):
def render_html(self, parent_context):
return format_html(
"<h1>{} was here</h1>", parent_context.get("first_name")
"<h1>{} was here</h1>", parent_context.get("first_name", "nobody")
)
template = Template(
@ -237,6 +237,41 @@ class TestComponentTag(SimpleTestCase):
html = template.render(Context({"my_component": MyComponent()}))
self.assertEqual(html, "<h1>Kilroy was here</h1>")
template = Template(
"{% load wagtailadmin_tags %}{% component my_component with first_name='Kilroy' %}"
)
html = template.render(Context({"my_component": MyComponent()}))
self.assertEqual(html, "<h1>Kilroy was here</h1>")
template = Template(
"{% load wagtailadmin_tags %}{% with first_name='Kilroy' %}{% component my_component with surname='Silk' only %}{% endwith %}"
)
html = template.render(Context({"my_component": MyComponent()}))
self.assertEqual(html, "<h1>nobody was here</h1>")
def test_fallback_render_method(self):
class MyComponent(Component):
def render_html(self, parent_context):
return format_html("<h1>I am a component</h1>")
class MyNonComponent:
def render(self):
return format_html("<h1>I am not a component</h1>")
template = Template("{% load wagtailadmin_tags %}{% component my_component %}")
html = template.render(Context({"my_component": MyComponent()}))
self.assertEqual(html, "<h1>I am a component</h1>")
with self.assertRaises(ValueError):
template.render(Context({"my_component": MyNonComponent()}))
template = Template(
"{% load wagtailadmin_tags %}{% component my_component fallback_render_method=True %}"
)
html = template.render(Context({"my_component": MyComponent()}))
self.assertEqual(html, "<h1>I am a component</h1>")
html = template.render(Context({"my_component": MyNonComponent()}))
self.assertEqual(html, "<h1>I am not a component</h1>")
def test_component_escapes_unsafe_strings(self):
class MyComponent(Component):
def render_html(self, parent_context):
@ -259,6 +294,17 @@ class TestComponentTag(SimpleTestCase):
template.render(Context({"my_component": "hello"}))
self.assertEqual(str(cm.exception), "Cannot render 'hello' as a component")
def test_render_as_var(self):
class MyComponent(Component):
def render_html(self, parent_context):
return format_html("<h1>I am a component</h1>")
template = Template(
"{% load wagtailadmin_tags %}{% component my_component as my_html %}The result was: {{ my_html }}"
)
html = template.render(Context({"my_component": MyComponent()}))
self.assertEqual(html, "The result was: <h1>I am a component</h1>")
@override_settings(
WAGTAIL_CONTENT_LANGUAGES=[