Components
In Ludic, you can create components similar to React components. These components don't have anything like a state similar to React, but they do consist of children and attributes.
Key Concepts
- Components: a component is a reusable chunk of code that defines a piece of your user interface. Think of it like a blueprint for an HTML element, but more powerful.
- Elements: these represent the individual HTML tags (like
<a>
,<div>
,<h1>
, etc.) that make up the structure of your page. - Attributes: These help define the properties on your components and elements. They let you modify things like a link's destination, text color, or an element's size.
- Hierarchy: Components can contain other components or elements, creating a tree-like structure.
- Types: A safety net to help you write correct code, preventing errors just like making sure LEGO pieces fit together properly.
Types of Components
- Regular: These are flexible, letting you have multiple children of any type.
- Strict: Perfect for when you need precise control over the structure of your component – like a table where you must have a head and a body.
Regular Components
Let's break down the simple Link component example from the README:
from typing import override
from ludic.types import Attrs, Component
class LinkAttrs(Attrs):
to: str
class Link(Component[str, LinkAttrs]):
@override
def render(self):
return a(
*self.children,
href=self.attrs["to"],
style={"color": "#abc"},
)
- HTML Rendering: This component renders as the following HTML element:
<a href="..." style="color:#abc">...</a>
- Type Hints:
Component[str, LinkAttrs]
provides type safety:str
: Enforces that all children of the component must be strings.LinkAttrs
: Ensures the required to attribute is present.
- Attributes: LinkAttrs inherits from
Attrs
, which is aTypedDict
(a dictionary with defined types for its keys).
The component would be instantiated like this:
Static type checkers will validate that you're providing the correct arguments and their types.
Multiple Children
The current definition doesn't strictly enforce a single child. This means you could technically pass multiple strings (Link("a", "b")
). To create a stricter component, inherit from ComponentStrict
: This subclass of Component allows for finer control over children. More about this in the next section.
Strict Components
Strict components offer more precise control over the types and structures of their children compared to regular components. Let's illustrate this with a Table component example:
from ludic.attrs import GlobalAttrs
from ludic.html import thead, tbody, tr
class TableHead(ComponentStrict[tr, GlobalAttrs]):
@override
def render(self) -> thead:
return thead(*self.children, **self.attrs)
class TableBody(ComponentStrict[*tuple[tr, ...], GlobalAttrs]):
@override
def render(self) -> tbody:
return tbody(*self.children, **self.attrs)
class Table(ComponentStrict[TableHead, TableBody, GlobalAttrs]):
@override
def render(self) -> table:
return table(
self.children[0],
self.children[1],
**self.attrs,
)
Explanation
- Strictness: The
ComponentStrict
class allows you to enforce the exact types and order of children. - Table Structure:
Table
: Expects precisely two children: aTableHead
followed by aTableBody
.TableHead
: Accepts only a singletr
(table row) element as its child.TableBody
: Accepts a variable number oftr
elements as children.
- Type Hints: The
*tuple[tr, ...]
syntax indicates thatTableBody
accepts zero or more tr elements.
Valid Usage (Passes Type Checking)
Table(
TableHead(tr(...)), # Table head with a single row
TableBody(tr(...), tr(...)) # Table body with multiple rows
)
Key Benefits
Strict components help you:
- Enforce Structure: Prevent incorrect usage that could break your component's layout or functionality.
- Type Safety: Static type checkers ensure you're building valid component hierarchies.
Attributes
To ensure type safety and clarity, define your component attributes using a subclass of the Attrs
class. Here's how:
from typing import NotRequired
from ludic.types import Attrs
class PersonAttrs(Attrs):
id: str
name: str
is_active: NotRequired[bool]
Understanding Attrs
and TypedDict
- The
Attrs
class is built upon Python'sTypedDict
concept (see PEP-589 for details). This provides type hints for dictionary-like data structures.
Controlling Required Attributes
- In the above case, all attributes except for
is_active
are required. If you want to make all attributes NOT required by default, you can pass thetotal=False
keyword argument to the class definition:
from typing import Required
from ludic.types import Attrs
class PersonAttrs(Attrs, total=False):
id: Required[str]
name: str
is_active: bool
In this case, all attributes are optional except for the id
attribute.
The Attrs
declaration is an information for type checkers
Attrs
class just provides typing information for static type checkers. Your code will work even if you pass key-word arguments to components without declaring them first.
Extending HTML Attributes
All attributes can also subclass from other classes, for example, you can extend the attributes for the <button>
HTML element:
from ludic.html import TdAttrs
from ludic.types import Attrs
class TableCellAttrs(TdAttrs):
is_numeric: bool
When implementing the component's render()
method, you might find the attrs_for(...)
helper useful too:
class TableCell(ComponentStrict[str, TableCellAttrs]):
@override
def render(self) -> td:
return td(self.children[0], **self.attrs_for(td))
The method passes only the attributes registered for the <td>
element.
Pre-defined Attributes
The ludic.attrs
module contains many attribute definition that you can reuse in your components, here are the most used ones:
HtmlAttrs
- Global HTML attributes available in all elements- The
class
andfor
attributes have the aliasesclass_
andfor_
- The
EventAttrs
- Event HTML attributes likeonclick
,onkey
, and so on.HtmxAttrs
- All HTMX attributes available.- All HTMX attributes have aliases with underscore, e.g.
hx_target
- All HTMX attributes have aliases with underscore, e.g.
GlobalAttrs
subclassesHtmlAttrs
,EventAttrs
andHtmxAttrs
[HtmlElementName]Attrs
- e.g.ButtonAttrs
,TdAttrs
, and so on.
HTML Elements
All available HTML elements can be found in ludic.html
module. The corresponding attributes are located in the ludic.attrs
module.
Rendering
To check how an element or component instance renders in HTML, you can use the .to_html()
method:
p("click ", Link("here", to="https://example.com")).to_html()
'<p>click <a href="https://example.com">here</a></p>'
Any string is automatically HTML escaped:
p("<script>alert('Hello world')</script>").to_html()
'<p><script>alert('Hello world')</script></p>'
Using f-strings
In Ludic, f-strings offer a bit more readable way to construct component content, especially if you need to do a lot of formatting with <b>
, <i>
, and other elements for improving typography. Let's modify the previous example using f-strings:
p1 = p(f"click {Link("here", to="https://example.com")}")
p2 = p("click ", Link("here", to="https://example.com"))
assert p1 == p2 # Identical components
- Temporary Dictionaries: to make f-strings safely work, they internally create temporary dictionaries to hold the components instances. To avoid memory leaks, these dictionaries need to be consumed by a component.
- Potential Leaks: Memory leaks can occur if:
- Component initialization with the f-string fails.
- You store an f-string in a variable but don't pass it to a component.
There are two cases it can create hanging objects (memory leaks):
Possible memory leak
The implementation of f-strings requires the creation of a temporary dict which can result in hanging objects in memory. To avoid memory leaks, there is the BaseElement.formatter
attribute which is a context manager clearing the temporary dict on exit.
The BaseElement.formatter
Context Manager
from ludic.types import BaseElement
with BaseElement.formatter:
# you can do anything with f-strings here, no memory leak
# is created since formatter dict is cleared on exit
Web Framework Request Handlers
The Ludic Web Framework (built on Starlette) automatically wraps request handlers with BaseElement.formatter
, providing a safe environment for f-strings.
Key Takeaway
While f-strings are convenient, exercise caution to prevent memory leaks. Use them within the provided safety mechanisms. In contexts like task queues or other web frameworks, you can use a similar mechanism of wrapping to achieve memory safety.
Available Methods
All components (and elements too) inherit the following properties and methods from the BaseElement
class:
BaseElement
children
- children of the componentattrs
- a dictionary containing attributesto_html()
- converts the component to an HTML documentto_string()
- converts the component to a string representation of the treeattrs_for(...)
- filter attributes to return only those valid for a given element or componenthas_attributes()
- whether the component has any attributesis_simple()
- whether the component contains one primitive childrender()
(abstract method) - render the component
Types and Helpers
The ludic.types
module contains many useful types:
NoChildren
- Makes a component accept no children.- example:
class Br(Component[NoChildren, Attrs]): ...
- example:
PrimitiveChildren
- Makes a component accept onlystr
,int
,float
orbool
- example:
class Paragraph(Component[PrimitiveChildren, Attrs]): ...
- example:
ComplexChildren
- Makes a component accept only non-primitive types.- example:
class Body(Component[ComplexChildren, Attrs]): ...
- example:
AnyChildren
- Makes a component accept any children types.- example:
class Section(Component[AnyChildren, Attrs]): ...
- example:
TAttrs
- type variable for attributesTChildren
- type variable for children of componentsTChildrenArgs
- type variable for children of strict componentsAttrs
- base for attributesBaseElement
- base for elementsElement
- base for HTML elementsElementStrict
- base for strict HTML elements- for example, the
<html>
element passes type checking only if the first child is<head>
and the second is<body>
- for example, the
Component
- abstract class for componentsComponentStrict
- abstract class for strict componentsBlank
- represents a blank component which is not rendered, only its children- example:
Blank(f"Hello {b("world")}").to_html() == 'Hello <b>world</b>'
- example:
Safe
- marker for a safe string which is not escaped- example:
a(Safe("This <b>won't</b> be escaped."))
- example:
JavaScript
- a marker for javascript, subclassesSafe
GlobalStyles
- type for CSS styles