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:
1 from typing import override 2 from ludic import Attrs, Component 3 4 class LinkAttrs(Attrs): 5 to: str 6 7 class Link(Component[str, LinkAttrs]): 8 @override 9 def render(self): 10 return a( 11 *self.children, 12 href=self.attrs["to"], 13 style={"color": "#abc"}, 14 )
- 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:
1 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:
1 from ludic.attrs import GlobalAttrs 2 from ludic.html import thead, tbody, tr 3 4 class TableHead(ComponentStrict[tr, GlobalAttrs]): 5 @override 6 def render(self) -> thead: 7 return thead(*self.children, **self.attrs) 8 9 class TableBody(ComponentStrict[*tuple[tr, ...], GlobalAttrs]): 10 @override 11 def render(self) -> tbody: 12 return tbody(*self.children, **self.attrs) 13 14 class Table(ComponentStrict[TableHead, TableBody, GlobalAttrs]): 15 @override 16 def render(self) -> table: 17 return table( 18 self.children[0], 19 self.children[1], 20 **self.attrs, 21 )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.
1 Table( 2 TableHead(tr(...)), # Table head with a single row 3 TableBody(tr(...), tr(...)) # Table body with multiple rows 4 )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:
1 from typing import NotRequired 2 from ludic.attrs import Attrs 3 4 class PersonAttrs(Attrs): 5 id: str 6 name: str 7 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:
1 from typing import Required 2 from ludic.attrs import Attrs 3 4 class PersonAttrs(Attrs, total=False): 5 id: Required[str] 6 name: str 7 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:
1 from ludic.html import TdAttrs 2 from ludic.attrs import Attrs 3 4 class TableCellAttrs(TdAttrs): 5 is_numeric: bool
When implementing the component's render()
method, you might find the attrs_for(...)
helper useful too:
1 class TableCell(ComponentStrict[str, TableCellAttrs]): 2 @override 3 def render(self) -> td: 4 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:
1 p("click ", Link("here", to="https://example.com")).to_html() 2 '<p>click <a href="https://example.com">here</a></p>'
Any string is automatically HTML escaped:
1 p("<script>alert('Hello world')</script>").to_html() 2 '<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:
1 p1 = p(f"click {Link("here", to="https://example.com")}") 2 p2 = p("click ", Link("here", to="https://example.com")) 3 4 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 Manager1 from ludic.base import BaseElement 2 3 with BaseElement.formatter: 4 # you can do anything with f-strings here, no memory leak 5 # 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 following is a list of all the other parts that can be used to type and build your application.
ludic.elements
#Element
– base for HTML elementsElementStrict
– base for strict HTML elementsBlank
– represents a blank component which is not rendered, only its children
ludic.components
#Component
– abstract class for componentsComponentStrict
– abstract class for strict componentsBlock
– component rendering as a divInline
– component rendering as a span
ludic.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 attributesSafe
– marker for a safe string which is not escapedJavaScript
– a marker for javascript, subclassesSafe
ludic.styles
#GlobalStyles
– type for HTML classes and their CSS propertiesCSSProperties
– type for CSS properties only