diff --git a/docs/recipes/api-rendering.md b/docs/recipes/api-rendering.md
new file mode 100644
index 00000000..9617f96e
--- /dev/null
+++ b/docs/recipes/api-rendering.md
@@ -0,0 +1,25 @@
+# API rendering
+
+For additional flexibility, django-pattern-library supports rendering patterns via an API endpoint.
+This can be useful when implementing a custom UI while still using the pattern library’s Django rendering features.
+
+The API endpoint is available at `api/v1/render-pattern`. It accepts POST requests with a JSON payload containing the following fields:
+
+- `template_name` – the path of the template to render
+- `config` – the configuration for the template, with the same data structure as the configuration files (`context` and `tags`).
+
+Here is an example, with curl:
+
+```bash
+echo '{"template_name": "patterns/molecules/button/button.html", "config": {"context": {"target_page": {"title": "API"}}, "tags": {"pageurl":{"target_page":{"raw": "/hello-api"}}}}}' | curl -d @- http://localhost:8000/api/v1/render-pattern
+```
+
+The response will be the pattern’s rendered HTML:
+
+```html
+
+ API
+
+```
+
+Note compared to iframe rendering, this API always renders the pattern’s HTML standalone, never within a base template.
diff --git a/mkdocs.yml b/mkdocs.yml
index b5b06784..381b9992 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -50,6 +50,7 @@ nav:
- 'Inclusion tags': 'recipes/inclusion-tags.md'
- 'Looping for tags': 'recipes/looping-for-tags.md'
- 'Pagination': 'recipes/pagination.md'
+ - 'API rendering': 'recipes/api-rendering.md'
- 'Reference':
- 'API and settings': 'reference/api.md'
- 'Concepts': 'reference/concepts.md'
diff --git a/pattern_library/monkey_utils.py b/pattern_library/monkey_utils.py
index 705fd4c1..d6ad07ba 100644
--- a/pattern_library/monkey_utils.py
+++ b/pattern_library/monkey_utils.py
@@ -2,11 +2,7 @@
from django.template.library import SimpleNode
-from pattern_library.utils import (
- get_pattern_config,
- is_pattern_library_context,
- render_pattern,
-)
+from pattern_library.utils import is_pattern_library_context, render_pattern
logger = logging.getLogger(__name__)
UNSPECIFIED = object()
@@ -31,9 +27,8 @@ def node_render(context):
tag_overridden = False
result = ""
- # Load pattern's config
- current_template_name = parser.origin.template_name
- pattern_config = get_pattern_config(current_template_name)
+ # Get overriden tag config.
+ tag_overrides = context.get("__pattern_library_tag_overrides", {})
# Extract values for lookup from the token
bits = token.split_contents()
@@ -41,7 +36,7 @@ def node_render(context):
arguments = " ".join(bits[1:]).strip()
# Get config for a specific tag
- tag_config = pattern_config.get("tags", {}).get(tag_name, {})
+ tag_config = tag_overrides.get(tag_name, {})
if tag_config:
# Get config for specific arguments
tag_config = tag_config.get(arguments, {})
@@ -88,7 +83,7 @@ def node_render(context):
logger.warning(
'No default or stub data defined for the "%s" tag in the "%s" template',
tag_name,
- current_template_name,
+ parser.origin.template_name,
)
return original_node_render(context)
diff --git a/pattern_library/urls.py b/pattern_library/urls.py
index 1b9b51a3..4887dfe3 100644
--- a/pattern_library/urls.py
+++ b/pattern_library/urls.py
@@ -1,4 +1,4 @@
-from django.urls import re_path
+from django.urls import path, re_path
from pattern_library import get_pattern_template_suffix, views
@@ -19,4 +19,6 @@
views.RenderPatternView.as_view(),
name="render_pattern",
),
+ # API rendering
+ path("api/v1/render-pattern", views.render_pattern_api, name="render_pattern_api"),
]
diff --git a/pattern_library/utils.py b/pattern_library/utils.py
index 4e2856a1..b1a81b8d 100644
--- a/pattern_library/utils.py
+++ b/pattern_library/utils.py
@@ -210,12 +210,18 @@ def get_pattern_markdown(template_name):
return markdown.markdown(f.read())
-def render_pattern(request, template_name, allow_non_patterns=False):
+def render_pattern(request, template_name, allow_non_patterns=False, config=None):
if not allow_non_patterns and not is_pattern(template_name):
raise TemplateIsNotPattern
- context = get_pattern_context(template_name)
+ if not config:
+ config = get_pattern_config(template_name)
+
+ context = config.get("context", {})
+ tags = config.get("tags", {})
+ mark_context_strings_safe(context)
context[get_pattern_context_var_name()] = True
+ context["__pattern_library_tag_overrides"] = tags
for modifier in registry.get_for_template(template_name):
modifier(context=context, request=request)
return render_to_string(template_name, request=request, context=context)
diff --git a/pattern_library/views.py b/pattern_library/views.py
index 7003e5b2..4e3ab01d 100644
--- a/pattern_library/views.py
+++ b/pattern_library/views.py
@@ -1,8 +1,11 @@
+import json
+
from django.http import Http404, HttpResponse
from django.template.loader import get_template
from django.utils.decorators import method_decorator
from django.utils.html import escape
from django.views.decorators.clickjacking import xframe_options_sameorigin
+from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import TemplateView
from pattern_library import get_base_template_names, get_pattern_base_template_name
@@ -99,3 +102,19 @@ def get(self, request, pattern_template_name=None):
return self.render_to_response(context)
return HttpResponse(rendered_pattern)
+
+
+@csrf_exempt
+def render_pattern_api(request):
+ data = json.loads(request.body.decode("utf-8"))
+ template_name = data["template_name"]
+ config = data["config"]
+
+ try:
+ rendered_pattern = render_pattern(
+ request, template_name, allow_non_patterns=False, config=config
+ )
+ except TemplateIsNotPattern:
+ raise Http404
+
+ return HttpResponse(rendered_pattern)
diff --git a/tests/templates/patterns/molecules/button/button.html b/tests/templates/patterns/molecules/button/button.html
new file mode 100644
index 00000000..006451f3
--- /dev/null
+++ b/tests/templates/patterns/molecules/button/button.html
@@ -0,0 +1,4 @@
+{% load test_tags %}
+
+ {% if label %}{{ label }}{% else %}{{ target_page.title }}{% endif %}
+
diff --git a/tests/templates/patterns/molecules/button/button.yaml b/tests/templates/patterns/molecules/button/button.yaml
new file mode 100644
index 00000000..4546360f
--- /dev/null
+++ b/tests/templates/patterns/molecules/button/button.yaml
@@ -0,0 +1,7 @@
+context:
+ target_page:
+ title: Get started
+tags:
+ pageurl:
+ target_page:
+ raw: /get-started
diff --git a/tests/templatetags/test_tags.py b/tests/templatetags/test_tags.py
index 1326eb08..496935f6 100644
--- a/tests/templatetags/test_tags.py
+++ b/tests/templatetags/test_tags.py
@@ -27,6 +27,12 @@ def default_html_tag_falsey(arg=None):
raise Exception("default_tag raised an exception")
+@register.simple_tag()
+def pageurl(page):
+ """Approximation of wagtail built-in tag for realistic example."""
+ return "/page/url"
+
+
# Get widget type of a field
@register.filter(name="widget_type")
def widget_type(bound_field):
@@ -36,3 +42,4 @@ def widget_type(bound_field):
override_tag(register, "error_tag")
override_tag(register, "default_html_tag", default_html="https://potato.com")
override_tag(register, "default_html_tag_falsey", default_html=None)
+override_tag(register, "pageurl")
diff --git a/tests/tests/test_context_modifiers.py b/tests/tests/test_context_modifiers.py
index 6a646567..12ddd9f5 100644
--- a/tests/tests/test_context_modifiers.py
+++ b/tests/tests/test_context_modifiers.py
@@ -125,6 +125,7 @@ def test_applied_by_render_pattern(self, render_to_string):
context={
"atom_var": "atom_var value from test_atom.yaml",
"is_pattern_library": True,
+ "__pattern_library_tag_overrides": {},
"foo": "bar",
"beep": "boop",
},
diff --git a/tests/tests/test_views.py b/tests/tests/test_views.py
index abd5ddb4..e8f9d00b 100644
--- a/tests/tests/test_views.py
+++ b/tests/tests/test_views.py
@@ -116,3 +116,32 @@ def test_fragment_extended_from_variable(self):
),
"base content - extended content",
)
+
+
+class APIViewsTestCase(SimpleTestCase):
+ def test_renders_with_tag_overrides(self):
+ api_endpoint = reverse("pattern_library:render_pattern_api")
+ response = self.client.post(
+ api_endpoint,
+ content_type="application/json",
+ data={
+ "template_name": "patterns/molecules/button/button.html",
+ "config": {
+ "context": {"target_page": {"title": "API"}},
+ "tags": {"pageurl": {"target_page": {"raw": "/hello-api"}}},
+ },
+ },
+ )
+ self.assertContains(response, "/hello-api")
+
+ def test_404(self):
+ api_endpoint = reverse("pattern_library:render_pattern_api")
+ response = self.client.post(
+ api_endpoint,
+ content_type="application/json",
+ data={
+ "template_name": "doesnotexist.html",
+ "config": {},
+ },
+ )
+ self.assertEqual(response.status_code, 404)