mirror of
https://github.com/django-components/django-components.git
synced 2025-09-20 20:59:46 +00:00
feat: on_slot_rendered extension hook + refactor debug highlight as extension (#1209)
* feat: on_slot_rendered extension hook + refactor debug highlight as extension * refactor: fix whitespace in test output
This commit is contained in:
parent
223fc2c68c
commit
6ff2d78a2f
12 changed files with 560 additions and 77 deletions
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -8,7 +8,8 @@ Summary:
|
||||||
|
|
||||||
- Overhauled typing system
|
- Overhauled typing system
|
||||||
- Middleware removed, no longer needed
|
- Middleware removed, no longer needed
|
||||||
- `get_template_data()` is the new canonical way to define template data
|
- `get_template_data()` is the new canonical way to define template data.
|
||||||
|
`get_context_data()` is now deprecated but will remain until v2.
|
||||||
- Slots API polished and prepared for v1.
|
- Slots API polished and prepared for v1.
|
||||||
- Merged `Component.Url` with `Component.View`
|
- Merged `Component.Url` with `Component.View`
|
||||||
- Added `Component.args`, `Component.kwargs`, `Component.slots`
|
- Added `Component.args`, `Component.kwargs`, `Component.slots`
|
||||||
|
@ -756,6 +757,24 @@ Summary:
|
||||||
|
|
||||||
Read more on [Component caching](https://django-components.github.io/django-components/0.140/concepts/advanced/component_caching/).
|
Read more on [Component caching](https://django-components.github.io/django-components/0.140/concepts/advanced/component_caching/).
|
||||||
|
|
||||||
|
- New extension hook `on_slot_rendered()`
|
||||||
|
|
||||||
|
This hook is called when a slot is rendered, and allows you to access and/or modify the rendered result.
|
||||||
|
|
||||||
|
This is used by the ["debug highlight" feature](https://django-components.github.io/django-components/0.140/guides/other/troubleshooting/#component-and-slot-highlighting).
|
||||||
|
|
||||||
|
To modify the rendered result, return the new value:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyExtension(ComponentExtension):
|
||||||
|
def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> Optional[str]:
|
||||||
|
return ctx.result + "<!-- Hello, world! -->"
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't want to modify the rendered result, return `None`.
|
||||||
|
|
||||||
|
See all [Extension hooks](https://django-components.github.io/django-components/0.140/reference/extension_hooks/).
|
||||||
|
|
||||||
#### Fix
|
#### Fix
|
||||||
|
|
||||||
- Fix bug: Context processors data was being generated anew for each component. Now the data is correctly created once and reused across components with the same request ([#1165](https://github.com/django-components/django-components/issues/1165)).
|
- Fix bug: Context processors data was being generated anew for each component. Now the data is correctly created once and reused across components with the same request ([#1165](https://github.com/django-components/django-components/issues/1165)).
|
||||||
|
|
|
@ -751,10 +751,11 @@ class InternalSettings:
|
||||||
|
|
||||||
# Prepend built-in extensions
|
# Prepend built-in extensions
|
||||||
from django_components.extensions.cache import CacheExtension
|
from django_components.extensions.cache import CacheExtension
|
||||||
|
from django_components.extensions.debug_highlight import DebugHighlightExtension
|
||||||
from django_components.extensions.defaults import DefaultsExtension
|
from django_components.extensions.defaults import DefaultsExtension
|
||||||
from django_components.extensions.view import ViewExtension
|
from django_components.extensions.view import ViewExtension
|
||||||
|
|
||||||
extensions = [CacheExtension, DefaultsExtension, ViewExtension] + list(extensions)
|
extensions = [CacheExtension, DefaultsExtension, ViewExtension, DebugHighlightExtension] + list(extensions)
|
||||||
|
|
||||||
# Extensions may be passed in either as classes or import strings.
|
# Extensions may be passed in either as classes or import strings.
|
||||||
extension_instances: List["ComponentExtension"] = []
|
extension_instances: List["ComponentExtension"] = []
|
||||||
|
|
|
@ -64,8 +64,8 @@ class HtmlAttrsNode(BaseNode):
|
||||||
<div class="my-class extra-class" data-id="123">
|
<div class="my-class extra-class" data-id="123">
|
||||||
```
|
```
|
||||||
|
|
||||||
**See more usage examples in
|
See more usage examples in
|
||||||
[HTML attributes](../../concepts/fundamentals/html_attributes#examples-for-html_attrs).**
|
[HTML attributes](../../concepts/fundamentals/html_attributes#examples-for-html_attrs).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tag = "html_attrs"
|
tag = "html_attrs"
|
||||||
|
|
|
@ -31,7 +31,7 @@ from django.template.loader_tags import BLOCK_CONTEXT_KEY, BlockContext
|
||||||
from django.test.signals import template_rendered
|
from django.test.signals import template_rendered
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from django_components.app_settings import ContextBehavior, app_settings
|
from django_components.app_settings import ContextBehavior
|
||||||
from django_components.component_media import ComponentMediaInput, ComponentMediaMeta
|
from django_components.component_media import ComponentMediaInput, ComponentMediaMeta
|
||||||
from django_components.component_registry import ComponentRegistry
|
from django_components.component_registry import ComponentRegistry
|
||||||
from django_components.component_registry import registry as registry_
|
from django_components.component_registry import registry as registry_
|
||||||
|
@ -74,7 +74,6 @@ from django_components.slots import (
|
||||||
resolve_fills,
|
resolve_fills,
|
||||||
)
|
)
|
||||||
from django_components.template import cached_template
|
from django_components.template import cached_template
|
||||||
from django_components.util.component_highlight import apply_component_highlight
|
|
||||||
from django_components.util.context import gen_context_processors_data, snapshot_context
|
from django_components.util.context import gen_context_processors_data, snapshot_context
|
||||||
from django_components.util.django_monkeypatch import is_template_cls_patched
|
from django_components.util.django_monkeypatch import is_template_cls_patched
|
||||||
from django_components.util.exception import component_error_message
|
from django_components.util.exception import component_error_message
|
||||||
|
@ -2943,9 +2942,6 @@ class Component(metaclass=ComponentMeta):
|
||||||
del component_context_cache[render_id] # type: ignore[arg-type]
|
del component_context_cache[render_id] # type: ignore[arg-type]
|
||||||
unregister_provide_reference(render_id) # type: ignore[arg-type]
|
unregister_provide_reference(render_id) # type: ignore[arg-type]
|
||||||
|
|
||||||
if app_settings.DEBUG_HIGHLIGHT_COMPONENTS:
|
|
||||||
html = apply_component_highlight("component", html, f"{self.name} ({render_id})")
|
|
||||||
|
|
||||||
html = extensions.on_component_rendered(
|
html = extensions.on_component_rendered(
|
||||||
OnComponentRenderedContext(
|
OnComponentRenderedContext(
|
||||||
component=self,
|
component=self,
|
||||||
|
|
|
@ -14,6 +14,7 @@ from django_components.util.routing import URLRoute
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
from django_components.component_registry import ComponentRegistry
|
from django_components.component_registry import ComponentRegistry
|
||||||
|
from django_components.slots import Slot, SlotResult
|
||||||
|
|
||||||
|
|
||||||
TCallable = TypeVar("TCallable", bound=Callable)
|
TCallable = TypeVar("TCallable", bound=Callable)
|
||||||
|
@ -128,6 +129,26 @@ class OnComponentRenderedContext(NamedTuple):
|
||||||
"""The rendered component"""
|
"""The rendered component"""
|
||||||
|
|
||||||
|
|
||||||
|
# TODO - Add `component` once we create instances inside `render()`
|
||||||
|
# See https://github.com/django-components/django-components/issues/1186
|
||||||
|
@mark_extension_hook_api
|
||||||
|
class OnSlotRenderedContext(NamedTuple):
|
||||||
|
component_cls: Type["Component"]
|
||||||
|
"""The Component class that contains the `{% slot %}` tag"""
|
||||||
|
component_id: str
|
||||||
|
"""The unique identifier for this component instance"""
|
||||||
|
slot: "Slot"
|
||||||
|
"""The Slot instance that was rendered"""
|
||||||
|
slot_name: str
|
||||||
|
"""The name of the `{% slot %}` tag"""
|
||||||
|
slot_is_required: bool
|
||||||
|
"""Whether the slot is required"""
|
||||||
|
slot_is_default: bool
|
||||||
|
"""Whether the slot is default"""
|
||||||
|
result: "SlotResult"
|
||||||
|
"""The rendered result of the slot"""
|
||||||
|
|
||||||
|
|
||||||
################################################
|
################################################
|
||||||
# EXTENSIONS CORE
|
# EXTENSIONS CORE
|
||||||
################################################
|
################################################
|
||||||
|
@ -529,6 +550,31 @@ class ComponentExtension:
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
##########################
|
||||||
|
# Tags lifecycle hooks
|
||||||
|
##########################
|
||||||
|
|
||||||
|
def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Called when a [`{% slot %}`](../template_tags#slot) tag was rendered.
|
||||||
|
|
||||||
|
Use this hook to access or post-process the slot's rendered output.
|
||||||
|
|
||||||
|
To modify the output, return a new string from this hook.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django_components import ComponentExtension, OnSlotRenderedContext
|
||||||
|
|
||||||
|
class MyExtension(ComponentExtension):
|
||||||
|
def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> Optional[str]:
|
||||||
|
# Append a comment to the slot's rendered output
|
||||||
|
return ctx.result + "<!-- MyExtension comment -->"
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Decorator to store events in `ExtensionManager._events` when django_components is not yet initialized.
|
# Decorator to store events in `ExtensionManager._events` when django_components is not yet initialized.
|
||||||
def store_events(func: TCallable) -> TCallable:
|
def store_events(func: TCallable) -> TCallable:
|
||||||
|
@ -846,6 +892,17 @@ class ExtensionManager:
|
||||||
ctx = ctx._replace(result=result)
|
ctx = ctx._replace(result=result)
|
||||||
return ctx.result
|
return ctx.result
|
||||||
|
|
||||||
|
##########################
|
||||||
|
# Tags lifecycle hooks
|
||||||
|
##########################
|
||||||
|
|
||||||
|
def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> Optional[str]:
|
||||||
|
for extension in self.extensions:
|
||||||
|
result = extension.on_slot_rendered(ctx)
|
||||||
|
if result is not None:
|
||||||
|
ctx = ctx._replace(result=result)
|
||||||
|
return ctx.result
|
||||||
|
|
||||||
|
|
||||||
# NOTE: This is a singleton which is takes the extensions from `app_settings.EXTENSIONS`
|
# NOTE: This is a singleton which is takes the extensions from `app_settings.EXTENSIONS`
|
||||||
extensions = ExtensionManager()
|
extensions = ExtensionManager()
|
||||||
|
|
131
src/django_components/extensions/debug_highlight.py
Normal file
131
src/django_components/extensions/debug_highlight.py
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
from typing import Any, Literal, NamedTuple, Optional, Type
|
||||||
|
|
||||||
|
from django_components.app_settings import app_settings
|
||||||
|
from django_components.extension import ComponentExtension, OnComponentRenderedContext, OnSlotRenderedContext
|
||||||
|
from django_components.util.misc import gen_id
|
||||||
|
|
||||||
|
|
||||||
|
class HighlightColor(NamedTuple):
|
||||||
|
text_color: str
|
||||||
|
border_color: str
|
||||||
|
|
||||||
|
|
||||||
|
COLORS = {
|
||||||
|
"component": HighlightColor(text_color="#2f14bb", border_color="blue"),
|
||||||
|
"slot": HighlightColor(text_color="#bb1414", border_color="#e40c0c"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def apply_component_highlight(type: Literal["component", "slot"], output: str, name: str) -> str:
|
||||||
|
"""
|
||||||
|
Wrap HTML (string) in a div with a border and a highlight color.
|
||||||
|
|
||||||
|
This is part of the component / slot highlighting feature. User can toggle on
|
||||||
|
to see the component / slot boundaries.
|
||||||
|
"""
|
||||||
|
color = COLORS[type]
|
||||||
|
|
||||||
|
# Because the component / slot name is set via styling as a `::before` pseudo-element,
|
||||||
|
# we need to generate a unique ID for each component / slot to avoid conflicts.
|
||||||
|
highlight_id = gen_id()
|
||||||
|
|
||||||
|
output = f"""
|
||||||
|
<style>
|
||||||
|
.{type}-highlight-{highlight_id}::before {{
|
||||||
|
content: "{name}: ";
|
||||||
|
font-weight: bold;
|
||||||
|
color: {color.text_color};
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
<div class="{type}-highlight-{highlight_id}" style="border: 1px solid {color.border_color}">
|
||||||
|
{output}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
# TODO - Deprecate `DEBUG_HIGHLIGHT_SLOTS` and `DEBUG_HIGHLIGHT_COMPONENTS` (with removal in v1)
|
||||||
|
# once `extension_defaults` is implemented.
|
||||||
|
# That way people will be able to set the highlighting from single place.
|
||||||
|
# At that point also document this extension in the docs:
|
||||||
|
# - Exposing `ComponentDebugHighlight` from `__init__.py`
|
||||||
|
# - Adding `Component.DebugHighlight` and `Component.debug_highlight` attributes to Component class
|
||||||
|
# so it's easier to find.
|
||||||
|
# - Check docstring of `ComponentDebugHighlight` in the docs and make sure it's correct.
|
||||||
|
class HighlightComponentsDescriptor:
|
||||||
|
def __get__(self, obj: Optional[Any], objtype: Type) -> bool:
|
||||||
|
return app_settings.DEBUG_HIGHLIGHT_COMPONENTS
|
||||||
|
|
||||||
|
|
||||||
|
class HighlightSlotsDescriptor:
|
||||||
|
def __get__(self, obj: Optional[Any], objtype: Type) -> bool:
|
||||||
|
return app_settings.DEBUG_HIGHLIGHT_SLOTS
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentDebugHighlight(ComponentExtension.ExtensionClass): # type: ignore
|
||||||
|
"""
|
||||||
|
The interface for `Component.DebugHighlight`.
|
||||||
|
|
||||||
|
The fields of this class are used to configure the component debug highlighting for this component
|
||||||
|
and its direct slots.
|
||||||
|
|
||||||
|
Read more about [Component debug highlighting](../../concepts/advanced/component_debug_highlighting).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django_components import Component
|
||||||
|
|
||||||
|
class MyComponent(Component):
|
||||||
|
class DebugHighlight:
|
||||||
|
highlight_components = True
|
||||||
|
highlight_slots = True
|
||||||
|
```
|
||||||
|
|
||||||
|
To highlight ALL components and slots, set
|
||||||
|
[`ComponentsSettings.DEBUG_HIGHLIGHT_SLOTS`](../../settings/components_settings.md#debug_highlight_slots) and
|
||||||
|
[`ComponentsSettings.DEBUG_HIGHLIGHT_COMPONENTS`](../../settings/components_settings.md#debug_highlight_components)
|
||||||
|
to `True`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO_v1 - Remove `DEBUG_HIGHLIGHT_COMPONENTS` and `DEBUG_HIGHLIGHT_SLOTS`
|
||||||
|
# Instead set this as plain boolean fields.
|
||||||
|
highlight_components = HighlightComponentsDescriptor()
|
||||||
|
"""Whether to highlight this component in the rendered output."""
|
||||||
|
highlight_slots = HighlightSlotsDescriptor()
|
||||||
|
"""Whether to highlight slots of this component in the rendered output."""
|
||||||
|
|
||||||
|
|
||||||
|
# TODO_v1 - Move into standalone extension (own repo?) and ask people to manually add this extension in settings.
|
||||||
|
class DebugHighlightExtension(ComponentExtension):
|
||||||
|
"""
|
||||||
|
This extension adds the ability to highlight components and slots in the rendered output.
|
||||||
|
|
||||||
|
To highlight slots, set `ComponentsSettings.DEBUG_HIGHLIGHT_SLOTS` to `True` in your settings.
|
||||||
|
|
||||||
|
To highlight components, set `ComponentsSettings.DEBUG_HIGHLIGHT_COMPONENTS` to `True`.
|
||||||
|
|
||||||
|
Highlighting is done by wrapping the content in a `<div>` with a border and a highlight color.
|
||||||
|
|
||||||
|
This extension is automatically added to all components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "debug_highlight"
|
||||||
|
ExtensionClass = ComponentDebugHighlight
|
||||||
|
|
||||||
|
# Apply highlight to the slot's rendered output
|
||||||
|
def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> Optional[str]:
|
||||||
|
debug_cls: Optional[ComponentDebugHighlight] = getattr(ctx.component_cls, "DebugHighlight", None)
|
||||||
|
if not debug_cls or not debug_cls.highlight_slots:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return apply_component_highlight("slot", ctx.result, f"{ctx.component_cls.__name__} - {ctx.slot_name}")
|
||||||
|
|
||||||
|
# Apply highlight to the rendered component
|
||||||
|
def on_component_rendered(self, ctx: OnComponentRenderedContext) -> Optional[str]:
|
||||||
|
debug_cls: Optional[ComponentDebugHighlight] = getattr(ctx.component_cls, "DebugHighlight", None)
|
||||||
|
if not debug_cls or not debug_cls.highlight_components:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return apply_component_highlight("component", ctx.result, f"{ctx.component.name} ({ctx.component_id})")
|
|
@ -66,7 +66,7 @@ def get_component_url(
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
view_cls: Optional[Type[ComponentView]] = getattr(component, "View", None)
|
view_cls: Optional[Type[ComponentView]] = getattr(component, "View", None)
|
||||||
if view_cls is None or not view_cls.public:
|
if not _is_view_public(view_cls):
|
||||||
raise RuntimeError("Component URL is not available - Component is not public")
|
raise RuntimeError("Component URL is not available - Component is not public")
|
||||||
|
|
||||||
route_name = _get_component_route_name(component)
|
route_name = _get_component_route_name(component)
|
||||||
|
@ -235,7 +235,7 @@ class ViewExtension(ComponentExtension):
|
||||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||||
comp_cls = ctx.component_cls
|
comp_cls = ctx.component_cls
|
||||||
view_cls: Optional[Type[ComponentView]] = getattr(comp_cls, "View", None)
|
view_cls: Optional[Type[ComponentView]] = getattr(comp_cls, "View", None)
|
||||||
if view_cls is None or not view_cls.public:
|
if not _is_view_public(view_cls):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create a URL route like `components/MyTable_a1b2c3/`
|
# Create a URL route like `components/MyTable_a1b2c3/`
|
||||||
|
@ -259,3 +259,9 @@ class ViewExtension(ComponentExtension):
|
||||||
if route is None:
|
if route is None:
|
||||||
return
|
return
|
||||||
extensions.remove_extension_urls(self.name, [route])
|
extensions.remove_extension_urls(self.name, [route])
|
||||||
|
|
||||||
|
|
||||||
|
def _is_view_public(view_cls: Optional[Type[ComponentView]]) -> bool:
|
||||||
|
if view_cls is None:
|
||||||
|
return False
|
||||||
|
return getattr(view_cls, "public", False)
|
||||||
|
|
|
@ -26,11 +26,11 @@ from django.template.exceptions import TemplateSyntaxError
|
||||||
from django.utils.html import conditional_escape
|
from django.utils.html import conditional_escape
|
||||||
from django.utils.safestring import SafeString, mark_safe
|
from django.utils.safestring import SafeString, mark_safe
|
||||||
|
|
||||||
from django_components.app_settings import ContextBehavior, app_settings
|
from django_components.app_settings import ContextBehavior
|
||||||
from django_components.context import _COMPONENT_CONTEXT_KEY, _INJECT_CONTEXT_KEY_PREFIX
|
from django_components.context import _COMPONENT_CONTEXT_KEY, _INJECT_CONTEXT_KEY_PREFIX
|
||||||
|
from django_components.extension import OnSlotRenderedContext, extensions
|
||||||
from django_components.node import BaseNode
|
from django_components.node import BaseNode
|
||||||
from django_components.perfutil.component import component_context_cache
|
from django_components.perfutil.component import component_context_cache
|
||||||
from django_components.util.component_highlight import apply_component_highlight
|
|
||||||
from django_components.util.exception import add_slot_to_error_message
|
from django_components.util.exception import add_slot_to_error_message
|
||||||
from django_components.util.logger import trace_component_msg
|
from django_components.util.logger import trace_component_msg
|
||||||
from django_components.util.misc import get_index, get_last_index, is_identifier
|
from django_components.util.misc import get_index, get_last_index, is_identifier
|
||||||
|
@ -851,8 +851,18 @@ class SlotNode(BaseNode):
|
||||||
# the render function ALWAYS receives them.
|
# the render function ALWAYS receives them.
|
||||||
output = slot(data=kwargs, fallback=fallback, context=used_ctx)
|
output = slot(data=kwargs, fallback=fallback, context=used_ctx)
|
||||||
|
|
||||||
if app_settings.DEBUG_HIGHLIGHT_SLOTS:
|
# Allow plugins to post-process the slot's rendered output
|
||||||
output = apply_component_highlight("slot", output, f"{component_name} - {slot_name}")
|
output = extensions.on_slot_rendered(
|
||||||
|
OnSlotRenderedContext(
|
||||||
|
component_cls=component_ctx.component_class,
|
||||||
|
component_id=component_ctx.component_id,
|
||||||
|
slot=slot,
|
||||||
|
slot_name=slot_name,
|
||||||
|
slot_is_required=is_required,
|
||||||
|
slot_is_default=is_default,
|
||||||
|
result=output,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
trace_component_msg(
|
trace_component_msg(
|
||||||
"RENDER_SLOT_END",
|
"RENDER_SLOT_END",
|
||||||
|
@ -1104,9 +1114,9 @@ class FillNode(BaseNode):
|
||||||
# ...
|
# ...
|
||||||
# {% endfill %}
|
# {% endfill %}
|
||||||
# {% endfor %}
|
# {% endfor %}
|
||||||
collected_fills: Optional[List[FillWithData]] = context.get(FILL_GEN_CONTEXT_KEY, None)
|
captured_fills: Optional[List[FillWithData]] = context.get(FILL_GEN_CONTEXT_KEY, None)
|
||||||
|
|
||||||
if collected_fills is None:
|
if captured_fills is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"FillNode.render() (AKA {% fill ... %} block) cannot be rendered outside of a Component context. "
|
"FillNode.render() (AKA {% fill ... %} block) cannot be rendered outside of a Component context. "
|
||||||
"Make sure that the {% fill %} tags are nested within {% component %} tags."
|
"Make sure that the {% fill %} tags are nested within {% component %} tags."
|
||||||
|
@ -1166,7 +1176,7 @@ class FillNode(BaseNode):
|
||||||
layer["forloop"] = layer["forloop"].copy()
|
layer["forloop"] = layer["forloop"].copy()
|
||||||
data.extra_context.update(layer)
|
data.extra_context.update(layer)
|
||||||
|
|
||||||
collected_fills.append(data)
|
captured_fills.append(data)
|
||||||
|
|
||||||
|
|
||||||
#######################################
|
#######################################
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
from typing import Literal, NamedTuple
|
|
||||||
|
|
||||||
from django_components.util.misc import gen_id
|
|
||||||
|
|
||||||
|
|
||||||
class HighlightColor(NamedTuple):
|
|
||||||
text_color: str
|
|
||||||
border_color: str
|
|
||||||
|
|
||||||
|
|
||||||
COLORS = {
|
|
||||||
"component": HighlightColor(text_color="#2f14bb", border_color="blue"),
|
|
||||||
"slot": HighlightColor(text_color="#bb1414", border_color="#e40c0c"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def apply_component_highlight(type: Literal["component", "slot"], output: str, name: str) -> str:
|
|
||||||
"""
|
|
||||||
Wrap HTML (string) in a div with a border and a highlight color.
|
|
||||||
|
|
||||||
This is part of the component / slot highlighting feature. User can toggle on
|
|
||||||
to see the component / slot boundaries.
|
|
||||||
"""
|
|
||||||
color = COLORS[type]
|
|
||||||
|
|
||||||
# Because the component / slot name is set via styling as a `::before` pseudo-element,
|
|
||||||
# we need to generate a unique ID for each component / slot to avoid conflicts.
|
|
||||||
highlight_id = gen_id()
|
|
||||||
|
|
||||||
output = f"""
|
|
||||||
<style>
|
|
||||||
.{type}-highlight-{highlight_id}::before {{
|
|
||||||
content: "{name}: ";
|
|
||||||
font-weight: bold;
|
|
||||||
color: {color.text_color};
|
|
||||||
}}
|
|
||||||
</style>
|
|
||||||
<div class="{type}-highlight-{highlight_id}" style="border: 1px solid {color.border_color}">
|
|
||||||
{output}
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
return output
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
@ -97,7 +98,14 @@ class TestExtensionsListCommand:
|
||||||
call_command("components", "ext", "list")
|
call_command("components", "ext", "list")
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
|
|
||||||
assert output.strip() == "name \n========\ncache \ndefaults\nview"
|
assert output.strip() == (
|
||||||
|
"name \n"
|
||||||
|
"===============\n"
|
||||||
|
"cache \n"
|
||||||
|
"defaults \n"
|
||||||
|
"view \n"
|
||||||
|
"debug_highlight"
|
||||||
|
)
|
||||||
|
|
||||||
@djc_test(
|
@djc_test(
|
||||||
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
@ -108,7 +116,16 @@ class TestExtensionsListCommand:
|
||||||
call_command("components", "ext", "list")
|
call_command("components", "ext", "list")
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
|
|
||||||
assert output.strip() == "name \n========\ncache \ndefaults\nview \nempty \ndummy"
|
assert output.strip() == (
|
||||||
|
"name \n"
|
||||||
|
"===============\n"
|
||||||
|
"cache \n"
|
||||||
|
"defaults \n"
|
||||||
|
"view \n"
|
||||||
|
"debug_highlight\n"
|
||||||
|
"empty \n"
|
||||||
|
"dummy"
|
||||||
|
)
|
||||||
|
|
||||||
@djc_test(
|
@djc_test(
|
||||||
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
@ -119,7 +136,16 @@ class TestExtensionsListCommand:
|
||||||
call_command("components", "ext", "list", "--all")
|
call_command("components", "ext", "list", "--all")
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
|
|
||||||
assert output.strip() == "name \n========\ncache \ndefaults\nview \nempty \ndummy"
|
assert output.strip() == (
|
||||||
|
"name \n"
|
||||||
|
"===============\n"
|
||||||
|
"cache \n"
|
||||||
|
"defaults \n"
|
||||||
|
"view \n"
|
||||||
|
"debug_highlight\n"
|
||||||
|
"empty \n"
|
||||||
|
"dummy"
|
||||||
|
)
|
||||||
|
|
||||||
@djc_test(
|
@djc_test(
|
||||||
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
@ -130,7 +156,16 @@ class TestExtensionsListCommand:
|
||||||
call_command("components", "ext", "list", "--columns", "name")
|
call_command("components", "ext", "list", "--columns", "name")
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
|
|
||||||
assert output.strip() == "name \n========\ncache \ndefaults\nview \nempty \ndummy"
|
assert output.strip() == (
|
||||||
|
"name \n"
|
||||||
|
"===============\n"
|
||||||
|
"cache \n"
|
||||||
|
"defaults \n"
|
||||||
|
"view \n"
|
||||||
|
"debug_highlight\n"
|
||||||
|
"empty \n"
|
||||||
|
"dummy"
|
||||||
|
)
|
||||||
|
|
||||||
@djc_test(
|
@djc_test(
|
||||||
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
@ -141,7 +176,14 @@ class TestExtensionsListCommand:
|
||||||
call_command("components", "ext", "list", "--simple")
|
call_command("components", "ext", "list", "--simple")
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
|
|
||||||
assert output.strip() == "cache \ndefaults\nview \nempty \ndummy"
|
assert output.strip() == (
|
||||||
|
"cache \n"
|
||||||
|
"defaults \n"
|
||||||
|
"view \n"
|
||||||
|
"debug_highlight\n"
|
||||||
|
"empty \n"
|
||||||
|
"dummy"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
|
@ -155,11 +197,16 @@ class TestExtensionsRunCommand:
|
||||||
call_command("components", "ext", "run")
|
call_command("components", "ext", "run")
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
|
|
||||||
|
# Fix line breaking in CI on the first line between the `[-h]` and `{{cmd_name}}`
|
||||||
|
output = re.compile(r"\]\s+\{").sub("] {", output)
|
||||||
|
# Fix line breaking in CI on the first line between the `{{cmd_name}}` and `...`
|
||||||
|
output = re.compile(r"\}\s+\.\.\.").sub("} ...", output)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
output
|
output
|
||||||
== dedent(
|
== dedent(
|
||||||
f"""
|
f"""
|
||||||
usage: components ext run [-h] {{cache,defaults,view,empty,dummy}} ...
|
usage: components ext run [-h] {{cache,defaults,view,debug_highlight,empty,dummy}} ...
|
||||||
|
|
||||||
Run a command added by an extension.
|
Run a command added by an extension.
|
||||||
|
|
||||||
|
@ -167,10 +214,11 @@ class TestExtensionsRunCommand:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
|
||||||
subcommands:
|
subcommands:
|
||||||
{{cache,defaults,view,empty,dummy}}
|
{{cache,defaults,view,debug_highlight,empty,dummy}}
|
||||||
cache Run commands added by the 'cache' extension.
|
cache Run commands added by the 'cache' extension.
|
||||||
defaults Run commands added by the 'defaults' extension.
|
defaults Run commands added by the 'defaults' extension.
|
||||||
view Run commands added by the 'view' extension.
|
view Run commands added by the 'view' extension.
|
||||||
|
debug_highlight Run commands added by the 'debug_highlight' extension.
|
||||||
empty Run commands added by the 'empty' extension.
|
empty Run commands added by the 'empty' extension.
|
||||||
dummy Run commands added by the 'dummy' extension.
|
dummy Run commands added by the 'dummy' extension.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,14 +1,60 @@
|
||||||
from django_components.util.component_highlight import apply_component_highlight, COLORS
|
from django.template import Context, Template
|
||||||
|
from pytest_django.asserts import assertHTMLEqual
|
||||||
|
|
||||||
|
from django_components import Component, register, types
|
||||||
|
from django_components.extensions.debug_highlight import apply_component_highlight, COLORS
|
||||||
from django_components.testing import djc_test
|
from django_components.testing import djc_test
|
||||||
from .testutils import setup_test_config
|
from .testutils import setup_test_config
|
||||||
|
|
||||||
setup_test_config({"autodiscover": False})
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_template() -> Template:
|
||||||
|
@register("inner")
|
||||||
|
class InnerComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<div class="inner">
|
||||||
|
<div>
|
||||||
|
1: {% slot "content" default / %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
2: {% slot "content" default / %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
@register("outer")
|
||||||
|
class OuterComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<div class="outer">
|
||||||
|
{% component "inner" %}
|
||||||
|
{{ content }}
|
||||||
|
{% endcomponent %}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {
|
||||||
|
"content": kwargs["content"],
|
||||||
|
}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% for item in items %}
|
||||||
|
<div class="item">
|
||||||
|
{% component "outer" content=item / %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestComponentHighlight:
|
class TestComponentHighlight:
|
||||||
def test_component_highlight(self):
|
def test_component_highlight_fn(self):
|
||||||
# Test component highlighting
|
# Test component highlighting
|
||||||
test_html = "<div>Test content</div>"
|
test_html = "<div>Test content</div>"
|
||||||
component_name = "TestComponent"
|
component_name = "TestComponent"
|
||||||
|
@ -22,7 +68,7 @@ class TestComponentHighlight:
|
||||||
assert COLORS["component"].text_color in result
|
assert COLORS["component"].text_color in result
|
||||||
assert COLORS["component"].border_color in result
|
assert COLORS["component"].border_color in result
|
||||||
|
|
||||||
def test_slot_highlight(self):
|
def test_slot_highlight_fn(self):
|
||||||
# Test slot highlighting
|
# Test slot highlighting
|
||||||
test_html = "<span>Slot content</span>"
|
test_html = "<span>Slot content</span>"
|
||||||
slot_name = "content-slot"
|
slot_name = "content-slot"
|
||||||
|
@ -35,3 +81,213 @@ class TestComponentHighlight:
|
||||||
# Check that the slot colors are used
|
# Check that the slot colors are used
|
||||||
assert COLORS["slot"].text_color in result
|
assert COLORS["slot"].text_color in result
|
||||||
assert COLORS["slot"].border_color in result
|
assert COLORS["slot"].border_color in result
|
||||||
|
|
||||||
|
@djc_test(components_settings={"debug_highlight_components": True})
|
||||||
|
def test_component_highlight_extension(self):
|
||||||
|
template = _prepare_template()
|
||||||
|
rendered = template.render(Context({"items": [1, 2]}))
|
||||||
|
|
||||||
|
expected = """
|
||||||
|
<div class="item">
|
||||||
|
<style>
|
||||||
|
.component-highlight-a1bc45::before {
|
||||||
|
content: "outer (ca1bc3f): ";
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2f14bb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="component-highlight-a1bc45" style="border: 1px solid blue">
|
||||||
|
<div class="outer" data-djc-id-ca1bc3f="">
|
||||||
|
<style>
|
||||||
|
.component-highlight-a1bc44::before {
|
||||||
|
content: "inner (ca1bc41): ";
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2f14bb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="component-highlight-a1bc44" style="border: 1px solid blue">
|
||||||
|
<div class="inner" data-djc-id-ca1bc41="">
|
||||||
|
<div>
|
||||||
|
1: 1
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
2: 1
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<style>
|
||||||
|
.component-highlight-a1bc49::before {
|
||||||
|
content: "outer (ca1bc46): ";
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2f14bb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="component-highlight-a1bc49" style="border: 1px solid blue">
|
||||||
|
<div class="outer" data-djc-id-ca1bc46="">
|
||||||
|
<style>
|
||||||
|
.component-highlight-a1bc48::before {
|
||||||
|
content: "inner (ca1bc47): ";
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2f14bb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="component-highlight-a1bc48" style="border: 1px solid blue">
|
||||||
|
<div class="inner" data-djc-id-ca1bc47="">
|
||||||
|
<div>
|
||||||
|
1: 2
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
2: 2
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
|
@djc_test(components_settings={"debug_highlight_slots": True})
|
||||||
|
def test_slot_highlight_extension(self):
|
||||||
|
template = _prepare_template()
|
||||||
|
rendered = template.render(Context({"items": [1, 2]}))
|
||||||
|
|
||||||
|
expected = """
|
||||||
|
<div class="item">
|
||||||
|
<div class="outer" data-djc-id-ca1bc3f="">
|
||||||
|
<div class="inner" data-djc-id-ca1bc41="">
|
||||||
|
<div>
|
||||||
|
1:
|
||||||
|
<style>
|
||||||
|
.slot-highlight-a1bc44::before {
|
||||||
|
content: "InnerComponent - content: ";
|
||||||
|
font-weight: bold;
|
||||||
|
color: #bb1414;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="slot-highlight-a1bc44" style="border: 1px solid #e40c0c">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
2:
|
||||||
|
<style>
|
||||||
|
.slot-highlight-a1bc45::before {
|
||||||
|
content: "InnerComponent - content: ";
|
||||||
|
font-weight: bold;
|
||||||
|
color: #bb1414;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="slot-highlight-a1bc45" style="border: 1px solid #e40c0c">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<div class="outer" data-djc-id-ca1bc46="">
|
||||||
|
<div class="inner" data-djc-id-ca1bc47="">
|
||||||
|
<div>
|
||||||
|
1:
|
||||||
|
<style>
|
||||||
|
.slot-highlight-a1bc48::before {
|
||||||
|
content: "InnerComponent - content: ";
|
||||||
|
font-weight: bold;
|
||||||
|
color: #bb1414;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="slot-highlight-a1bc48" style="border: 1px solid #e40c0c">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
2:
|
||||||
|
<style>
|
||||||
|
.slot-highlight-a1bc49::before {
|
||||||
|
content: "InnerComponent - content: ";
|
||||||
|
font-weight: bold;
|
||||||
|
color: #bb1414;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="slot-highlight-a1bc49" style="border: 1px solid #e40c0c">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
|
def test_highlight_on_component_class(self):
|
||||||
|
@register("inner")
|
||||||
|
class InnerComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<div class="inner">
|
||||||
|
<div>
|
||||||
|
1: {% slot "content" default / %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
2: {% slot "content" default / %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
class DebugHighlight:
|
||||||
|
highlight_components = True
|
||||||
|
highlight_slots = True
|
||||||
|
|
||||||
|
template = Template("""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "inner" %}
|
||||||
|
{{ content }}
|
||||||
|
{% endcomponent %}
|
||||||
|
""")
|
||||||
|
rendered = template.render(Context({"content": "Hello, world!"}))
|
||||||
|
|
||||||
|
expected = """
|
||||||
|
<style>
|
||||||
|
.component-highlight-a1bc44::before {
|
||||||
|
content: "inner (ca1bc3f): ";
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2f14bb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="component-highlight-a1bc44" style="border: 1px solid blue">
|
||||||
|
<div class="inner" data-djc-id-ca1bc3f="">
|
||||||
|
<div>
|
||||||
|
1:
|
||||||
|
<style>
|
||||||
|
.slot-highlight-a1bc42::before {
|
||||||
|
content: "InnerComponent - content: ";
|
||||||
|
font-weight: bold;
|
||||||
|
color: #bb1414;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="slot-highlight-a1bc42" style="border: 1px solid #e40c0c">
|
||||||
|
Hello, world!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
2:
|
||||||
|
<style>
|
||||||
|
.slot-highlight-a1bc43::before {
|
||||||
|
content: "InnerComponent - content: ";
|
||||||
|
font-weight: bold;
|
||||||
|
color: #bb1414;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="slot-highlight-a1bc43" style="border: 1px solid #e40c0c">
|
||||||
|
Hello, world!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
assertHTMLEqual(rendered, expected)
|
||||||
|
|
|
@ -22,6 +22,7 @@ from django_components.extension import (
|
||||||
OnComponentDataContext,
|
OnComponentDataContext,
|
||||||
)
|
)
|
||||||
from django_components.extensions.cache import CacheExtension
|
from django_components.extensions.cache import CacheExtension
|
||||||
|
from django_components.extensions.debug_highlight import DebugHighlightExtension
|
||||||
from django_components.extensions.defaults import DefaultsExtension
|
from django_components.extensions.defaults import DefaultsExtension
|
||||||
from django_components.extensions.view import ViewExtension
|
from django_components.extensions.view import ViewExtension
|
||||||
|
|
||||||
|
@ -132,11 +133,12 @@ def with_registry(on_created: Callable):
|
||||||
class TestExtension:
|
class TestExtension:
|
||||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
def test_extensions_setting(self):
|
def test_extensions_setting(self):
|
||||||
assert len(app_settings.EXTENSIONS) == 4
|
assert len(app_settings.EXTENSIONS) == 5
|
||||||
assert isinstance(app_settings.EXTENSIONS[0], CacheExtension)
|
assert isinstance(app_settings.EXTENSIONS[0], CacheExtension)
|
||||||
assert isinstance(app_settings.EXTENSIONS[1], DefaultsExtension)
|
assert isinstance(app_settings.EXTENSIONS[1], DefaultsExtension)
|
||||||
assert isinstance(app_settings.EXTENSIONS[2], ViewExtension)
|
assert isinstance(app_settings.EXTENSIONS[2], ViewExtension)
|
||||||
assert isinstance(app_settings.EXTENSIONS[3], DummyExtension)
|
assert isinstance(app_settings.EXTENSIONS[3], DebugHighlightExtension)
|
||||||
|
assert isinstance(app_settings.EXTENSIONS[4], DummyExtension)
|
||||||
|
|
||||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
def test_access_component_from_extension(self):
|
def test_access_component_from_extension(self):
|
||||||
|
@ -175,7 +177,7 @@ class TestExtension:
|
||||||
class TestExtensionHooks:
|
class TestExtensionHooks:
|
||||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
def test_component_class_lifecycle_hooks(self):
|
def test_component_class_lifecycle_hooks(self):
|
||||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
||||||
|
|
||||||
assert len(extension.calls["on_component_class_created"]) == 0
|
assert len(extension.calls["on_component_class_created"]) == 0
|
||||||
assert len(extension.calls["on_component_class_deleted"]) == 0
|
assert len(extension.calls["on_component_class_deleted"]) == 0
|
||||||
|
@ -207,7 +209,7 @@ class TestExtensionHooks:
|
||||||
|
|
||||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
def test_registry_lifecycle_hooks(self):
|
def test_registry_lifecycle_hooks(self):
|
||||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
||||||
|
|
||||||
assert len(extension.calls["on_registry_created"]) == 0
|
assert len(extension.calls["on_registry_created"]) == 0
|
||||||
assert len(extension.calls["on_registry_deleted"]) == 0
|
assert len(extension.calls["on_registry_deleted"]) == 0
|
||||||
|
@ -244,7 +246,7 @@ class TestExtensionHooks:
|
||||||
return {"name": kwargs.get("name", "World")}
|
return {"name": kwargs.get("name", "World")}
|
||||||
|
|
||||||
registry.register("test_comp", TestComponent)
|
registry.register("test_comp", TestComponent)
|
||||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
||||||
|
|
||||||
# Verify on_component_registered was called
|
# Verify on_component_registered was called
|
||||||
assert len(extension.calls["on_component_registered"]) == 1
|
assert len(extension.calls["on_component_registered"]) == 1
|
||||||
|
@ -282,7 +284,7 @@ class TestExtensionHooks:
|
||||||
test_slots = {"content": "Some content"}
|
test_slots = {"content": "Some content"}
|
||||||
TestComponent.render(context=test_context, args=("arg1", "arg2"), kwargs={"name": "Test"}, slots=test_slots)
|
TestComponent.render(context=test_context, args=("arg1", "arg2"), kwargs={"name": "Test"}, slots=test_slots)
|
||||||
|
|
||||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
||||||
|
|
||||||
# Verify on_component_input was called with correct args
|
# Verify on_component_input was called with correct args
|
||||||
assert len(extension.calls["on_component_input"]) == 1
|
assert len(extension.calls["on_component_input"]) == 1
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue