mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 06:18:17 +00:00
feat: on_render (#1231)
* feat: on_render * docs: fix typos * refactor: fix linter errors * refactor: make `error` in on_render_after optional to fix benchmarks * refactor: benchmark attempt 2 * refactor: fix linter errors * refactor: fix formatting
This commit is contained in:
parent
46e524e37d
commit
eceebb9696
24 changed files with 1793 additions and 417 deletions
|
@ -1,58 +1,333 @@
|
|||
_New in version 0.96_
|
||||
|
||||
Component hooks are functions that allow you to intercept the rendering process at specific positions.
|
||||
Intercept the rendering lifecycle with Component hooks.
|
||||
|
||||
Unlike the [extension hooks](../../../reference/extension_hooks/), these are defined directly
|
||||
on the [`Component`](../../../reference/api#django_components.Component) class.
|
||||
|
||||
## Available hooks
|
||||
|
||||
- `on_render_before`
|
||||
### `on_render_before`
|
||||
|
||||
```py
|
||||
def on_render_before(
|
||||
self: Component,
|
||||
context: Context,
|
||||
template: Template
|
||||
) -> None:
|
||||
```
|
||||
```py
|
||||
def on_render_before(
|
||||
self: Component,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
) -> None:
|
||||
```
|
||||
|
||||
Hook that runs just before the component's template is rendered.
|
||||
[`Component.on_render_before`](../../../reference/api#django_components.Component.on_render_before) runs just before the component's template is rendered.
|
||||
|
||||
You can use this hook to access or modify the context or the template:
|
||||
It is called for every component, including nested ones, as part of
|
||||
the component render lifecycle.
|
||||
|
||||
```py
|
||||
def on_render_before(self, context, template) -> None:
|
||||
# Insert value into the Context
|
||||
context["from_on_before"] = ":)"
|
||||
It receives the [Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
|
||||
and the [Template](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
|
||||
as arguments.
|
||||
|
||||
# Append text into the Template
|
||||
template.nodelist.append(TextNode("FROM_ON_BEFORE"))
|
||||
```
|
||||
The `template` argument is `None` if the component has no template.
|
||||
|
||||
- `on_render_after`
|
||||
**Example:**
|
||||
|
||||
```py
|
||||
def on_render_after(
|
||||
self: Component,
|
||||
context: Context,
|
||||
template: Template,
|
||||
content: str
|
||||
) -> None | str | SafeString:
|
||||
```
|
||||
You can use this hook to access the context or the template:
|
||||
|
||||
Hook that runs just after the component's template was rendered.
|
||||
It receives the rendered output as the last argument.
|
||||
```py
|
||||
from django.template import Context, Template
|
||||
from django_components import Component
|
||||
|
||||
You can use this hook to access the context or the template, but modifying
|
||||
them won't have any effect.
|
||||
class MyTable(Component):
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
# Insert value into the Context
|
||||
context["from_on_before"] = ":)"
|
||||
|
||||
To override the content that gets rendered, you can return a string or SafeString from this hook:
|
||||
assert isinstance(template, Template)
|
||||
```
|
||||
|
||||
```py
|
||||
def on_render_after(self, context, template, content):
|
||||
# Prepend text to the rendered content
|
||||
return "Chocolate cookie recipe: " + content
|
||||
```
|
||||
!!! warning
|
||||
|
||||
## Component hooks example
|
||||
If you want to pass data to the template, prefer using
|
||||
[`get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
|
||||
instead of this hook.
|
||||
|
||||
!!! warning
|
||||
|
||||
Do NOT modify the template in this hook. The template is reused across renders.
|
||||
|
||||
Since this hook is called for every component, this means that the template would be modified
|
||||
every time a component is rendered.
|
||||
|
||||
### `on_render`
|
||||
|
||||
_New in version 0.140_
|
||||
|
||||
```py
|
||||
def on_render(
|
||||
self: Component,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
) -> Union[str, SafeString, OnRenderGenerator, None]:
|
||||
```
|
||||
|
||||
[`Component.on_render`](../../../reference/api#django_components.Component.on_render) does the actual rendering.
|
||||
|
||||
You can override this method to:
|
||||
|
||||
- Change what template gets rendered
|
||||
- Modify the context
|
||||
- Modify the rendered output after it has been rendered
|
||||
- Handle errors
|
||||
|
||||
The default implementation renders the component's
|
||||
[Template](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
|
||||
with the given
|
||||
[Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context).
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
if template is None:
|
||||
return None
|
||||
else:
|
||||
return template.render(context)
|
||||
```
|
||||
|
||||
The `template` argument is `None` if the component has no template.
|
||||
|
||||
#### Modifying rendered template
|
||||
|
||||
To change what gets rendered, you can:
|
||||
|
||||
- Render a different template
|
||||
- Render a component
|
||||
- Return a different string or SafeString
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
return "Hello"
|
||||
```
|
||||
|
||||
You can also use [`on_render()`](../../../reference/api#django_components.Component.on_render) as a router,
|
||||
rendering other components based on the parent component's arguments:
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
# Select different component based on `feature_new_table` kwarg
|
||||
if self.kwargs.get("feature_new_table"):
|
||||
comp_cls = NewTable
|
||||
else:
|
||||
comp_cls = OldTable
|
||||
|
||||
# Render the selected component
|
||||
return comp_cls.render(
|
||||
args=self.args,
|
||||
kwargs=self.kwargs,
|
||||
slots=self.slots,
|
||||
context=context,
|
||||
)
|
||||
```
|
||||
|
||||
#### Post-processing rendered template
|
||||
|
||||
When you render the original template in [`on_render()`](../../../reference/api#django_components.Component.on_render) as:
|
||||
|
||||
```py
|
||||
template.render(context)
|
||||
```
|
||||
|
||||
The result is NOT the final output, but an intermediate result. Nested components
|
||||
are not rendered yet.
|
||||
|
||||
Instead, django-components needs to take this result and process it
|
||||
to actually render the child components.
|
||||
|
||||
To access the final output, you can `yield` the result instead of returning it.
|
||||
|
||||
This will return a tuple of (rendered HTML, error). The error is `None` if the rendering succeeded.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
|
||||
if error is None:
|
||||
# The rendering succeeded
|
||||
return html
|
||||
else:
|
||||
# The rendering failed
|
||||
print(f"Error: {error}")
|
||||
```
|
||||
|
||||
At this point you can do 3 things:
|
||||
|
||||
1. Return a new HTML
|
||||
|
||||
The new HTML will be used as the final output.
|
||||
|
||||
If the original template raised an error, it will be ignored.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
|
||||
return "NEW HTML"
|
||||
```
|
||||
|
||||
2. Raise a new exception
|
||||
|
||||
The new exception is what will bubble up from the component.
|
||||
|
||||
The original HTML and original error will be ignored.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
|
||||
raise Exception("Error message")
|
||||
```
|
||||
|
||||
3. Return nothing (or `None`) to handle the result as usual
|
||||
|
||||
If you don't raise an exception, and neither return a new HTML,
|
||||
then original HTML / error will be used:
|
||||
|
||||
- If rendering succeeded, the original HTML will be used as the final output.
|
||||
- If rendering failed, the original error will be propagated.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
|
||||
if error is not None:
|
||||
# The rendering failed
|
||||
print(f"Error: {error}")
|
||||
```
|
||||
|
||||
#### Example: ErrorBoundary
|
||||
|
||||
[`on_render()`](../../../reference/api#django_components.Component.on_render) can be used to
|
||||
implement React's [ErrorBoundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary).
|
||||
|
||||
That is, a component that catches errors in nested components and displays a fallback UI instead:
|
||||
|
||||
```django
|
||||
{% component "error_boundary" %}
|
||||
{% fill "content" %}
|
||||
{% component "nested_component" %}
|
||||
{% endfill %}
|
||||
{% fill "fallback" %}
|
||||
Sorry, something went wrong.
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
To implement this, we render the fallback slot in [`on_render()`](../../../reference/api#django_components.Component.on_render)
|
||||
and return it if an error occured:
|
||||
|
||||
```djc_py
|
||||
class ErrorFallback(Component):
|
||||
template = """
|
||||
{% slot "content" default / %}
|
||||
"""
|
||||
|
||||
def on_render(self, context, template):
|
||||
fallback = self.slots.fallback
|
||||
|
||||
if fallback is None:
|
||||
raise ValueError("fallback slot is required")
|
||||
|
||||
html, error = yield template.render(context)
|
||||
|
||||
if error is not None:
|
||||
return fallback()
|
||||
else:
|
||||
return html
|
||||
```
|
||||
|
||||
### `on_render_after`
|
||||
|
||||
```py
|
||||
def on_render_after(
|
||||
self: Component,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
result: Optional[str | SafeString],
|
||||
error: Optional[Exception],
|
||||
) -> Union[str, SafeString, None]:
|
||||
```
|
||||
|
||||
[`on_render_after()`](../../../reference/api#django_components.Component.on_render_after) runs when the component was fully rendered,
|
||||
including all its children.
|
||||
|
||||
It receives the same arguments as [`on_render_before()`](#on_render_before),
|
||||
plus the outcome of the rendering:
|
||||
|
||||
- `result`: The rendered output of the component. `None` if the rendering failed.
|
||||
- `error`: The error that occurred during the rendering, or `None` if the rendering succeeded.
|
||||
|
||||
[`on_render_after()`](../../../reference/api#django_components.Component.on_render_after) behaves the same way
|
||||
as the second part of [`on_render()`](#on_render) (after the `yield`).
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render_after(self, context, template, result, error):
|
||||
if error is None:
|
||||
# The rendering succeeded
|
||||
return result
|
||||
else:
|
||||
# The rendering failed
|
||||
print(f"Error: {error}")
|
||||
```
|
||||
|
||||
Same as [`on_render()`](#on_render),
|
||||
you can return a new HTML, raise a new exception, or return nothing:
|
||||
|
||||
1. Return a new HTML
|
||||
|
||||
The new HTML will be used as the final output.
|
||||
|
||||
If the original template raised an error, it will be ignored.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render_after(self, context, template, result, error):
|
||||
return "NEW HTML"
|
||||
```
|
||||
|
||||
2. Raise a new exception
|
||||
|
||||
The new exception is what will bubble up from the component.
|
||||
|
||||
The original HTML and original error will be ignored.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render_after(self, context, template, result, error):
|
||||
raise Exception("Error message")
|
||||
```
|
||||
|
||||
3. Return nothing (or `None`) to handle the result as usual
|
||||
|
||||
If you don't raise an exception, and neither return a new HTML,
|
||||
then original HTML / error will be used:
|
||||
|
||||
- If rendering succeeded, the original HTML will be used as the final output.
|
||||
- If rendering failed, the original error will be propagated.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render_after(self, context, template, result, error):
|
||||
if error is not None:
|
||||
# The rendering failed
|
||||
print(f"Error: {error}")
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
|
|
@ -118,6 +118,92 @@ class Button(Component):
|
|||
</button>
|
||||
```
|
||||
|
||||
### Dynamic templates
|
||||
|
||||
Each component has only a single template associated with it.
|
||||
|
||||
However, whether it's for A/B testing or for preserving public API
|
||||
when sharing your components, sometimes you may need to render different templates
|
||||
based on the input to your component.
|
||||
|
||||
You can use [`Component.on_render()`](../../reference/api.md#django_components.Component.on_render)
|
||||
to dynamically override what template gets rendered.
|
||||
|
||||
By default, the component's template is rendered as-is.
|
||||
|
||||
```py
|
||||
class Table(Component):
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
if template is not None:
|
||||
return template.render(context)
|
||||
```
|
||||
|
||||
If you want to render a different template in its place,
|
||||
we recommended you to:
|
||||
|
||||
1. Wrap the substitute templates as new Components
|
||||
2. Then render those Components inside [`Component.on_render()`](../../reference/api.md#django_components.Component.on_render):
|
||||
|
||||
```py
|
||||
class TableNew(Component):
|
||||
template_file = "table_new.html"
|
||||
|
||||
class TableOld(Component):
|
||||
template_file = "table_old.html"
|
||||
|
||||
class Table(Component):
|
||||
def on_render(self, context, template):
|
||||
if self.kwargs.get("feat_table_new_ui"):
|
||||
return TableNew.render(
|
||||
args=self.args,
|
||||
kwargs=self.kwargs,
|
||||
slots=self.slots,
|
||||
)
|
||||
else:
|
||||
return TableOld.render(
|
||||
args=self.args,
|
||||
kwargs=self.kwargs,
|
||||
slots=self.slots,
|
||||
)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
If you do not wrap the templates as Components,
|
||||
there is a risk that some [extensions](../../advanced/extensions) will not work as expected.
|
||||
|
||||
```py
|
||||
new_template = Template("""
|
||||
{% load django_components %}
|
||||
<div>
|
||||
{% slot "content" %}
|
||||
Other template
|
||||
{% endslot %}
|
||||
</div>
|
||||
""")
|
||||
|
||||
class Table(Component):
|
||||
def on_render(self, context, template):
|
||||
if self.kwargs.get("feat_table_new_ui"):
|
||||
return new_template.render(context)
|
||||
else:
|
||||
return template.render(context)
|
||||
```
|
||||
|
||||
### Template-less components
|
||||
|
||||
Since you can use [`Component.on_render()`](../../reference/api.md#django_components.Component.on_render)
|
||||
to render *other* components, there is no need to define a template for the component.
|
||||
|
||||
So even an empty component like this is valid:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
pass
|
||||
```
|
||||
|
||||
These "template-less" components can be useful as base classes for other components, or as mixins.
|
||||
|
||||
### HTML processing
|
||||
|
||||
Django Components expects the rendered template to be a valid HTML. This is needed to enable features like [CSS / JS variables](../html_js_css_variables).
|
||||
|
|
|
@ -205,7 +205,7 @@ and [`self.slots`](../../../reference/api/#django_components.Component.slots) pr
|
|||
|
||||
```py
|
||||
class ProfileCard(Component):
|
||||
def on_render_before(self, *args, **kwargs):
|
||||
def on_render_before(self, context: Context, template: Optional[Template]):
|
||||
# Access inputs via self.args, self.kwargs, self.slots
|
||||
self.args[0]
|
||||
self.kwargs.get("show_details", False)
|
||||
|
|
|
@ -10,6 +10,7 @@ Render API is available inside these [`Component`](../../../reference/api#django
|
|||
- [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data)
|
||||
- [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
|
||||
- [`on_render_before()`](../../../reference/api#django_components.Component.on_render_before)
|
||||
- [`on_render()`](../../../reference/api#django_components.Component.on_render)
|
||||
- [`on_render_after()`](../../../reference/api#django_components.Component.on_render_after)
|
||||
|
||||
Example:
|
||||
|
@ -89,7 +90,7 @@ class Table(Component):
|
|||
page: int
|
||||
per_page: int
|
||||
|
||||
def on_render_before(self, context: Context, template: Template) -> None:
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
assert self.args.page == 123
|
||||
assert self.args.per_page == 10
|
||||
|
||||
|
@ -104,7 +105,7 @@ Without `Args` class:
|
|||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
def on_render_before(self, context: Context, template: Template) -> None:
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
assert self.args[0] == 123
|
||||
assert self.args[1] == 10
|
||||
```
|
||||
|
@ -131,7 +132,7 @@ class Table(Component):
|
|||
page: int
|
||||
per_page: int
|
||||
|
||||
def on_render_before(self, context: Context, template: Template) -> None:
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
assert self.kwargs.page == 123
|
||||
assert self.kwargs.per_page == 10
|
||||
|
||||
|
@ -146,7 +147,7 @@ Without `Kwargs` class:
|
|||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
def on_render_before(self, context: Context, template: Template) -> None:
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
assert self.kwargs["page"] == 123
|
||||
assert self.kwargs["per_page"] == 10
|
||||
```
|
||||
|
@ -173,7 +174,7 @@ class Table(Component):
|
|||
header: SlotInput
|
||||
footer: SlotInput
|
||||
|
||||
def on_render_before(self, context: Context, template: Template) -> None:
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
assert isinstance(self.slots.header, Slot)
|
||||
assert isinstance(self.slots.footer, Slot)
|
||||
|
||||
|
@ -191,7 +192,7 @@ Without `Slots` class:
|
|||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
class Table(Component):
|
||||
def on_render_before(self, context: Context, template: Template) -> None:
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
assert isinstance(self.slots["header"], Slot)
|
||||
assert isinstance(self.slots["footer"], Slot)
|
||||
```
|
||||
|
|
|
@ -305,8 +305,8 @@ class Calendar(Component):
|
|||
"my_slot": content,
|
||||
}
|
||||
|
||||
# But in other methods you can still access the slots with `Component.slots`
|
||||
def on_render_before(self, *args, **kwargs):
|
||||
# In other methods you can still access the slots with `Component.slots`
|
||||
def on_render_before(self, context, template):
|
||||
if "my_slot" in self.slots:
|
||||
# Do something
|
||||
```
|
||||
|
|
|
@ -135,13 +135,13 @@ Thus, you can check where a slot was filled from by printing it out:
|
|||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
def on_render_before(self, *args, **kwargs):
|
||||
def on_render_before(self, context: Context, template: Optional[Template]):
|
||||
print(self.slots)
|
||||
```
|
||||
|
||||
might print:
|
||||
|
||||
```txt
|
||||
```python
|
||||
{
|
||||
'content': <Slot component_name='layout' slot_name='content'>,
|
||||
'header': <Slot component_name='my_page' slot_name='header'>,
|
||||
|
|
|
@ -91,6 +91,10 @@
|
|||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.OnRenderGenerator
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ProvideNode
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
|
|
@ -126,7 +126,8 @@ name | type | description
|
|||
`component` | [`Component`](../api#django_components.Component) | The Component instance that is being rendered
|
||||
`component_cls` | [`Type[Component]`](../api#django_components.Component) | The Component class
|
||||
`component_id` | `str` | The unique identifier for this component instance
|
||||
`result` | `str` | The rendered component
|
||||
`error` | `Optional[Exception]` | The error that occurred during rendering, or `None` if rendering was successful
|
||||
`result` | `Optional[str]` | The rendered component, or `None` if rendering failed
|
||||
|
||||
::: django_components.extension.ComponentExtension.on_component_unregistered
|
||||
options:
|
||||
|
|
|
@ -20,7 +20,7 @@ Import as
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1022" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1010" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
@ -43,7 +43,7 @@ If you insert this tag multiple times, ALL CSS links will be duplicately inserte
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1044" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1032" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L3350" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L3685" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
# Template variables
|
||||
|
||||
Here is a list of all variables that are automatically available from inside the component's
|
||||
template and in [`on_render_before` / `on_render_after`](../concepts/advanced/hooks.md#available-hooks)
|
||||
hooks.
|
||||
template:
|
||||
|
||||
|
||||
::: django_components.component.ComponentVars.args
|
||||
|
|
3
docs/templates/reference_templatevars.md
vendored
3
docs/templates/reference_templatevars.md
vendored
|
@ -1,5 +1,4 @@
|
|||
# Template variables
|
||||
|
||||
Here is a list of all variables that are automatically available from inside the component's
|
||||
template and in [`on_render_before` / `on_render_after`](../concepts/advanced/hooks.md#available-hooks)
|
||||
hooks.
|
||||
template:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue