Components¶
You often have a snippet of templating that you’d like to re-use. Many existing templating systems have “macros” for this: units of templating that can be re-used and called from other templates.
The whole mechanism, though, is quite magical:
Where do the macros come from? Multiple layers of context magic and specially named directories provide the answer.
What macros are available at the cursor position I’m at in a template? It’s hard for an editor or IDE to predict and provide autocomplete.
What are the macros arguments and what is this template’s special syntax for providing them? And can my editor help on autocomplete or tell me when I got it wrong (or the macro changed its signature)?
How does my current scope interact with the macro’s scope, and where does it get other parts of its scope from?
tdom
, courtesy of htm.py
, makes this more Pythonic through the use of “components.”
Instead of some sorta-callable, a component is a normal Python callable—e.g., a function—with normal Python arguments and return values.
Simple Heading¶
Here is a component callable – a Heading
function – which returns a Node
:
def Heading():
"""The default heading."""
return html(t"<h1>My Title</h1>")
def main():
"""Main entry point."""
result = html(t"<{Heading} />")
return result
Simple Props¶
As expected, components can have props, passed in as what looks like HTML attributes.
Here we pass a title
as an argument to Heading
, using a simple HTML attribute string value:
def Heading(title):
"""Default heading."""
return html(t"<h1>{title}</h1>")
def main():
"""Main entry point."""
result = html(t'<{Heading} title="My Title" />')
return result
Children As Props¶
If your template has children inside the component element, your component can ask for them as an argument:
def Heading(title, children):
"""The default heading."""
return html(t"<h1>{title}</h1><div>{children}</div>")
def main():
"""Main entry point."""
result = html(t'<{Heading} title="My Title">Child<//>')
return result
children
is a keyword parameter that is available to components.
Note how the component closes with <//>
when it contains nested children, as opposed to the self-closing form in the first example.
Expressions as Prop Values¶
The “prop” can also be a Python symbol, using curly braces as the attribute value:
def Heading(title):
"""The default heading."""
return html(t"<h1>{title}</h1>")
def main():
"""Main entry point."""
result = html(t'<{Heading} title={"My Title"} />')
return result
Prop Values from Scope Variables¶
That prop value can also be an in-scope variable:
def Heading(title):
"""The default heading."""
return html(t"<h1>{title}</h1>")
this_title = "My Title"
def main():
"""Main entry point."""
result = html(t"<{Heading} title={this_title} />")
return result
Optional Props¶
Since this is typical function-argument stuff, you can have optional props through argument defaults:
def Heading(title="My Title"):
"""The default heading."""
return html(t"<h1>{title}</h1>")
def main():
"""Main entry point."""
result = html(t"<{Heading} />")
return result
Pass Component as Prop¶
Here’s a useful pattern: you can pass a component as a “prop” to another component.
This lets the caller – in this case, the result
line – do the driving:
def DefaultHeading():
"""The default heading."""
return html(t"<h1>Default Heading</h1>")
def Body(heading):
"""The body which renders the heading."""
return html(t"<body><{heading} /></body>")
def main():
"""Main entry point."""
result = html(t"<{Body} heading={DefaultHeading} />")
return result
Default Component for Prop¶
As a variation, let the caller do the driving but make the prop default to a default component if none was provided:
def DefaultHeading(): # pragma: nocover
"""The default heading."""
return html(t"<h1>Default Heading</h1>")
def OtherHeading():
"""Another heading used in another condition."""
return html(t"<h1>Other Heading</h1>")
def Body(heading):
"""Render the body with a heading based on which is passed in."""
return html(t"<body><{heading} /></body>")
def main():
"""Main entry point."""
result = html(t"<{Body} heading={OtherHeading}/>")
return result
Conditional Default¶
One final variation for passing a component as a prop… move the “default or passed-in” decision into the template itself:
def DefaultHeading():
"""The default heading."""
return html(t"<h1>Default Heading</h1>")
def OtherHeading():
"""Another heading used in another condition."""
return html(t"<h1>Other Heading</h1>")
def Body(heading):
"""Render the body with a heading based on which is passed in."""
return html(t"<body>{heading if heading else DefaultHeading}</body>")
def main():
"""Main entry point."""
result = html(t"<{Body} heading={OtherHeading}/>")
return result
Children as Prop¶
You can combine different props and arguments.
In this case, title
is a prop.
children
is another argument, but is provided automatically by tdom
.
def Heading(title, children):
"""The default heading."""
return html(t"<h1>{title}</h1><div>{children}</div>")
def main():
"""Main entry point."""
result = html(t'<{Heading} title="My Title">Child<//>')
return result
Generators as Components¶
You can also have components that act as generators. For example, imagine you have a todo list. There might be a lot of todos, so you want to generate them in a memory-efficient way:
def Todos():
"""A sequence of li items."""
for todo in ["First", "Second"]: # noqa B007
# TODO Andrea need to add generator support
yield html(t"<li>{todo}</li>")
def main():
"""Main entry point."""
result = html(t"<ul><{Todos}/></ul>")
return result
Subcomponents¶
Subcomponents are also supported:
def Todo(label):
"""An individual to do component."""
return html(t"<li>{label}</li>")
def TodoList(todos):
"""A to do list component."""
# Pass an argument, not the normal props style
return html(t"<ul>{[Todo(label) for label in todos]}</ul>")
def main():
"""Main entry point."""
todos = ["first"]
return html(
t"""
<h1>{title}</h1>
<{TodoList} todos={todos} />
"""
)