Components
When the same piece of UI appears on several pages, such as a card, a button, or a page header, write it once as a component and reuse it. A Citry component is a Python class based on Component. It takes inputs, and its template turns them into HTML.
Write a component
Set template to the markup the component produces:
from citry import Component
class Welcome(Component):
template = """
<section class="welcome">
<h1>Welcome!</h1>
</section>
"""
welcome = Welcome()
html = str(welcome)
str(...) renders the component and returns the HTML as a string.
Pass values in
List the keyword arguments a component accepts in a nested Kwargs class. The template can read each one by its name:
from citry import Component
class Welcome(Component):
class Kwargs:
name: str
message_count: int = 0
template = """
<section class="welcome">
<h1>Welcome, {{ name }}!</h1>
<p>You have {{ message_count }} new messages.</p>
</section>
"""
welcome = Welcome(name="Ada", message_count=3)
html = str(welcome)
name is required, and message_count defaults to 0. Citry rejects a missing required value or a name the class does not list. Inputs and validation covers defaults, slots, and type checking.
A component without Kwargs also passes its keyword arguments to the template, but accepts any name.
slots is a reserved keyword argument
Modal(slots={...}) passes content for the component's slots, not an input named slots. See Slots.
Compute template values
When the template needs a value that is not an input as written, override template_data() and return the values the template should see. This component turns a list of messages into a count:
from citry import Component
class InboxSummary(Component):
class Kwargs:
name: str
messages: list[str]
def template_data(
self,
kwargs: Kwargs,
slots,
) -> dict[str, object]:
return {
"name": kwargs.name,
"message_count": len(kwargs.messages),
}
template = """
<section class="inbox-summary">
<h2>{{ name }}'s inbox</h2>
<p>{{ message_count }} new messages</p>
</section>
"""
Once you override template_data(), the template gets the values it returns in place of the inputs. Here the template cannot read messages.
With a Kwargs class, read inputs as attributes, such as kwargs.name. Without one, kwargs is a dictionary, so read kwargs["name"].
Nest components
Inside a template, a <c-*> tag renders another component. Write the class name after c-, so Welcome is <c-Welcome>:
from citry import Component
class ProfilePage(Component):
class Kwargs:
user_name: str
template = """
<main>
<c-Welcome c-name="user_name" />
</main>
"""
A plain attribute such as name="Ada" passes a string. The c- prefix in c-name="user_name" passes the value of a Python expression instead.
In a component tag, letter case does not matter after c-, and you can also write a class name of several words with hyphens between the words. <c-TaskCard>, <c-taskcard>, and <c-task-card> all render TaskCard. Registration explains how Citry finds the class for a tag.
You can also build a component in Python and pass it in as a value. The template inserts it with {{ ... }}:
from citry import Component
class PageFrame(Component):
class Kwargs:
body: object
template = """
<main>{{ body }}</main>
"""
welcome = Welcome(name="Ada")
page = PageFrame(body=welcome)
html = str(page)
To let the outer template supply markup that the component places in its own layout, use Slots.
Use a template file
An inline template suits short markup. For longer markup, set template_file instead:
from citry import Component
class AccountPanel(Component):
template_file = "account_panel.html"
A relative path is looked up first in the directory of the file that defines the class, then in the template directories configured on the component's Citry instance. An absolute path is used as written.
Set template or template_file, not both. Setting both raises an error when Python defines the class.
Build now, render later
Calling a component class, as in Welcome(name="Ada"), does not render it. It returns a CitryElement: a description of what to render, holding the class, its inputs, and its slot content. Rendering happens when you call str(...), or call render() and then serialize():
welcome = Welcome(name="Ada", message_count=3)
html = welcome.render().serialize()
assert str(welcome) == html
Because an element is only a description, you can build a whole page out of elements first and render it once at the end. You can also render the same element several times, or insert it in several places. Rendering covers the steps in detail.
Less common setups
Leave out the template
A component does not need a template. Without one it renders nothing by default. This suits a base class that other components extend, or a component whose on_render() hook produces the complete output.
Render faster
For small display-only components that do not need hooks or their own instance, Simple components render with less overhead.