Skip to content

HTMY

The primary focus of this example is how to create htmy components that work together with fasthx and make use of its utilities. The components use TailwindCSS for styling -- if you are not familiar with TailwindCSS, just ignore the class_="..." arguments, they are not important from the perspective of fasthx and htmy. The focus should be on the htmy components, context usage, and route decorators.

First, let's create an htmy_app.py file, import everything that is required for the example, and also define a simple Pydantic User model for the application:

import random
from dataclasses import dataclass
from datetime import date

from fastapi import FastAPI
from htmy import Component, Context, html
from pydantic import BaseModel

from fasthx.htmy import HTMY, ComponentHeader, CurrentRequest, RouteParams


class User(BaseModel):
    """User model."""

    name: str
    birthday: date

The main content on the user interface will be a user list, so let's start by creating a simple UserListItem component:

@dataclass
class UserListItem:
    """User list item component."""

    user: User

    def htmy(self, context: Context) -> Component:
        return html.li(
            html.span(self.user.name, class_="font-semibold"),
            html.em(f" (born {self.user.birthday.isoformat()})"),
            class_="text-lg",
        )

As you can see, the component has a single user property and it renders an <li> HTML element with the user's name and birthday in it.

The next component we need is the user list itself. This is going to be the most complex part of the example:

  • To showcase htmy context usage, this component will display some information about the application's state in addition to the list of users.
  • We will also add a bit of HTMX to the component to make it re-render every second.
@dataclass
class UserOverview:
    """
    Component that shows a user list and some additional info about the application's state.

    The component reloads itself every second.
    """

    users: list[User]
    ordered: bool = False

    def htmy(self, context: Context) -> Component:
        # Load the current request from the context.
        request = CurrentRequest.from_context(context)
        # Load route parameters (resolved dependencies) from the context.
        route_params = RouteParams.from_context(context)
        # Get the user-agent from the context which is added by a request processor.
        user_agent: str = context["user-agent"]
        # Get the rerenders query parameter from the route parameters.
        rerenders: int = route_params["rerenders"]

        # Create the user list item generator.
        user_list_items = (UserListItem(u) for u in self.users)

        # Create the ordered or unordered user list.
        user_list = (
            html.ol(*user_list_items, class_="list-decimal list-inside")
            if self.ordered
            else html.ul(*user_list_items, class_="list-disc list-inside")
        )

        # Randomly decide whether an ordered or unordered list should be rendered next.
        next_variant = random.choice(("ordered", "unordered"))  # noqa: S311

        return html.div(
            # -- Some content about the application state.
            html.p(html.span("Last request: ", class_="font-semibold"), str(request.url)),
            html.p(html.span("User agent: ", class_="font-semibold"), user_agent),
            html.p(html.span("Re-renders: ", class_="font-semibold"), str(rerenders)),
            html.hr(),
            # -- User list.
            user_list,
            # -- HTMX directives.
            hx_trigger="load delay:1000",
            hx_get=f"/users?rerenders={rerenders+1}",
            hx_swap="outerHTML",
            # Send the next component variant in an X-Component header.
            hx_headers=f'{{"X-Component": "{next_variant}"}}',
            # -- Styling
            class_="flex flex-col gap-4",
        )

Most of this code is basic Python and htmy usage (including the hx_* HTMX attributes). The important, fasthx-specific things that require special attention are:

  • The use of CurrentRequest.from_context() to get access to the current fastapi.Request instance.
  • The use of RouteParams.from_context() to get access to every route parameter (resolved FastAPI dependency) as a mapping.
  • The context["user-agent"] lookup that accesses a value from the context which will be added by a request processor later in the example.

We need one last htmy component, the index page. Most of this component is just the basic HTML document structure with some TailwindCSS styling and metadata. There is also a bit of HTMX in the body for lazy loading the actual page content, the user list we just created.

@dataclass
class IndexPage:
    """Index page with TailwindCSS styling."""

    def htmy(self, context: Context) -> Component:
        return (
            html.DOCTYPE.html,
            html.html(
                html.head(
                    # Some metadata
                    html.title("FastHX + HTMY example"),
                    html.meta.charset(),
                    html.meta.viewport(),
                    # TailwindCSS
                    html.script(src="https://cdn.tailwindcss.com"),
                    # HTMX
                    html.script(src="https://unpkg.com/htmx.org@2.0.2"),
                ),
                html.body(
                    # Page content: lazy-loaded user list.
                    html.div(hx_get="/users", hx_trigger="load", hx_swap="outerHTML"),
                    class_=(
                        "h-screen w-screen flex flex-col items-center justify-center "
                        " gap-4 bg-slate-800 text-white"
                    ),
                ),
            ),
        )

With all the components ready, we can now create the FastAPI and fasthx.htmy.HTMY instances:

# Create the app instance.
app = FastAPI()

# Create the FastHX HTMY instance that renders all route results.
htmy = HTMY(
    # Register a request processor that adds a user-agent key to the htmy context.
    request_processors=[
        lambda request: {"user-agent": request.headers.get("user-agent")},
    ]
)

Note how we added a request processor function to the HTMY instance that takes the current FastAPI Request and returns a context mapping that is merged into the htmy rendering context and made available to every component.

All that remains now is the routing. We need two routes: one that serves the index page, and one that renders the ordered or unordered user list.

The index page route is trivial. The htmy.page() decorator expects a component factory (well more precisely a fasthx.ComponentSelector) that accepts the route's return value and returns an htmy component. Since IndexPage has no properties, we use a simple lambda to create such a function:

@app.get("/")
@htmy.page(lambda _: IndexPage())
def index() -> None:
    """The index page of the application."""
    ...

The /users route is a bit more complex: we need to use the fasthx.htmy.ComponentHeader utility, because depending on the value of the X-Component header (remember the hx_headers declaration in UserOverview.htmy()) it must render the route's result either with the ordered or unordered version of UserOverview.

The route also has a rerenders query parameter just to showcase how fasthx makes resolved route dependencies accessible to components through the htmy rendering context (see UserOverview.htmy() for details).

The full route declaration is as follows:

@app.get("/users")
@htmy.hx(
    # Use a header-based component selector that can serve ordered or
    # unordered user lists, depending on what the client requests.
    ComponentHeader(
        "X-Component",
        {
            "ordered": lambda users: UserOverview(users, True),
            "unordered": UserOverview,
        },
        default=UserOverview,
    )
)
def get_users(rerenders: int = 0) -> list[User]:
    """Returns the list of users in random order."""
    result = [
        User(name="John", birthday=date(1940, 10, 9)),
        User(name="Paul", birthday=date(1942, 6, 18)),
        User(name="George", birthday=date(1943, 2, 25)),
        User(name="Ringo", birthday=date(1940, 7, 7)),
    ]
    random.shuffle(result)
    return result

We finally have everything, all that remains is running our application. Depending on how you installed FastAPI, you can do this for example with:

  • the fastapi CLI like this: fastapi dev htmy_app.py,
  • or with uvicorn like this: uvicorn htmy_app:app --reload.

If everything went well, the application will be available at http://127.0.0.1:8000.