Ludic Logo
Edit

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 from Attrs, which is a TypedDict (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: a TableHead followed by a TableBody.
    • TableHead: Accepts only a single tr (table row) element as its child.
    • TableBody: Accepts a variable number of tr elements as children.
  • Type Hints: The *tuple[tr, ...] syntax indicates that TableBody 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
  • 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's TypedDict 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.

The Attrs declaration is an information for type checkers
The 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.

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 and for attributes have the aliases class_ and for_
  • EventAttrs – Event HTML attributes like on_click, on_key, and so on.
  • HtmxAttrs – All HTMX attributes available.
    • All HTMX attributes have aliases with an underscore, e.g. hx_target
  • GlobalAttrs subclasses HtmlAttrs, EventAttrs, and HtmxAttrs
  • [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>&lt;script&gt;alert(&#x27;Hello world&#x27;)&lt;/script&gt;</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
Important 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):

  1. Component initialization with the f-string fails.
  2. You store an f-string in a variable but don't pass it to a component.
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.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 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 Takeaways

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 component
    • attrs – a dictionary containing attributes
    • to_html() – converts the component to an HTML document
    • to_string() – converts the component to a string representation of the tree
    • attrs_for(...) – filter attributes to return only those valid for a given element or component
    • has_attributes() – whether the component has any attributes
    • is_simple() – whether the component contains one primitive child
    • render() (*abstract method*) – render the component

Types and Helpers

#

The ludic.types module contains many useful types:

  • NoChildren – Makes a component accept no children
  • PrimitiveChildren – Makes a component accept only str, int, float or bool
  • ComplexChildren – Makes a component accept only non-primitive types
  • AnyChildren – Makes a component accept any children types
  • TAttrs – type variable for attributes
  • TChildren – type variable for children of components
  • TChildrenArgs – type variable for children of strict components
  • Attrs – base for attributes
  • BaseElement – base for elements
  • Element – base for HTML elements
  • ElementStrict – base for strict HTML elements
  • Component – abstract class for components
  • ComponentStrict – abstract class for strict components
  • Blank – represents a blank component which is not rendered, only its children
  • Safe – marker for a safe string which is not escaped
  • JavaScript – a marker for javascript, subclasses Safe
  • GlobalStyles – type for CSS styles
Made with Ludic and HTMX and 🐍 • DiscordGitHub