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 a simplified version of the Link
component:
from typing import override from ludic 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 fromAttrs
, which is aTypedDict
(a dictionary with defined types for its keys)
The component would be instantiated like this:
Link("here", to="https://example.org")
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 simplified Table
component:
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.
Table( TableHead(tr(...)), # Table head with a single row TableBody(tr(...), tr(...)) # Table body with multiple rows )Key Benefits
- 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.attrs 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 {Code('is_active')} are required. If you want to make all attributes NOT required by default, you can pass the total=False
keyword argument to the class definition:
from typing import Required from ludic.attrs 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.
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.attrs 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 definitions 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 likeon_click
,on_key
, and so on.HtmxAttrs
– All HTMX attributes available.- All HTMX attributes have aliases with an underscore, e.g.
hx_target
- All HTMX attributes have aliases with an 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 the 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 componentsImportant Note: Memory Considerations
Temporary Dictionaries: to make f-strings safely work, they internally create temporary dictionaries to hold the component instances. To avoid memory leaks, these dictionaries need to be consumed by a component.
There are two cases it can create hanging objects (memory leaks):
- Component initialization with the f-string fails.
- You store an f-string in a variable but don't pass it to a component.
BaseElement.formatter
Context Managerfrom ludic.base import BaseElement with BaseElement.formatter: # you can do anything with f-strings here, no memory leak # is created since formatter dict is cleared on exitWeb Framework Request Handlers
The Ludic Web Framework (built on Starlette) automatically wraps request handlers with BaseElement.formatter
, providing a safe environment for f-strings.
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 childrenPrimitiveChildren
– Makes a component accept onlystr
,int
,float
orbool
ComplexChildren
– Makes a component accept only non-primitive typesAnyChildren
– Makes a component accept any children typesTAttrs
– 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 elementsComponent
– abstract class for componentsComponentStrict
– abstract class for strict componentsBlank
– represents a blank component which is not rendered, only its childrenSafe
– marker for a safe string which is not escapedJavaScript
– a marker for javascript, subclassesSafe
GlobalStyles
– type for CSS styles