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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue