Tutorial
By doing this tutorial you can learn the full compone
API very quickly,
because there are only a couple of concepts and methods you need to know about
to effectively use compone
.
Hello World¶
from compone import Component, html
@Component
def Hello(name: str):
return html.Div[
html.H1[f"Hello {name}!"],
]
content = str(Hello("World"))
print(content)
# <div><h1>Hello World!</h1></div>
What's going on here?
- We are defining a new Component with the
@Component
decorator, with one prop:name
.
It converts the function into a Component class under the hood. - We use the square brackets syntax (
__getitem__
) to specify the children of theDiv
element.
Same for theH1
element with our text. - We "render" the Component into a string then print it.
functions are converted to classes
It's important to know that functions with the @Component
decorator
are dynamically converted to classes, so you are not calling the function
directly, but creating an instance of your Component
class and calling
it's __str__
method when rendering it.
Why don't we just use a simple function to return strings?
That's a good question! See the next chapters for features that make
Component
s more powerful than doing that.
Composability with children¶
The main feature of compone is the ability to compose Component
s with other
Components via the children parameter. (The concept is similar to React.js).
This way any component can be nested inside another component. There are 2 ways
you can pass children to a Component:
Bracket syntax¶
Here is an example of nesting two of your custom Components:
from compone import Component
@Component
def Parent(children = ""):
return f"""
--- Start of Parent ---
{children}
--- End of Parent ---
"""
@Component
def Children(children = ""):
return f"""
--- Start of Children ---
{children}
--- End of Children ---
"""
print(
Parent[
Children["Hello World"]
]
)
This will print:
--- Start of Parent ---
--- Start of Children ---
Hello World
--- End of Children ---
--- End of Parent ---
Whitespace
Note the whitespace before the lines, which is because of spaces and newlines in the multiline strings.
Ther rendering is lazy, all the children will be rendered only at the time parent is rendered.
You can return a list from a Component, and it's elements will be concatenated:
from compone import Component, html
@Component
def ListParent(children = ""):
return [
"--- Start of Parent ---",
children,
"--- End of Parent ---",
]
@Component
def ListChildren(children = ""):
return [
"--- Start of Children ---",
children,
"--- End of Children ---"
]
print(
ListParent[
ListChildren["Hello World"]
]
)
This doesn't print whitespaces like the previous example, because the list elements are concatenated directly:
--- Start of Parent ------ Start of Children ---Hello World--- End of Children ------ End of Parent ---
Context manager syntax¶
When you have more complex Components with a lot more children (like in HTML usually), you can use the context manager syntax.
from compone import Component
@Component
def Section(*, title: str, children):
return html.Div[
html.H1[title],
children,
]
@Component
def Article(title: str, children):
return html.Article[
html.H2[title],
children,
]
with Section(title="Section title") as main:
with Article(title="Article title") as article:
article += html.P["World"]
print(main)
Output (no spaces):
As you can see, you can mix and match the two syntaxes, any way you like.
My suggestion is to use the bracket syntax for simpler, shorter Components, and the context manager syntax for more complex Components, especially when you want to have custom code in the middle of a Component.
HTML Components¶
All HTML tags are available as Components in the html
module.
Here is a full HTML page with separate Components for sections:
import datetime as dt
from compone import Component, html
@Component
def Page(title: str, children):
return html.Html[
html.Head[
html.Title[title],
html.Meta(name="author", content="György Kiss"),
],
html.Body[children],
]
@Component
def Header():
return html.Header[html.H1["Header"],]
@Component
def Content(children):
return html.Article[children]
@Component
def Footer():
return html.Footer[
html.P[
"Footer",
html.Br(),
"Copyright ©",
dt.datetime.now().year,
],
]
page = Page(title="My awesome website")[
Header(),
Content["Main content"],
Footer(),
]
print(page)
This will print (without indentation):
<html>
<head>
<title>My awesome website</title>
<meta name="author" content="György Kiss" />
</head>
<body>
<header><h1>Header</h1></header>
<article>Main content</article>
<footer>
<p>Footer<br />Copyright ©2024</p>
</footer>
</body>
</html>
HTML attributes¶
Any parameter you pass to an HTML Component will be rendered as HTML attribute, with underscores converted to dashes:
print(html.Div(id="my-id", some_attribute="value", data_value="data"))
# <div id="my-id" some-attribute="value" data-value="data"></div>
For Python keywords, append an underscore to the attribute name:
print(html.Div(class_="my-class"))
# <div class="my-class"></div>
print(html.Label(for_="my-for"))
# <label for="my-for"></label>
Boolean type attributes will be rendered only when True
:
print(html.Button(disabled=True))
# <button disabled></button>
print(html.Label(disabled=False))
# <button></button>
List attribute types will be joined with spaces:
HTML autoescape¶
Every HTML attribute and children will be automatically escaped by default. You
can opt-out with marking the string with the safe
class (which uses MarkupSafe
under the hood for now.). Every Component is safe by default.
from compone import html, safe
evil_user_input = "<script>alert('Hello!')</script>"
safe_div = html.Div[evil_user_input]
print(safe_div)
# <div><script>alert('Hello!')</script></div>
Danger
Be careful when using safe
, always think about every single case
whether it will contain user input or not, don't just blindly apply
it when you encounter an escaped content which should be rendered as html.
For example, you can't just mark safe
a dynamically rendered JavaScript
which contains user input, because it can lead to XSS attacks.
If you pass a list or *args
as HTML Component children, str
will be
autoescaped, safe
objects will be rendered as is:
from compone import html, safe
evil_username = '<script>alert("Hello!")</script>'
look_ma_no_xss = html.Div[
safe("<script>console.log('Valid script')</script>"),
evil_username,
]
print(look_ma_no_xss)
# <div><script>console.log('Valid script')</script><script>alert("Hello!")</script></div>
This still doesn't protect against XSS
This is still susceptible to XSS attacks, you have to escape user input
differently when injecting it to JavaScript context.
See OWASP XSS prevention cheat sheet
to defend against different kind of attacks.
HTML helpers¶
There are some helpers to make it less cumbersome when working with HTML.
You can manipulate the html class
attribute easier with the html.classes
helper.
It is inspired by the classnames
JavaScript library.
Dynamically applying classes to HTML class
property:
my_dynamic_classes = html.classes({"red": False, "green": True})
print(html.Div(class_=my_dynamic_classes))
# <div class="green"></div>
Concatenating list of classes with strings:
my_classes = html.classes("red", "green", ["blue", "yellow"], "brown black")
print(html.Div(class_=my_classes))
# <div class="red green blue yellow brown black"></div>
Component introspection¶
If for some reason you need to inspect a Component class, you can use the .props
property
for all parameters of the instance:
@Component
def MyComponent(a=1, b=2):
return str(a*b)
my_component = MyComponent()
print(my_component.props)
# {'a': 1, 'b': 2}
You can access the children of a Component instance with the .children
property:
@Component
def MyComponent(a=1, b=2, children=None):
return html.Div[str(a*b), children]
my_component = MyComponent["My Content", html.P["New paragraph"]]
print(my_component.children)
# ('My Content', <P()>)
Class components¶
In rare cases when you have a complex logic and want to sticky it to one class,
you can use the Component
decorator on classes too.
The class only need to have a .render()
method.
@Component
class MyComponent:
def __init__(self, a=1, b=2):
self._a = a
self._b = b
def render(self, children):
return html.Div[str(self._a * self._b), " ", children]
my_component = MyComponent(2, 3)["eggs"]
print(my_component)
# <div>6 eggs</div>
Works the same as function components
This works the same way as function Components, the decorator
converts your class to a Component class and calls your .render()
method when the Component __str__
method is called.