mirror of
https://github.com/django-components/django-components.git
synced 2025-11-01 15:44:12 +00:00
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
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:
parent
3e837e20c6
commit
eee3910b54
6 changed files with 277 additions and 116 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue