kopia lustrzana https://github.com/wagtail/wagtail
Support passing extra context variables via the {% component %} tag
rodzic
dd95b8b0d2
commit
6009903c55
docs
extending
releases
wagtail/admin
templatetags
tests
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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=[
|
||||
|
|
Ładowanie…
Reference in New Issue