refactor: use lamba with yield in Component.on_render() (#1428)
Some checks are pending
Docs - build & deploy / docs (push) Waiting to run
Run tests / build (ubuntu-latest, 3.10) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.11) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.12) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.8) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.9) (push) Waiting to run
Run tests / build (windows-latest, 3.10) (push) Waiting to run
Run tests / build (windows-latest, 3.11) (push) Waiting to run
Run tests / build (windows-latest, 3.12) (push) Waiting to run
Run tests / build (windows-latest, 3.13) (push) Waiting to run
Run tests / build (windows-latest, 3.8) (push) Waiting to run
Run tests / build (windows-latest, 3.9) (push) Waiting to run
Run tests / test_docs (3.13) (push) Waiting to run
Run tests / test_sampleproject (3.13) (push) Waiting to run

This commit is contained in:
Juro Oravec 2025-10-03 18:19:21 +02:00 committed by GitHub
parent 3e837e20c6
commit eee3910b54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 277 additions and 116 deletions

View file

@ -54,9 +54,6 @@ class MyTable(Component):
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_
@ -86,9 +83,7 @@ with the given
```py
class MyTable(Component):
def on_render(self, context, template):
if template is None:
return None
else:
if template:
return template.render(context)
```
@ -98,14 +93,26 @@ The `template` argument is `None` if the component has no template.
To change what gets rendered, you can:
- Render a different template
- Render a component
- Return a different string or SafeString
- Render a template
- Return a string or SafeString
```py
class MyTable(Component):
def on_render(self, context, template):
return "Hello"
# Return a string
return "<p>Hello</p>"
# Render a component
return MyOtherTable.render(
args=self.args,
kwargs=self.kwargs,
slots=self.slots,
context=context,
)
# Render a template
return get_template("my_other_table.html").render(context)
```
You can also use [`on_render()`](../../../reference/api#django_components.Component.on_render) as a router,
@ -145,14 +152,19 @@ 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 is not a problem when you return the result directly as above. Django-components will take care of rendering the child components.
This will return a tuple of (rendered HTML, error). The error is `None` if the rendering succeeded.
But if you want to access the final output, you must `yield` the result instead of returning it.
Yielding the result will return a tuple of `(rendered_html, error)`:
- On success, the error is `None` - `(string, None)`
- On failure, the rendered HTML is `None` - `(None, Exception)`
```py
class MyTable(Component):
def on_render(self, context, template):
html, error = yield template.render(context)
html, error = yield lambda: template.render(context)
if error is None:
# The rendering succeeded
@ -162,23 +174,34 @@ class MyTable(Component):
print(f"Error: {error}")
```
!!! warning
Notice that we actually yield a **lambda function** instead of the result itself.
This is because calling `template.render(context)` may raise an exception.
When you wrap the result in a lambda function, and the rendering fails,
the error will be yielded back in the `(None, Exception)` tuple.
At this point you can do 3 things:
1. Return a new HTML
1. Return new HTML
The new HTML will be used as the final output.
If the original template raised an error, it will be ignored.
If the original template raised an error, the original error will be ignored.
```py
class MyTable(Component):
def on_render(self, context, template):
html, error = yield template.render(context)
html, error = yield lambda: template.render(context)
return "NEW HTML"
# Fallback if rendering failed
# Otherwise, we keep the original HTML
if error is not None:
return "FALLBACK HTML"
```
2. Raise a new exception
2. Raise new exception
The new exception is what will bubble up from the component.
@ -187,48 +210,55 @@ At this point you can do 3 things:
```py
class MyTable(Component):
def on_render(self, context, template):
html, error = yield template.render(context)
html, error = yield lambda: template.render(context)
raise Exception("Error message")
# Override the original error
# Otherwise, we keep the original HTML
if error is not None:
raise Exception("My new error") from error
```
3. Return nothing (or `None`) to handle the result as usual
3. No change - Return nothing or `None`
If you don't raise an exception, and neither return a new HTML,
then original HTML / error will be used:
If you neither raise an exception, nor return a new HTML,
then the 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.
This can be useful for side effects like tracking the errors that occurred during the rendering:
```py
from myapp.metrics import track_rendering_error
class MyTable(Component):
def on_render(self, context, template):
html, error = yield template.render(context)
html, error = yield lambda: template.render(context)
# Track how many times the rendering failed
if error is not None:
# The rendering failed
print(f"Error: {error}")
track_rendering_error(error)
```
#### Multiple yields
You can yield multiple times within the same `on_render` method. This is useful for complex rendering scenarios where you need to render different templates or handle multiple rendering operations:
You can yield multiple times within the same [`on_render()`](../../../reference/api#django_components.Component.on_render) method. This is useful for complex rendering scenarios:
```py
class MyTable(Component):
def on_render(self, context, template):
# First yield - render with one context
# First yield
with context.push({"mode": "header"}):
header_html, header_error = yield template.render(context)
header_html, header_error = yield lambda: template.render(context)
# Second yield - render with different context
# Second yield
with context.push({"mode": "body"}):
body_html, body_error = yield template.render(context)
body_html, body_error = yield lambda: template.render(context)
# Third yield - render a string directly
# Third yield
footer_html, footer_error = yield "Footer content"
# Process all results and return final output
# Process all
if header_error or body_error or footer_error:
return "Error occurred during rendering"
@ -246,7 +276,7 @@ That is, a component that catches errors in nested components and displays a fal
```django
{% component "error_boundary" %}
{% fill "content" %}
{% fill "default" %}
{% component "nested_component" %}
{% endfill %}
{% fill "fallback" %}
@ -259,10 +289,13 @@ To implement this, we render the fallback slot in [`on_render()`](../../../refer
and return it if an error occured:
```djc_py
class ErrorFallback(Component):
class Kwargs(NamedTuple):
fallback: Optional[str] = None
from typing import NamedTuple, Optional
from django.template import Context, Template
from django.utils.safestring import mark_safe
from django_components import Component, OnRenderGenerator, SlotInput, types
class ErrorFallback(Component):
class Slots(NamedTuple):
default: Optional[SlotInput] = None
fallback: Optional[SlotInput] = None
@ -280,30 +313,22 @@ class ErrorFallback(Component):
context: Context,
template: Template,
) -> OnRenderGenerator:
fallback_kwarg = cast(ErrorFallback.Kwargs, self.kwargs).fallback
fallback_slot = cast(ErrorFallback.Slots, self.slots).default
fallback_slot = self.slots.default
if fallback_kwarg is not None and fallback_slot is not None:
raise TemplateSyntaxError(
"The 'fallback' argument and slot cannot both be provided. Please provide only one.",
)
result, error = yield lambda: template.render(context)
result, error = yield template.render(context)
# No error, return the result
# No error, return the original result
if error is None:
return result
return None
# Error, return the fallback
if fallback_kwarg is not None:
return fallback_kwarg
elif fallback_slot is not None:
# Render the template second time, this time with the error
# So that we render the fallback slot with proper access to the outer context and whatnot.
if fallback_slot is not None:
# Render the template second time, this time rendering
# the fallback branch
with context.push({"error": error}):
return template.render(context)
else:
return ""
return mark_safe("<pre>An error occurred</pre>")
```
### `on_render_after`
@ -333,30 +358,31 @@ 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
# If rendering succeeded, keep the original result
# Otherwise, print the error
if error is not None:
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
1. Return new HTML
The new HTML will be used as the final output.
If the original template raised an error, it will be ignored.
If the original template raised an error, the original error will be ignored.
```py
class MyTable(Component):
def on_render_after(self, context, template, result, error):
return "NEW HTML"
# Fallback if rendering failed
# Otherwise, we keep the original HTML
if error is not None:
return "FALLBACK HTML"
```
2. Raise a new exception
2. Raise new exception
The new exception is what will bubble up from the component.
@ -365,26 +391,33 @@ you can return a new HTML, raise a new exception, or return nothing:
```py
class MyTable(Component):
def on_render_after(self, context, template, result, error):
raise Exception("Error message")
# Override the original error
# Otherwise, we keep the original HTML
if error is not None:
raise Exception("My new error") from error
```
3. Return nothing (or `None`) to handle the result as usual
3. No change - Return nothing or `None`
If you don't raise an exception, and neither return a new HTML,
then original HTML / error will be used:
If you neither raise an exception, nor return a new HTML,
then the 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.
This can be useful for side effects like tracking the errors that occurred during the rendering:
```py
from myapp.metrics import track_rendering_error
class MyTable(Component):
def on_render_after(self, context, template, result, error):
# Track how many times the rendering failed
if error is not None:
# The rendering failed
print(f"Error: {error}")
track_rendering_error(error)
```
## Example
## Example: Tabs
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.