mirror of
https://github.com/django-components/django-components.git
synced 2025-08-09 16:57:59 +00:00
feat: add component hooks (#631)
This commit is contained in:
parent
3c6f478f8a
commit
0cfc40231b
3 changed files with 253 additions and 0 deletions
87
README.md
87
README.md
|
@ -55,6 +55,7 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
|||
- [Rendering HTML attributes](#rendering-html-attributes)
|
||||
- [Template tag syntax](#template-tag-syntax)
|
||||
- [Prop drilling and dependency injection (provide / inject)](#prop-drilling-and-dependency-injection-provide--inject)
|
||||
- [Component hooks](#component-hooks)
|
||||
- [Component context and scope](#component-context-and-scope)
|
||||
- [Customizing component tags with TagFormatter](#customizing-component-tags-with-tagformatter)
|
||||
- [Defining HTML/JS/CSS files](#defining-htmljscss-files)
|
||||
|
@ -809,6 +810,8 @@ class MyComponent(Component):
|
|||
|
||||
### Adding type hints with Generics
|
||||
|
||||
_New in version 0.92_
|
||||
|
||||
The `Component` class optionally accepts type parameters
|
||||
that allow you to specify the types of args, kwargs, slots, and
|
||||
data:
|
||||
|
@ -937,6 +940,8 @@ class Button(Component[Args, Kwargs, Data, Slots]):
|
|||
|
||||
### Runtime input validation with types
|
||||
|
||||
_New in version 0.96_
|
||||
|
||||
> NOTE: Kwargs, slots, and data validation is supported only for Python >=3.11
|
||||
|
||||
In Python 3.11 and later, when you specify the component types, you will get also runtime validation of the inputs you pass to `Component.render` or `Component.render_to_response`.
|
||||
|
@ -2554,6 +2559,88 @@ renders:
|
|||
<div>123</div>
|
||||
```
|
||||
|
||||
## Component hooks
|
||||
|
||||
_New in version 0.96_
|
||||
|
||||
Component hooks are functions that allow you to intercept the rendering process at specific positions.
|
||||
|
||||
### Available hooks
|
||||
|
||||
- `on_render_before`
|
||||
|
||||
```py
|
||||
def on_render_before(
|
||||
self: Component,
|
||||
context: Context,
|
||||
template: Template
|
||||
) -> None:
|
||||
```
|
||||
|
||||
Hook that runs just before the component's template is rendered.
|
||||
|
||||
You can use this hook to access or modify the context or the template:
|
||||
|
||||
```py
|
||||
def on_render_before(self, context, template) -> None:
|
||||
# Insert value into the Context
|
||||
context["from_on_before"] = ":)"
|
||||
|
||||
# Append text into the Template
|
||||
template.nodelist.append(TextNode("FROM_ON_BEFORE"))
|
||||
```
|
||||
|
||||
- `on_render_after`
|
||||
|
||||
```py
|
||||
def on_render_after(
|
||||
self: Component,
|
||||
context: Context,
|
||||
template: Template,
|
||||
content: str
|
||||
) -> None | str | SafeString:
|
||||
```
|
||||
|
||||
Hook that runs just after the component's template was rendered.
|
||||
It receives the rendered output as the last argument.
|
||||
|
||||
You can use this hook to access the context or the template, but modifying
|
||||
them won't have any effect.
|
||||
|
||||
To override the content that gets rendered, you can return a string or SafeString from this hook:
|
||||
|
||||
```py
|
||||
def on_render_after(self, context, template, content):
|
||||
# Prepend text to the rendered content
|
||||
return "Chocolate cookie recipe: " + content
|
||||
```
|
||||
|
||||
### Component hooks example
|
||||
|
||||
You can use hooks together with [provide / inject](#how-to-use-provide--inject) to create components
|
||||
that accept a list of items via a slot.
|
||||
|
||||
In the example below, each `tab_item` component will be rendered on a separate tab page, but they are all defined in the default slot of the `tabs` component.
|
||||
|
||||
[See here for how it was done](https://github.com/EmilStenstrom/django-components/discussions/540)
|
||||
|
||||
```django
|
||||
{% component "tabs" %}
|
||||
{% component "tab_item" header="Tab 1" %}
|
||||
<p>
|
||||
hello from tab 1
|
||||
</p>
|
||||
{% component "button" %}
|
||||
Click me!
|
||||
{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
|
||||
{% component "tab_item" header="Tab 2" %}
|
||||
Hello this is tab 2
|
||||
{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
## Component context and scope
|
||||
|
||||
By default, context variables are passed down the template as in regular Django - deeper scopes can access the variables from the outer scopes. So if you have several nested forloops, then inside the deep-most loop you can access variables defined by all previous loops.
|
||||
|
|
|
@ -169,6 +169,27 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
|
||||
View = ComponentView
|
||||
|
||||
def on_render_before(self, context: Context, template: Template) -> None:
|
||||
"""
|
||||
Hook that runs just before the component's template is rendered.
|
||||
|
||||
You can use this hook to access or modify the context or the template.
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_render_after(self, context: Context, template: Template, content: str) -> Optional[SlotResult]:
|
||||
"""
|
||||
Hook that runs just after the component's template was rendered.
|
||||
It receives the rendered output as the last argument.
|
||||
|
||||
You can use this hook to access the context or the template, but modifying
|
||||
them won't have any effect.
|
||||
|
||||
To override the content that gets rendered, you can return a string or SafeString
|
||||
from this hook.
|
||||
"""
|
||||
pass
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
registered_name: Optional[str] = None,
|
||||
|
@ -563,8 +584,13 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
},
|
||||
}
|
||||
):
|
||||
self.on_render_before(context, template)
|
||||
|
||||
rendered_component = template.render(context)
|
||||
|
||||
new_output = self.on_render_after(context, template, rendered_component)
|
||||
rendered_component = new_output if new_output is not None else rendered_component
|
||||
|
||||
if is_dependency_middleware_active():
|
||||
output = RENDERED_COMMENT_TEMPLATE.format(name=self.name) + rendered_component
|
||||
else:
|
||||
|
|
|
@ -17,6 +17,7 @@ from unittest import skipIf
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.template import Context, RequestContext, Template, TemplateSyntaxError
|
||||
from django.template.base import TextNode
|
||||
from django.utils.safestring import SafeString
|
||||
|
||||
from django_components import Component, SlotFunc, registry, types
|
||||
|
@ -851,3 +852,142 @@ class ComponentRenderTest(BaseTestCase):
|
|||
rendered_resp.content.decode("utf-8"),
|
||||
"Variable: <strong>123</strong>",
|
||||
)
|
||||
|
||||
|
||||
class ComponentHookTest(BaseTestCase):
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_on_render_before(self):
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
args: {{ args|safe }}
|
||||
kwargs: {{ kwargs|safe }}
|
||||
---
|
||||
from_on_before: {{ from_on_before }}
|
||||
"""
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
return {
|
||||
"args": args,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
|
||||
def on_render_before(self, context: Context, template: Template) -> None:
|
||||
# Insert value into the Context
|
||||
context["from_on_before"] = ":)"
|
||||
|
||||
# Insert text into the Template
|
||||
template.nodelist.append(TextNode("\n---\nFROM_ON_BEFORE"))
|
||||
|
||||
rendered = SimpleComponent.render()
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
args: ()
|
||||
kwargs: {}
|
||||
---
|
||||
from_on_before: :)
|
||||
---
|
||||
FROM_ON_BEFORE
|
||||
""",
|
||||
)
|
||||
|
||||
# Check that modifying the context or template does nothing
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_on_render_after(self):
|
||||
captured_content = None
|
||||
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
args: {{ args|safe }}
|
||||
kwargs: {{ kwargs|safe }}
|
||||
---
|
||||
from_on_before: {{ from_on_before }}
|
||||
"""
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
return {
|
||||
"args": args,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
|
||||
# Check that modifying the context or template does nothing
|
||||
def on_render_after(self, context: Context, template: Template, content: str) -> None:
|
||||
# Insert value into the Context
|
||||
context["from_on_before"] = ":)"
|
||||
|
||||
# Insert text into the Template
|
||||
template.nodelist.append(TextNode("\n---\nFROM_ON_BEFORE"))
|
||||
|
||||
nonlocal captured_content
|
||||
captured_content = content
|
||||
|
||||
rendered = SimpleComponent.render()
|
||||
|
||||
self.assertHTMLEqual(
|
||||
captured_content,
|
||||
"""
|
||||
args: ()
|
||||
kwargs: {}
|
||||
---
|
||||
from_on_before:
|
||||
""",
|
||||
)
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
args: ()
|
||||
kwargs: {}
|
||||
---
|
||||
from_on_before:
|
||||
""",
|
||||
)
|
||||
|
||||
# Check that modifying the context or template does nothing
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_on_render_after_override_output(self):
|
||||
captured_content = None
|
||||
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
args: {{ args|safe }}
|
||||
kwargs: {{ kwargs|safe }}
|
||||
---
|
||||
from_on_before: {{ from_on_before }}
|
||||
"""
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
return {
|
||||
"args": args,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
|
||||
def on_render_after(self, context: Context, template: Template, content: str) -> str:
|
||||
nonlocal captured_content
|
||||
captured_content = content
|
||||
|
||||
return "Chocolate cookie recipe: " + content
|
||||
|
||||
rendered = SimpleComponent.render()
|
||||
|
||||
self.assertHTMLEqual(
|
||||
captured_content,
|
||||
"""
|
||||
args: ()
|
||||
kwargs: {}
|
||||
---
|
||||
from_on_before:
|
||||
""",
|
||||
)
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
Chocolate cookie recipe:
|
||||
args: ()
|
||||
kwargs: {}
|
||||
---
|
||||
from_on_before:
|
||||
""",
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue