Component hooks
Sometimes a component needs Python logic that its template cannot express: show a placeholder instead of the template when there is no data, turn an error into a friendly message, or add an attribute to the script tags it brings. A hook is a method that Citry calls at a fixed point while it renders the component, so your code can change the result.
A component has two hooks:
on_render()replaces the component's output, or reacts after it renders.on_dependencies()changes the script and style tags the component adds to the page.
To change every component in an application at once, write an Extension instead.
on_render() output
Citry calls on_render() after it prepares the component's data and before it renders the template. Return None to render the template as usual. Return anything else to use it as the component's whole output.
This table shows a message instead of an empty table:
from citry import Component, Markup
class Table(Component):
class Kwargs:
rows: list[str] | None = None
def template_data(self, kwargs: Kwargs, slots):
return {"rows": kwargs.rows or []}
def on_render(self):
if not self.kwargs.rows:
return Markup("<p>No data yet</p>")
return None
template = """
<table>
<tr c-for="row in rows">
<td>{{ row }}</td>
</tr>
</table>
"""
Table() shows the message. Table(rows=["Ada", "Alan"]) renders the table.
on_render() can return:
- a plain
str, which the page shows as text; - a string of HTML wrapped in
Markup, as above; - a component, such as
Message(text="Hello"); - a
CitryRenderthat was already rendered; - a
Slot, which Citry renders without data; - a
ComponentLike.
Because None means "render the template", return "" to show nothing.
A plain str is text, the same as a value in {{ }}. Citry escapes it, so a string of HTML shows its tags as characters on the page:
def on_render(self):
if not self.kwargs.rows:
# Wrong: the page shows "<p>No data yet</p>" as text.
return "<p>No data yet</p>"
return None
Wrap the HTML in Markup to insert it as HTML, or return a component:
def on_render(self):
if not self.kwargs.rows:
# Right: Markup marks the string as HTML.
return Markup("<p>No data yet</p>")
return None
Citry does not escape Markup
Citry inserts the Markup you return from on_render() as HTML, without escaping. Never build it from request data. Return request data as a plain str, or put it in a template or a component input, where Citry escapes it.
The hook can read self.kwargs, self.slots, self.parent, and self.inject(). To pass values to the template, use template_data() instead.
Prefer c-if and c-for in the template
The Table above only shows how the hook works. When the output depends only on the data, branch in the template with c-if and c-else, or loop with c-for, as in <p c-if="not rows">No data yet</p>. The template is easier to read, and citry check reports mistakes in it, such as an unknown name.
A component that defines on_render() also cannot be a simple component, which skips the per-component setup: with simple = True the class raises TypeError when Python defines it, and with simple = "vue" the first render raises it. The form with yield also adds a small cost to every render.
yield to catch errors
For most error handling, wrap the part that may fail in the built-in <c-error-fallback> tag; see Error boundaries. Use on_render() when deciding what to show needs Python code.
Add yield to on_render(). Code before the yield runs before the template. The yield waits until the component and everything inside it has rendered, then gives you a (result, error) pair:
def on_render(self):
result, error = yield
if error is not None:
return Markup("<p>Could not load this section.</p>")
return None
When rendering succeeds, result is the rendered CitryRender and error is None. When it fails, result is None and error is the exception.
After the yield, you can:
- return new content to replace the result;
- raise an exception;
- return
Noneto keep a successful result, or to let the error continue to the components around this one.
on_dependencies() tags
on_dependencies() receives the script and style tags that one rendered component adds to the page. Those are its own js and css, the files in its Dependencies class, and the stylesheet that holds its css_data() values. Change the lists in place, or return a new (scripts, styles) pair. Return None to keep them as they are.
This component adds crossorigin to its external scripts:
from citry import Component
from citry.ext.dependencies import Script, Style
class Chart(Component):
class Dependencies:
js = ["https://cdn.example.com/chart.js"]
@classmethod
def on_dependencies(
cls,
scripts: list[Script],
styles: list[Style],
):
for script in scripts:
if script.url:
script.attrs["crossorigin"] = "anonymous"
return (scripts, styles)
template = """
<div class="chart"></div>
"""
The hook is a classmethod. Citry calls it when it turns the render into HTML, once for each time the component appears on the page.
Removing the component's own script stops its browser code from running. Remove an entry only when the same code reaches the page another way.
Less common cases
Cached renders
When component caching finds a stored result, Citry reuses it and does not run the data methods, the template, or on_render(). If the hook's result depends on something other than the component's inputs, make that value part of the cache key, or the stored result outlives the condition that produced it.
Avoid str(result)
Do not turn result into a string to look at the HTML or to add to it. For a component that runs in the browser, the string holds the whole browser app, Citry's runtime script included, not just the component's tags. A plain string you return also shows as text, so returned HTML must be wrapped in Markup.
A natural first attempt logs the HTML and adds a line under it:
def on_render(self):
result, error = yield
if error is not None:
return None
# Wrong: for a component that runs in the browser, the log line
# holds the whole browser app, runtime script included.
html = str(result)
logger.info("Rendered %s", html)
# Wrong: the new <p> lands after Citry's scripts, outside the
# part of the page that Vue updates.
return Markup(html + "<p>Updated today</p>")
Instead, put fixed content in the template and keep the result as it is:
import logging
from citry import Component
logger = logging.getLogger(__name__)
class Counter(Component):
def template_data(self, kwargs, slots):
return {"count": 0}
def on_render(self):
result, error = yield
if error is None:
# Log what the hook knows, not the HTML.
logger.info("Rendered %s", type(self).__name__)
# None keeps the result unchanged.
return None
template = """
<button @click="count += 1">{{ count }}</button>
<p>Updated today</p>
"""
To put the output inside another component, pass result to it, as in return Panel(body=result), or wrap the component where you use it, as in <c-Panel><c-Counter /></c-Panel>. Place result in one spot on the page: placing it in two raises an error when the page is turned into HTML. To read the HTML in a log or a test, turn the whole render into a string after it finishes:
counter = Counter()
html = str(counter)
assert "Updated today" in html
yield more than once
Instead of a bare yield, you can yield new content. Citry renders it and sends back a new (result, error) pair, so one hook can try several outputs in turn. Here Report draws a chart, falls back to a summary when the chart fails, and shows a message when the summary fails too:
from citry import Component, Markup
class Chart(Component):
class Kwargs:
points: list[int]
def template_data(self, kwargs: Kwargs, slots):
if not kwargs.points:
raise ValueError("no points to draw")
return {"top": max(kwargs.points)}
template = """
<svg class="chart">{{ top }}</svg>
"""
class ChartSummary(Component):
class Kwargs:
points: list[int]
def template_data(self, kwargs: Kwargs, slots):
return {"count": len(kwargs.points)}
template = """
<p>{{ count }} points</p>
"""
class Report(Component):
class Kwargs:
points: list[int]
def template_data(self, kwargs: Kwargs, slots):
return {"points": kwargs.points}
def on_render(self):
# First render the template, which draws the chart.
result, error = yield
if error is None:
return None
# The chart failed, so try the summary in its place.
summary = ChartSummary(points=self.kwargs.points)
result, error = yield summary
if error is None:
return None
return Markup("<p>The report is not available.</p>")
template = """
<c-Chart c-points="points" />
"""
Report(points=[3, 7]) renders the chart. Report(points=[]) makes Chart fail, so it renders <p>0 points</p> instead. If ChartSummary failed too, the hook would return the message. Returning None after a yield keeps whatever that yield rendered. The on_render() reference describes every step.
Duplicate tags
The hook runs before Citry removes tags that several components share. When two components add the same script, the first one wins, together with any attribute the hook added. Stylesheets follow stricter rules; see Order and duplicates.
Change page-wide tags
The hook sees only this component's tags. An extension's on_dependencies() hook sees the tags of every component on the page, and can also add scripts that run before all others. Citry adds its own browser runtime after that hook, so neither hook sees it. See Add scripts and styles.
Next steps
- Component JS and CSS adds code and styles to one component.
- Dependency files adds libraries and shared files.
- Place JavaScript and CSS chooses where the tags go in the page.
- Rendering explains how a component becomes HTML.