Recursion
Use this pattern to draw nested data, such as a file tree, comment threads, or a menu. TreeNode renders one node's label, then renders <c-TreeNode> again for each child.
"""A self-rendering tree, used as a live example in the docs.
TreeNode renders one node's label and then renders itself for each
child, so one component draws the whole nested structure.
"""
from __future__ import annotations
from typing import Any
# Both come from typing_extensions so a NotRequired key is optional
# on Python 3.10 too.
from typing_extensions import NotRequired, TypedDict
from citry import Component
class TreeNodeData(TypedDict):
label: str
children: NotRequired[list[TreeNodeData]]
class TreeNode(Component):
"""One node of a tree: its label, then itself for each child.
The recursion stops on its own: a leaf has no children, so the
``c-if`` skips the child list and no further TreeNode is rendered.
"""
class Kwargs:
node: TreeNodeData
class Slots:
pass
def template_data(
self,
kwargs: Kwargs,
slots: Slots,
) -> dict[str, Any]:
node = kwargs.node
children = node.get("children", [])
has_children = len(children) > 0
# A down arrow marks a node with children; a dot marks a
# leaf.
icon = "▾" if has_children else "•"
return {
"label": node["label"],
"children": children,
"has_children": has_children,
"icon": icon,
}
template = """
<div class="tree-node">
<span class="tree-node__label">
<span class="tree-node__icon">{{ icon }}</span>
{{ label }}
</span>
<div c-if="has_children" class="tree-node__children">
<c-TreeNode c-for="child in children" c-node="child" />
</div>
</div>
"""
css = """
.tree-node {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.95rem;
}
.tree-node__label {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.15rem 0;
color: #1f2328;
}
.tree-node__icon {
color: #6e7781;
}
.tree-node__children {
margin-left: 1.1rem;
padding-left: 0.75rem;
border-left: 1px solid #d0d7de;
}
"""
"""Standalone page that renders the recursion example."""
from typing import Any
from citry import Component
class RecursionPage(Component):
"""A full page showing a nested tree drawn by TreeNode."""
class Kwargs:
pass
class Slots:
pass
def template_data(
self,
kwargs: Kwargs,
slots: Slots,
) -> dict[str, Any]:
# A small folder tree, three levels deep, passed in one piece.
tree = {
"label": "project",
"children": [
{
"label": "src",
"children": [
{"label": "app.py"},
{"label": "utils.py"},
],
},
{
"label": "docs",
"children": [
{
"label": "guide",
"children": [
{"label": "intro.md"},
{"label": "advanced.md"},
],
},
],
},
{"label": "README.md"},
],
}
return {"tree": tree}
template = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Recursion example</title>
<c-css />
<style>
body {
margin: 0;
padding: 1.5rem;
font-family: system-ui, sans-serif;
}
</style>
</head>
<body>
<c-TreeNode c-node="tree" />
<c-js />
</body>
</html>
"""
The recursion stops on its own. A leaf has no children, so the c-if on the child list is false and no further TreeNode renders. Your own data needs the same end.
Stop endless recursion
A node that appears again inside its own children never reaches a leaf. Citry stops such a render with a RecursionError once components are nested more than 2,000 levels deep. The message starts with the component that repeats:
Component TreeNode is nested more than 2000 components deep
(TreeNode > TreeNode > ... > TreeNode > TreeNode > TreeNode).
<c-error-fallback> does not catch this error, so the whole render fails. Fix the data so every branch ends in a leaf. If your page really nests deeper than 2,000 components, raise the limit with the max_component_depth setting:
app = Citry(max_component_depth=5_000)