mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 14:28:18 +00:00
fix: various fixes for inject/provide and html_attrs (#541)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
23d91218bd
commit
2684b41c07
7 changed files with 106 additions and 30 deletions
|
@ -15,6 +15,10 @@ HTML_ATTRS_DEFAULTS_KEY = "defaults"
|
|||
HTML_ATTRS_ATTRS_KEY = "attrs"
|
||||
|
||||
|
||||
def _default(val: Any, default_val: Any) -> Any:
|
||||
return val if val is not None else default_val
|
||||
|
||||
|
||||
class HtmlAttrsNode(Node):
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -57,8 +61,9 @@ class HtmlAttrsNode(Node):
|
|||
attrs_and_defaults_from_kwargs = process_aggregate_kwargs(attrs_and_defaults_from_kwargs)
|
||||
|
||||
# NOTE: We want to allow to use `html_attrs` even without `attrs` or `defaults` params
|
||||
attrs = attrs_and_defaults_from_kwargs.get(HTML_ATTRS_ATTRS_KEY, {})
|
||||
default_attrs = attrs_and_defaults_from_kwargs.get(HTML_ATTRS_DEFAULTS_KEY, {})
|
||||
# Or when they are None
|
||||
attrs = _default(attrs_and_defaults_from_kwargs.get(HTML_ATTRS_ATTRS_KEY, None), {})
|
||||
default_attrs = _default(attrs_and_defaults_from_kwargs.get(HTML_ATTRS_DEFAULTS_KEY, None), {})
|
||||
|
||||
final_attrs = {**default_attrs, **attrs}
|
||||
final_attrs = append_attributes(*final_attrs.items(), *append_attrs)
|
||||
|
|
|
@ -10,7 +10,7 @@ from django.template.context import Context
|
|||
from django.template.exceptions import TemplateSyntaxError
|
||||
from django.template.loader import get_template
|
||||
from django.template.loader_tags import BLOCK_CONTEXT_KEY
|
||||
from django.utils.html import escape
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.safestring import SafeString, mark_safe
|
||||
from django.views import View
|
||||
|
||||
|
@ -337,6 +337,7 @@ class Component(View, metaclass=ComponentMeta):
|
|||
|
||||
return comp._render(context, args, kwargs, slots, escape_slots_content)
|
||||
|
||||
# This is the internal entrypoint for the render function
|
||||
def _render(
|
||||
self,
|
||||
context: Union[Dict[str, Any], Context] = None,
|
||||
|
@ -344,6 +345,19 @@ class Component(View, metaclass=ComponentMeta):
|
|||
kwargs: Optional[Dict[str, Any]] = None,
|
||||
slots: Optional[Mapping[SlotName, SlotContent]] = None,
|
||||
escape_slots_content: bool = True,
|
||||
) -> str:
|
||||
try:
|
||||
return self._render_impl(context, args, kwargs, slots, escape_slots_content)
|
||||
except Exception as err:
|
||||
raise type(err)(f"An error occured while rendering component '{self.name}':\n{repr(err)}") from err
|
||||
|
||||
def _render_impl(
|
||||
self,
|
||||
context: Union[Dict[str, Any], Context] = None,
|
||||
args: Optional[Union[List, Tuple]] = None,
|
||||
kwargs: Optional[Dict[str, Any]] = None,
|
||||
slots: Optional[Mapping[SlotName, SlotContent]] = None,
|
||||
escape_slots_content: bool = True,
|
||||
) -> str:
|
||||
# Allow to provide no args/kwargs
|
||||
args = args or []
|
||||
|
@ -441,13 +455,13 @@ class Component(View, metaclass=ComponentMeta):
|
|||
for slot_name, content in slots_data.items():
|
||||
if isinstance(content, (str, SafeString)):
|
||||
content_func = _nodelist_to_slot_render_func(
|
||||
NodeList([TextNode(escape(content) if escape_content else content)])
|
||||
NodeList([TextNode(conditional_escape(content) if escape_content else content)])
|
||||
)
|
||||
else:
|
||||
|
||||
def content_func(ctx: Context, kwargs: Dict[str, Any], slot_ref: SlotRef) -> SlotRenderedContent:
|
||||
rendered = content(ctx, kwargs, slot_ref)
|
||||
return escape(rendered) if escape_content else rendered
|
||||
return conditional_escape(rendered) if escape_content else rendered
|
||||
|
||||
slot_fills[slot_name] = FillContent(
|
||||
content_func=content_func,
|
||||
|
|
|
@ -100,7 +100,7 @@ def get_injected_context_var(
|
|||
# Otherwise raise error
|
||||
raise KeyError(
|
||||
f"Component '{component_name}' tried to inject a variable '{key}' before it was provided."
|
||||
f"To fix this, make sure that at least one ancestor of component '{component_name}' has"
|
||||
f" To fix this, make sure that at least one ancestor of component '{component_name}' has"
|
||||
f" the variable '{key}' in their 'provide' attribute."
|
||||
)
|
||||
|
||||
|
|
|
@ -11,7 +11,11 @@ from django.template.exceptions import TemplateSyntaxError
|
|||
from django.utils.safestring import SafeString, mark_safe
|
||||
|
||||
from django_components.app_settings import ContextBehavior, app_settings
|
||||
from django_components.context import _FILLED_SLOTS_CONTENT_CONTEXT_KEY, _ROOT_CTX_CONTEXT_KEY
|
||||
from django_components.context import (
|
||||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY,
|
||||
_INJECT_CONTEXT_KEY_PREFIX,
|
||||
_ROOT_CTX_CONTEXT_KEY,
|
||||
)
|
||||
from django_components.expression import resolve_expression_as_identifier, safe_resolve_dict
|
||||
from django_components.logger import trace_msg
|
||||
from django_components.node import NodeTraverse, nodelist_has_content, walk_nodelist
|
||||
|
@ -144,6 +148,16 @@ class SlotNode(Node):
|
|||
|
||||
extra_context: Dict[str, Any] = {}
|
||||
|
||||
# Irrespective of which context we use ("root" context or the one passed to this
|
||||
# render function), pass down the keys used by inject/provide feature. This makes it
|
||||
# possible to pass the provided values down the slots, e.g.:
|
||||
# {% provide "abc" val=123 %}
|
||||
# {% slot "content" %}{% endslot %}
|
||||
# {% endprovide %}
|
||||
for key, value in context.flatten().items():
|
||||
if key.startswith(_INJECT_CONTEXT_KEY_PREFIX):
|
||||
extra_context[key] = value
|
||||
|
||||
# If slot fill is using `{% fill "myslot" default="abc" %}`, then set the "abc" to
|
||||
# the context, so users can refer to the default slot from within the fill content.
|
||||
slot_ref = SlotRef(self, context)
|
||||
|
|
|
@ -233,6 +233,7 @@ class HtmlAttrsTests(BaseTestCase):
|
|||
return {"attrs": attrs}
|
||||
|
||||
template = Template(self.template_str)
|
||||
|
||||
with self.assertRaisesMessage(TemplateSyntaxError, "Received argument 'attrs' both as a regular input"):
|
||||
template.render(Context({"class_var": "padding-top-8"}))
|
||||
|
||||
|
@ -250,6 +251,7 @@ class HtmlAttrsTests(BaseTestCase):
|
|||
return {"attrs": attrs}
|
||||
|
||||
template = Template(self.template_str)
|
||||
|
||||
with self.assertRaisesMessage(
|
||||
TemplateSyntaxError,
|
||||
"Received argument 'defaults' both as a regular input",
|
||||
|
|
|
@ -481,6 +481,45 @@ class ProvideTemplateTagTest(BaseTestCase):
|
|||
""",
|
||||
)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_slot_in_provide(self):
|
||||
@component.register("injectee")
|
||||
class InjectComponent(component.Component):
|
||||
template: types.django_html = """
|
||||
<div> injected: {{ var|safe }} </div>
|
||||
"""
|
||||
|
||||
def get_context_data(self):
|
||||
var = self.inject("my_provide", "default")
|
||||
return {"var": var}
|
||||
|
||||
@component.register("parent")
|
||||
class ParentComponent(component.Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% provide "my_provide" key="hi" another=123 %}
|
||||
{% slot "content" default %}{% endslot %}
|
||||
{% endprovide %}
|
||||
"""
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "parent" %}
|
||||
{% component "injectee" %}{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
rendered = template.render(Context({}))
|
||||
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<div>
|
||||
injected: DepInject(key='hi', another=123)
|
||||
</div>
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
class InjectTest(BaseTestCase):
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
|
@ -530,6 +569,7 @@ class InjectTest(BaseTestCase):
|
|||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
template.render(Context({}))
|
||||
|
||||
|
@ -581,6 +621,7 @@ class InjectTest(BaseTestCase):
|
|||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
template.render(Context({}))
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import textwrap
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from django.template import Context, Template, TemplateSyntaxError
|
||||
|
@ -210,6 +209,7 @@ class ComponentSlottedTemplateTagTest(BaseTestCase):
|
|||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
|
||||
with self.assertRaises(TemplateSyntaxError):
|
||||
template.render(Context({}))
|
||||
|
||||
|
@ -284,6 +284,7 @@ class ComponentSlottedTemplateTagTest(BaseTestCase):
|
|||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
|
||||
with self.assertRaises(TemplateSyntaxError):
|
||||
template.render(Context({}))
|
||||
|
||||
|
@ -381,6 +382,7 @@ class ComponentSlottedTemplateTagTest(BaseTestCase):
|
|||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
|
||||
with self.assertRaises(TemplateSyntaxError):
|
||||
template.render(Context({}))
|
||||
|
||||
|
@ -398,6 +400,7 @@ class ComponentSlottedTemplateTagTest(BaseTestCase):
|
|||
return Template(template_str)
|
||||
|
||||
c = BadComponent("name")
|
||||
|
||||
with self.assertRaises(TemplateSyntaxError):
|
||||
c.render(Context({}))
|
||||
|
||||
|
@ -417,20 +420,16 @@ class ComponentSlottedTemplateTagTest(BaseTestCase):
|
|||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
with self.assertRaises(TemplateSyntaxError):
|
||||
try:
|
||||
template.render(Context({}))
|
||||
except TemplateSyntaxError as e:
|
||||
self.assertEqual(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
Component 'test1' passed fill that refers to undefined slot: 'haeder'.
|
||||
Unfilled slot names are: ['footer', 'header'].
|
||||
Did you mean 'header'?"""
|
||||
),
|
||||
str(e),
|
||||
)
|
||||
raise e
|
||||
|
||||
with self.assertRaisesMessage(
|
||||
TemplateSyntaxError,
|
||||
(
|
||||
"Component 'test1' passed fill that refers to undefined slot: 'haeder'.\\n"
|
||||
"Unfilled slot names are: ['footer', 'header'].\\n"
|
||||
"Did you mean 'header'?"
|
||||
),
|
||||
):
|
||||
template.render(Context({}))
|
||||
|
||||
# NOTE: This is relevant only for the "isolated" mode
|
||||
@parametrize_context_behavior(["isolated"])
|
||||
|
@ -1144,15 +1143,16 @@ class SlotFillTemplateSyntaxErrorTests(BaseTestCase):
|
|||
{% include 'slotted_template.html' with context=None only %}
|
||||
"""
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "broken_component" %}
|
||||
{% fill "header" %}Custom header {% endfill %}
|
||||
{% fill "main" %}Custom main{% endfill %}
|
||||
{% fill "footer" %}Custom footer{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "broken_component" %}
|
||||
{% fill "header" %}Custom header {% endfill %}
|
||||
{% fill "main" %}Custom main{% endfill %}
|
||||
{% fill "footer" %}Custom footer{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
Template(template_str).render(Context({}))
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue