mirror of
https://github.com/django-components/django-components.git
synced 2025-08-10 17:28:00 +00:00
fix: do not render slot tag when extracting fill tags (#786)
* fix: do not render slot tag when extracting fill tags * refactor: show component path in error message * docs: update changelog * refactor: fix tests
This commit is contained in:
parent
5a23101038
commit
dd6d668b5e
6 changed files with 182 additions and 2 deletions
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -1,5 +1,31 @@
|
||||||
# Release notes
|
# Release notes
|
||||||
|
|
||||||
|
## v0.114
|
||||||
|
|
||||||
|
#### Fix
|
||||||
|
|
||||||
|
- Prevent rendering Slot tags during fill discovery stage to fix a case when a component inside a slot
|
||||||
|
fill tried to access provided data too early.
|
||||||
|
|
||||||
|
## v0.113
|
||||||
|
|
||||||
|
#### Fix
|
||||||
|
|
||||||
|
- Ensure consistent order of scripts in `Component.Media.js`
|
||||||
|
|
||||||
|
## v0.112
|
||||||
|
|
||||||
|
#### Fix
|
||||||
|
|
||||||
|
- Allow components to accept default fill even if no default slot was encountered during rendering
|
||||||
|
|
||||||
|
## v0.111
|
||||||
|
|
||||||
|
#### Fix
|
||||||
|
|
||||||
|
- Prevent rendering Component tags during fill discovery stage to fix a case when a component inside the default slot
|
||||||
|
tried to access provided data too early.
|
||||||
|
|
||||||
## 🚨📢 v0.110
|
## 🚨📢 v0.110
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
|
|
@ -617,7 +617,31 @@ class Component(
|
||||||
try:
|
try:
|
||||||
return self._render_impl(context, args, kwargs, slots, escape_slots_content, type, render_dependencies)
|
return self._render_impl(context, args, kwargs, slots, escape_slots_content, type, render_dependencies)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
raise _type(err)(f"An error occured while rendering component '{self.name}':\n{repr(err)}") from err
|
# Nicely format the error message to include the component path.
|
||||||
|
# E.g.
|
||||||
|
# ```
|
||||||
|
# KeyError: "An error occured while rendering components ProjectPage > ProjectLayoutTabbed >
|
||||||
|
# Layout > RenderContextProvider > Base > TabItem:
|
||||||
|
# Component 'TabItem' tried to inject a variable '_tab' before it was provided.
|
||||||
|
# ```
|
||||||
|
|
||||||
|
if not hasattr(err, "_components"):
|
||||||
|
err._components = [] # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
components = getattr(err, "_components", [])
|
||||||
|
|
||||||
|
# Access the exception's message, see https://stackoverflow.com/a/75549200/9788634
|
||||||
|
if not components:
|
||||||
|
orig_msg = err.args[0]
|
||||||
|
else:
|
||||||
|
orig_msg = err.args[0].split("\n", 1)[1]
|
||||||
|
|
||||||
|
components.insert(0, self.name)
|
||||||
|
comp_path = " > ".join(components)
|
||||||
|
prefix = f"An error occured while rendering components {comp_path}:\n"
|
||||||
|
|
||||||
|
err.args = (prefix + orig_msg,) # tuple of one
|
||||||
|
raise err
|
||||||
|
|
||||||
def _render_impl(
|
def _render_impl(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -203,6 +203,12 @@ class SlotNode(BaseNode):
|
||||||
def render(self, context: Context) -> SafeString:
|
def render(self, context: Context) -> SafeString:
|
||||||
trace_msg("RENDR", "SLOT", self.trace_id, self.node_id)
|
trace_msg("RENDR", "SLOT", self.trace_id, self.node_id)
|
||||||
|
|
||||||
|
# Do not render `{% slot %}` tags within the `{% component %} .. {% endcomponent %}` tags
|
||||||
|
# at the fill discovery stage (when we render the component's body to determine if the body
|
||||||
|
# is a default slot, or contains named slots).
|
||||||
|
if _is_extracting_fill(context):
|
||||||
|
return ""
|
||||||
|
|
||||||
if _COMPONENT_SLOT_CTX_CONTEXT_KEY not in context or not context[_COMPONENT_SLOT_CTX_CONTEXT_KEY]:
|
if _COMPONENT_SLOT_CTX_CONTEXT_KEY not in context or not context[_COMPONENT_SLOT_CTX_CONTEXT_KEY]:
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
"Encountered a SlotNode outside of a ComponentNode context. "
|
"Encountered a SlotNode outside of a ComponentNode context. "
|
||||||
|
|
|
@ -318,6 +318,67 @@ class ComponentTest(BaseTestCase):
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_prepends_exceptions_with_component_path(self):
|
||||||
|
@register("broken")
|
||||||
|
class Broken(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<div> injected: {{ data|safe }} </div>
|
||||||
|
<main>
|
||||||
|
{% slot "content" default / %}
|
||||||
|
</main>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
data = self.inject("my_provide")
|
||||||
|
data["data1"] # This should raise TypeError
|
||||||
|
return {"data": data}
|
||||||
|
|
||||||
|
@register("provider")
|
||||||
|
class Provider(Component):
|
||||||
|
def get_context_data(self, data: Any) -> Any:
|
||||||
|
return {"data": data}
|
||||||
|
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" data=data %}
|
||||||
|
{% slot "content" default / %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@register("parent")
|
||||||
|
class Parent(Component):
|
||||||
|
def get_context_data(self, data: Any) -> Any:
|
||||||
|
return {"data": data}
|
||||||
|
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "provider" data=data %}
|
||||||
|
{% component "broken" %}
|
||||||
|
{% slot "content" default / %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@register("root")
|
||||||
|
class Root(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "parent" data=123 %}
|
||||||
|
{% fill "content" %}
|
||||||
|
456
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
TypeError,
|
||||||
|
"An error occured while rendering components Root > parent > provider > broken:\n"
|
||||||
|
"tuple indices must be integers or slices, not str",
|
||||||
|
):
|
||||||
|
Root.render()
|
||||||
|
|
||||||
|
|
||||||
class ComponentValidationTest(BaseTestCase):
|
class ComponentValidationTest(BaseTestCase):
|
||||||
def test_validate_input_passes(self):
|
def test_validate_input_passes(self):
|
||||||
|
|
|
@ -483,7 +483,7 @@ class DynamicComponentTemplateTagTest(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
template = Template(simple_tag_template)
|
||||||
with self.assertRaisesMessage(TypeError, "got an unexpected keyword argument \\'invalid_variable\\'"):
|
with self.assertRaisesMessage(TypeError, "got an unexpected keyword argument 'invalid_variable'"):
|
||||||
template.render(Context({}))
|
template.render(Context({}))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -740,6 +740,7 @@ class InjectTest(BaseTestCase):
|
||||||
with self.assertRaises(RuntimeError):
|
with self.assertRaises(RuntimeError):
|
||||||
comp.inject("abc", "def")
|
comp.inject("abc", "def")
|
||||||
|
|
||||||
|
# See https://github.com/EmilStenstrom/django-components/pull/778
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_inject_in_fill(self):
|
def test_inject_in_fill(self):
|
||||||
@register("injectee")
|
@register("injectee")
|
||||||
|
@ -804,3 +805,65 @@ class InjectTest(BaseTestCase):
|
||||||
<main>456</main>
|
<main>456</main>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# See https://github.com/EmilStenstrom/django-components/pull/786
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_inject_in_slot_in_fill(self):
|
||||||
|
@register("injectee")
|
||||||
|
class Injectee(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<div> injected: {{ data|safe }} </div>
|
||||||
|
<main>
|
||||||
|
{% slot "content" default / %}
|
||||||
|
</main>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
data = self.inject("my_provide")
|
||||||
|
return {"data": data}
|
||||||
|
|
||||||
|
@register("provider")
|
||||||
|
class Provider(Component):
|
||||||
|
def get_context_data(self, data: Any) -> Any:
|
||||||
|
return {"data": data}
|
||||||
|
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" data=data %}
|
||||||
|
{% slot "content" default / %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@register("parent")
|
||||||
|
class Parent(Component):
|
||||||
|
def get_context_data(self, data: Any) -> Any:
|
||||||
|
return {"data": data}
|
||||||
|
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "provider" data=data %}
|
||||||
|
{% slot "content" default / %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@register("root")
|
||||||
|
class Root(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "parent" data=123 %}
|
||||||
|
{% component "injectee" / %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
rendered = Root.render()
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div>
|
||||||
|
injected: DepInject(key='hi', data=123)
|
||||||
|
</div>
|
||||||
|
<main></main>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue