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)
|
- [Rendering HTML attributes](#rendering-html-attributes)
|
||||||
- [Template tag syntax](#template-tag-syntax)
|
- [Template tag syntax](#template-tag-syntax)
|
||||||
- [Prop drilling and dependency injection (provide / inject)](#prop-drilling-and-dependency-injection-provide--inject)
|
- [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)
|
- [Component context and scope](#component-context-and-scope)
|
||||||
- [Customizing component tags with TagFormatter](#customizing-component-tags-with-tagformatter)
|
- [Customizing component tags with TagFormatter](#customizing-component-tags-with-tagformatter)
|
||||||
- [Defining HTML/JS/CSS files](#defining-htmljscss-files)
|
- [Defining HTML/JS/CSS files](#defining-htmljscss-files)
|
||||||
|
@ -809,6 +810,8 @@ class MyComponent(Component):
|
||||||
|
|
||||||
### Adding type hints with Generics
|
### Adding type hints with Generics
|
||||||
|
|
||||||
|
_New in version 0.92_
|
||||||
|
|
||||||
The `Component` class optionally accepts type parameters
|
The `Component` class optionally accepts type parameters
|
||||||
that allow you to specify the types of args, kwargs, slots, and
|
that allow you to specify the types of args, kwargs, slots, and
|
||||||
data:
|
data:
|
||||||
|
@ -937,6 +940,8 @@ class Button(Component[Args, Kwargs, Data, Slots]):
|
||||||
|
|
||||||
### Runtime input validation with types
|
### Runtime input validation with types
|
||||||
|
|
||||||
|
_New in version 0.96_
|
||||||
|
|
||||||
> NOTE: Kwargs, slots, and data validation is supported only for Python >=3.11
|
> 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`.
|
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>
|
<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
|
## 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.
|
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
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
registered_name: Optional[str] = None,
|
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)
|
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():
|
if is_dependency_middleware_active():
|
||||||
output = RENDERED_COMMENT_TEMPLATE.format(name=self.name) + rendered_component
|
output = RENDERED_COMMENT_TEMPLATE.format(name=self.name) + rendered_component
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -17,6 +17,7 @@ from unittest import skipIf
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.template import Context, RequestContext, Template, TemplateSyntaxError
|
from django.template import Context, RequestContext, Template, TemplateSyntaxError
|
||||||
|
from django.template.base import TextNode
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
|
|
||||||
from django_components import Component, SlotFunc, registry, types
|
from django_components import Component, SlotFunc, registry, types
|
||||||
|
@ -851,3 +852,142 @@ class ComponentRenderTest(BaseTestCase):
|
||||||
rendered_resp.content.decode("utf-8"),
|
rendered_resp.content.decode("utf-8"),
|
||||||
"Variable: <strong>123</strong>",
|
"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