{% endif %}
-
- {% comment %}
- This markup is intentionally left without closing div tags so that the contents can be populated with child elements between dialog and enddialog
- For the end tags please see end-dialog.html
- {% endcomment %}
+ {{ children }}
+
+
+
+
diff --git a/wagtail/admin/templates/wagtailadmin/shared/dialog/end_dialog.html b/wagtail/admin/templates/wagtailadmin/shared/dialog/end_dialog.html
deleted file mode 100644
index 4fdd390e79..0000000000
--- a/wagtail/admin/templates/wagtailadmin/shared/dialog/end_dialog.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-{% comment %}
- This markup is used to close the end tags for dialog.html so that content can be nested between both tags like {% dialog %}{% enddialog %}
-{% endcomment %}
diff --git a/wagtail/admin/templates/wagtailadmin/shared/help_block.html b/wagtail/admin/templates/wagtailadmin/shared/help_block.html
new file mode 100644
index 0000000000..48ca31bed4
--- /dev/null
+++ b/wagtail/admin/templates/wagtailadmin/shared/help_block.html
@@ -0,0 +1,10 @@
+{% load wagtailadmin_tags %}
+
+
+ {% if status == 'info' %}
+ {% icon name='help' %}
+ {% else %}
+ {% icon name='warning' %}
+ {% endif %}
+ {{ children }}
+
diff --git a/wagtail/admin/templatetags/wagtailadmin_tags.py b/wagtail/admin/templatetags/wagtailadmin_tags.py
index 559eff5be9..92b57e63c6 100644
--- a/wagtail/admin/templatetags/wagtailadmin_tags.py
+++ b/wagtail/admin/templatetags/wagtailadmin_tags.py
@@ -8,6 +8,8 @@ from django.contrib.admin.utils import quote
from django.contrib.humanize.templatetags.humanize import intcomma, naturaltime
from django.contrib.messages.constants import DEFAULT_TAGS as MESSAGE_TAGS
from django.shortcuts import resolve_url as resolve_url_func
+from django.template import Context
+from django.template.base import token_kwargs
from django.template.defaultfilters import stringfilter
from django.templatetags.static import static
from django.urls import reverse
@@ -859,61 +861,147 @@ def component(context, obj, fallback_render_method=False):
return obj.render_html(context)
-@register.inclusion_tag("wagtailadmin/shared/dialog/dialog.html")
-def dialog(
- id,
- title,
- icon_name=None,
- subtitle=None,
- message_status=None,
- message_heading=None,
- message_description=None,
-):
+class FragmentNode(template.Node):
+ def __init__(self, nodelist, target_var):
+ self.nodelist = nodelist
+ self.target_var = target_var
+
+ def render(self, context):
+ fragment = self.nodelist.render(context) if self.nodelist else ""
+ context[self.target_var] = fragment
+ return ""
+
+
+@register.tag(name="fragment")
+def fragment(parser, token):
"""
- Dialog tag - to be used with its corresponding {% enddialog %} tag with dialog content markup nested between
+ Store a template fragment as a variable.
+
+ Usage:
+ {% fragment as header_title %}
+ {% blocktrans trimmed %}Welcome to the {{ site_name }} Wagtail CMS{% endblocktrans %}
+ {% fragment %}
+
+ Copy-paste of slippers’ fragment template tag.
+ See https://github.com/mixxorz/slippers/blob/254c720e6bb02eb46ae07d104863fce41d4d3164/slippers/templatetags/slippers.py#L173.
"""
- if not title:
- raise ValueError("You must supply a title")
- if not id:
- raise ValueError("You must supply an id")
+ error_message = "The syntax for fragment is {% fragment as variable_name %}"
- # Used for determining which icon the message will use
- message_status_type = {
- "info": {
- "message_icon_name": "info-circle",
- },
- "warning": {
- "message_icon_name": "warning",
- },
- "critical": {
- "message_icon_name": "warning",
- },
- "success": {
- "message_icon_name": "circle-check",
- },
- }
+ try:
+ tag_name, _, target_var = token.split_contents()
+ nodelist = parser.parse(("endfragment",))
+ parser.delete_first_token()
+ except ValueError:
+ if settings.DEBUG:
+ raise template.TemplateSyntaxError(error_message)
+ return ""
- context = {
- "id": id,
- "title": title,
- "icon_name": icon_name,
- "subtitle": subtitle,
- "message_heading": message_heading,
- "message_description": message_description,
- "message_status": message_status,
- }
-
- # If there is a message status then add the context for that message type
- if message_status:
- context.update(**message_status_type[message_status])
-
- return context
+ return FragmentNode(nodelist, target_var)
-# Closing tag for dialog tag {% enddialog %}
-@register.inclusion_tag("wagtailadmin/shared/dialog/end_dialog.html")
-def enddialog():
- return
+class BlockInclusionNode(template.Node):
+ """
+ Create template-driven tags like Django’s inclusion_tag / InclusionNode, but for block-level tags.
+
+ Usage:
+ {% my_tag status="test" label="Alert" %}
+ Proceed with caution.
+ {% endmy_tag %}
+
+ Within `my_tag`’s template, the template fragment will be accessible as the {{ children }} context variable.
+
+ The output can also be stored as a variable in the parent context:
+
+ {% my_tag status="test" label="Alert" as my_variable %}
+ Proceed with caution.
+ {% endmy_tag %}
+
+ Inspired by slippers’ Component Node.
+ See https://github.com/mixxorz/slippers/blob/254c720e6bb02eb46ae07d104863fce41d4d3164/slippers/templatetags/slippers.py#L47.
+ """
+
+ def __init__(self, nodelist, template, extra_context, target_var=None):
+ self.nodelist = nodelist
+ self.template = template
+ self.extra_context = extra_context
+ self.target_var = target_var
+
+ def get_context_data(self, parent_context):
+ return parent_context
+
+ def render(self, context):
+ children = self.nodelist.render(context) if self.nodelist else ""
+
+ values = {
+ # Resolve the tag’s parameters within the current context.
+ key: value.resolve(context)
+ for key, value in self.extra_context.items()
+ }
+
+ t = context.template.engine.get_template(self.template)
+ # Add the `children` variable in the rendered template’s context.
+ context_data = self.get_context_data({**values, "children": children})
+ output = t.render(Context(context_data, autoescape=context.autoescape))
+
+ if self.target_var:
+ context[self.target_var] = output
+ return ""
+
+ return output
+
+ @classmethod
+ def handle(cls, parser, token):
+ tag_name, *remaining_bits = token.split_contents()
+
+ nodelist = parser.parse((f"end{tag_name}",))
+ parser.delete_first_token()
+
+ extra_context = token_kwargs(remaining_bits, parser)
+
+ # Allow component fragment to be assigned to a variable
+ target_var = None
+ if len(remaining_bits) >= 2 and remaining_bits[-2] == "as":
+ target_var = remaining_bits[-1]
+
+ return cls(nodelist, cls.template, extra_context, target_var)
+
+
+class DialogNode(BlockInclusionNode):
+ template = "wagtailadmin/shared/dialog/dialog.html"
+
+ def get_context_data(self, parent_context):
+ context = super().get_context_data(parent_context)
+
+ if "title" not in context:
+ raise TypeError("You must supply a title")
+ if "id" not in context:
+ raise TypeError("You must supply an id")
+
+ # Used for determining which icon the message will use
+ message_icon_name = {
+ "info": "info-circle",
+ "warning": "warning",
+ "critical": "warning",
+ "success": "circle-check",
+ }
+
+ message_status = context.get("message_status")
+
+ # If there is a message status then determine which icon to use.
+ if message_status:
+ context["message_icon_name"] = message_icon_name[message_status]
+
+ return context
+
+
+register.tag("dialog", DialogNode.handle)
+
+
+class HelpBlockNode(BlockInclusionNode):
+ template = "wagtailadmin/shared/help_block.html"
+
+
+register.tag("help_block", HelpBlockNode.handle)
# Button used to open dialogs
diff --git a/wagtail/admin/tests/test_templatetags.py b/wagtail/admin/tests/test_templatetags.py
index 40ce154ce2..3461013039 100644
--- a/wagtail/admin/tests/test_templatetags.py
+++ b/wagtail/admin/tests/test_templatetags.py
@@ -3,7 +3,7 @@ from datetime import timedelta
from unittest import mock
from django.conf import settings
-from django.template import Context, Template
+from django.template import Context, Template, TemplateSyntaxError
from django.test import TestCase
from django.test.utils import override_settings
from django.utils import timezone
@@ -247,3 +247,125 @@ class TestInternationalisationTags(TestCase):
# check with an invalid id
with self.assertNumQueries(0):
self.assertIsNone(locale_label_from_id(self.locale_ids[-1] + 100), None)
+
+
+class ComponentTest(TestCase):
+ def test_render_block_component(self):
+ template = """
+ {% load wagtailadmin_tags %}
+ {% help_block status="info" %}Proceed with caution{% endhelp_block %}
+ """
+
+ expected = """
+
+ """
+
+ self.assertHTMLEqual(expected, Template(template).render(context))
diff --git a/wagtail/contrib/styleguide/templates/wagtailstyleguide/base.html b/wagtail/contrib/styleguide/templates/wagtailstyleguide/base.html
index 0e14247920..1d8fed7b78 100644
--- a/wagtail/contrib/styleguide/templates/wagtailstyleguide/base.html
+++ b/wagtail/contrib/styleguide/templates/wagtailstyleguide/base.html
@@ -256,21 +256,18 @@
Help text is not to be confused with the messages that appear in a banner drop down from the top of the screen. Help text are permanent instructions, visible on every page view, that explain or warn about something.
This is help text that might be just for information, explaining what happens next, or drawing the user's attention to something they're about to do
It could be multiple lines
-
+ {% endhelp_block %}
-
- {% icon name='warning' %}
+ {% help_block status="warning" %}
A warning message might be output in cases where a user's action could have serious consequences
-
+ {% endhelp_block %}
-
- {% icon name='warning' %}
+ {% help_block status="critical" %}
A critical message would probably be rare, in cases where a particularly brittle or dangerously destructive action could be performed and needs to be warned about.
-