Lifecycle hooks
New in version 0.96
Intercept the rendering lifecycle with Component hooks.
Unlike the extension hooks, these are defined directly on the Component
class.
Available hooks¤
on_render_before
¤
Component.on_render_before
runs just before the component's template is rendered.
It is called for every component, including nested ones, as part of the component render lifecycle.
It receives the Context and the Template as arguments.
The template
argument is None
if the component has no template.
Example:
You can use this hook to access the context or the template:
from django.template import Context, Template
from django_components import Component
class MyTable(Component):
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
# Insert value into the Context
context["from_on_before"] = ":)"
assert isinstance(template, Template)
Warning
If you want to pass data to the template, prefer using get_template_data()
instead of this hook.
Warning
Do NOT modify the template in this hook. The template is reused across renders.
on_render
¤
New in version 0.140
def on_render(
self: Component,
context: Context,
template: Optional[Template],
) -> Union[str, SafeString, OnRenderGenerator, None]:
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 with the given Context.
class MyTable(Component):
def on_render(self, context, template):
if template:
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 component
- Render a template
- Return a string or SafeString
class MyTable(Component):
def on_render(self, context, template):
# 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()
as a router, rendering other components based on the parent component's arguments:
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()
as:
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.
This is not a problem when you return the result directly as above. Django-components will take care of rendering the child components.
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)
class MyTable(Component):
def on_render(self, context, template):
html, error = yield lambda: template.render(context)
if error is None:
# The rendering succeeded
return html
else:
# The rendering failed
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:
-
Return new HTML
The new HTML will be used as the final output.
If the original template raised an error, the original error will be ignored.
-
Raise new exception
The new exception is what will bubble up from the component.
The original HTML and original error will be ignored.
-
No change - Return nothing or
None
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:
Multiple yields¤
You can yield multiple times within the same on_render()
method. This is useful for complex rendering scenarios:
class MyTable(Component):
def on_render(self, context, template):
# First yield
with context.push({"mode": "header"}):
header_html, header_error = yield lambda: template.render(context)
# Second yield
with context.push({"mode": "body"}):
body_html, body_error = yield lambda: template.render(context)
# Third yield
footer_html, footer_error = yield "Footer content"
# Process all
if header_error or body_error or footer_error:
return "Error occurred during rendering"
return f"{header_html}\n{body_html}\n{footer_html}"
Each yield operation is independent and returns its own (html, error)
tuple, allowing you to handle each rendering result separately.
Example: ErrorBoundary¤
on_render()
can be used to implement React's ErrorBoundary.
That is, a component that catches errors in nested components and displays a fallback UI instead:
{% component "error_boundary" %}
{% fill "default" %}
{% component "nested_component" %}
{% endfill %}
{% fill "fallback" %}
Sorry, something went wrong.
{% endfill %}
{% endcomponent %}
To implement this, we render the fallback slot in on_render()
and return it if an error occured:
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
template: types.django_html = """
{% if not error %}
{% slot "default" default / %}
{% else %}
{% slot "fallback" error=error / %}
{% endif %}
"""
def on_render(
self,
context: Context,
template: Template,
) -> OnRenderGenerator:
fallback_slot = self.slots.default
result, error = yield lambda: template.render(context)
# No error, return the original result
if error is None:
return None
# Error, return the fallback
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 mark_safe("<pre>An error occurred</pre>")
on_render_after
¤
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()
runs when the component was fully rendered, including all its children.
It receives the same arguments as 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, orNone
if the rendering succeeded.
on_render_after()
behaves the same way as the second part of on_render()
(after the yield
).
class MyTable(Component):
def on_render_after(self, context, template, result, error):
# If rendering succeeded, keep the original result
# Otherwise, print the error
if error is not None:
print(f"Error: {error}")
Same as on_render()
, you can return a new HTML, raise a new exception, or return nothing:
-
Return new HTML
The new HTML will be used as the final output.
If the original template raised an error, the original error will be ignored.
-
Raise new exception
The new exception is what will bubble up from the component.
The original HTML and original error will be ignored.
-
No change - Return nothing or
None
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:
Example: Tabs¤
You can use hooks together with 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.