Build your own components
Goal
Package a piece of page you keep rebuilding into something you can use like a built-in component. There are three levels, and the first one covers almost everyone.
Before you start
You can build pages comfortably and have repeated yourself at least once; repetition is what components cure. The levels escalate in power and difficulty together, so start at level one and climb only when a level genuinely cannot express what you want.
Level 1: a function that returns components
A "component" can be nothing more than a function returning the content you keep writing. It composes, takes parameters, and needs no new concepts:
from drafter import *
from dataclasses import dataclass
@dataclass
class State:
pass
def PetCard(name: str, species: str, motto: str) -> PageContent:
return Div(
Header(name, 3),
Emphasis(species),
Paragraph('"' + motto + '"'),
style_border="2px solid #888",
style_padding="8px",
style_margin="8px"
)
@route
def index(state: State) -> Page:
return Page(state, [
Header("The Residents"),
PetCard("Ada", "corgi", "Every walk is the best walk."),
PetCard("Captain", "grey cat", "I was here first."),
PetCard("Domino", "black cat", "You saw nothing.")
])
start_server(State())
Name these functions like components (PetCard, not
make_pet_card) and keep them pure: parameters in, content out,
no state mutation. This level is enough for cards, banners,
navigation bars, and any repeated furniture.
Level 2: RawHTML for markup no component makes
When the structure you need has no component equivalent, a function can wrap RawHTML. You gain arbitrary markup and lose Drafter's safety, so the safety rule applies in full: only your own text, never a visitor's, goes into the markup.
def Badge(label: str) -> PageContent:
return RawHTML(
'<span style="border-radius: 9px; background: gold; '
'padding: 2px 8px;">' + label + "</span>"
)
(Calling Badge(user_typed_text) would break the rule; even at
level 2, keep visitor text out.)
Level 3: a real Component class
Real components subclass Component and describe their
rendering with the RenderPlan API, the same machinery the
built-ins use. This buys equality for tests, styling keywords,
and multi-element rendering, at the cost of touching Drafter's
internals:
from drafter.components import Component, PageContent
from drafter.components.page_content import ComponentArgument, RenderPlan
@dataclass(repr=False)
class AdBox(Component):
tag = "div"
product: str
text: str
ARGUMENTS = [
ComponentArgument("product", is_content=True),
ComponentArgument("text", is_content=True),
]
def __init__(self, product: str, text: str, **kwargs):
self.product = product
self.text = text
self.extra_settings = kwargs
def get_children(self, context):
return [
RenderPlan(kind="tag", tag_name="hr"),
RenderPlan(kind="tag", tag_name="section", children=[
RenderPlan(kind="tag", tag_name="h3",
children=[self.product]),
RenderPlan(kind="tag", tag_name="p",
children=[self.text]),
]),
]
The library's own SelectBox, CheckBox, and Table are the
reference implementations to read next. Beyond this lies level
four, interactive custom elements with a TypeScript half (how
Timer and Map work), which is
developer-docs territory.
Common problems
- The level-1 function mutates state: then it is secretly a route helper, not a component. Pass values in; return content out.
- Tests cannot match the level-2 markup:
assert_hassees aRawHTMLblob, matched only by its exact string. Level 1 components test naturally; another reason to prefer them. - A level-3 field leaks into the rendering (a stray
attribute or inline style): every dataclass field becomes an
attribute unless handled; declare
ARGUMENTSfully, and useRENAME_ATTRS = {"field": ""}to suppress a field that is configuration rather than markup. - It feels like fighting the framework: check whether composition (level 1) around existing components expresses the idea; it usually can.
Related
- Embed HTML: the safety rules level 2 inherits.
- Developer documentation: architecture and internals for level 3 and beyond.