Skip to content

Commit 8b64318

Browse files
authored
Merge pull request #43 from volfpeter/htmy-integration
HTMY integration
2 parents 2829d45 + 46b7e15 commit 8b64318

File tree

18 files changed

+1116
-40
lines changed

18 files changed

+1116
-40
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,6 @@ cython_debug/
158158
# and can be added to the global gitignore or merged into this file. For a more nuclear
159159
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160160
#.idea/
161+
162+
# Untracked folder for dev stuff
163+
dev

README.md

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99

1010
# FastHX
1111

12-
FastAPI and HTMX, the right way.
12+
FastAPI server-side rendering with built-in HTMX support.
1313

1414
Key features:
1515

1616
- **Decorator syntax** that works with FastAPI as one would expect, no need for unused or magic dependencies in routes.
17-
- Works with **any templating engine** or server-side rendering library, e.g. `markyp-html` or `dominate`.
18-
- Built-in **Jinja2 templating support** (even with multiple template folders).
17+
- Built for **HTMX**, but can be used without it.
18+
- Works with **any templating engine** or server-side rendering library, e.g. `htmy`, `jinja2`, or `dominate`.
1919
- Gives the rendering engine **access to all dependencies** of the decorated route.
20-
- FastAPI **routes will keep working normally by default** if they receive **non-HTMX** requests, so the same route can serve data and render HTML at the same time.
20+
- HTMX **routes work as expected** if they receive non-HTMX requests, so the same route can serve data and render HTML at the same time.
2121
- **Response headers** you set in your routes are kept after rendering, as you would expect in FastAPI.
2222
- **Correct typing** makes it possible to apply other (typed) decorators to your routes.
2323
- Works with both **sync** and **async routes**.
@@ -30,15 +30,62 @@ The package is available on PyPI and can be installed with:
3030
$ pip install fasthx
3131
```
3232

33+
The package has optional dependencies for the following **official integrations**:
34+
35+
- [htmy](https://volfpeter.github.io/htmy/): `pip install fasthx[htmy]`.
36+
- [jinja][https://jinja.palletsprojects.com/en/stable/]: `pip install fasthx[jinja]`.
37+
3338
## Examples
3439

3540
For complete, but simple examples that showcase the basic use of `FastHX`, please see the [examples](https://github.com/volfpeter/fasthx/tree/main/examples) folder.
3641

37-
If you're looking for a more complex (`Jinja2`) example with features like active search, lazy-loading, server-sent events, custom server-side HTMX triggers, dialogs, and TailwindCSS and DaisyUI integration, check out this [FastAPI-HTMX-Tailwind example](https://github.com/volfpeter/fastapi-htmx-tailwind-example).
42+
### HTMY templating
43+
44+
Requires: `pip install fasthx[htmy]`.
45+
46+
Serving HTML and HTMX requests with [htmy](https://volfpeter.github.io/htmy/) is as easy as creating a `fasthx.htmy.HTMY` instance and using its `hx()` and `page()` decorator methods on your routes.
47+
48+
The example below assumes the existence of an `IndexPage` and a `UserList` `htmy` component. The full working example with the `htmy` components can be found [here](https://github.com/volfpeter/fasthx/tree/main/examples/htmy-rendering).
49+
50+
```python
51+
from datetime import date
52+
53+
from fastapi import FastAPI
54+
from pydantic import BaseModel
55+
56+
from fasthx.htmy import HTMY
57+
58+
# Pydantic model for the application
59+
class User(BaseModel):
60+
name: str
61+
birthday: date
62+
63+
# Create the FastAPI application.
64+
app = FastAPI()
65+
66+
# Create the FastHX HTMY instance that renders all route results.
67+
htmy = HTMY()
68+
69+
@app.get("/users")
70+
@htmy.hx(UserList) # Render the result using the UserList component.
71+
def get_users(rerenders: int = 0) -> list[User]:
72+
return [
73+
User(name="John", birthday=date(1940, 10, 9)),
74+
User(name="Paul", birthday=date(1942, 6, 18)),
75+
User(name="George", birthday=date(1943, 2, 25)),
76+
User(name="Ringo", birthday=date(1940, 7, 7)),
77+
]
78+
79+
@app.get("/")
80+
@htmy.page(IndexPage) # Render the index page.
81+
def index() -> None: ...
82+
```
3883

3984
### Jinja2 templating
4085

41-
To start serving HTML and HTMX requests, all you need to do is create an instance of `fasthx.Jinja` and use its `hx()` or `page()` methods as decorators on your routes. `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML, saving you some boilerplate code. See the example code below:
86+
Requires: `pip install fasthx[jinja]`.
87+
88+
To start serving HTML and HTMX requests, all you need to do is create an instance of `fasthx.Jinja` and use its `hx()` or `page()` methods as decorators on your routes. `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:
4289

4390
```python
4491
from fastapi import FastAPI
@@ -79,9 +126,15 @@ def htmx_only() -> list[User]:
79126
return [User(first_name="Billy", last_name="Shears")]
80127
```
81128

129+
See the full working example [here](https://github.com/volfpeter/fasthx/tree/main/examples/jinja-rendering).
130+
82131
### Custom templating
83132

84-
If you're not into Jinja templating, the `hx()` and `page()` decorators give you all the flexibility you need: you can integrate any HTML rendering or templating engine into `fasthx` simply by implementing the `HTMLRenderer` protocol. Similarly to the Jinja case, `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:
133+
Requires: `pip install fasthx`.
134+
135+
If you would like to use a rendering engine without FastHX integration, you can easily build on the `hx()` and `page()` decorators which give you all the functionality you will need. All you need to do is implement the `HTMLRenderer` protocol.
136+
137+
Similarly to the Jinja case, `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:
85138

86139
```python
87140
from typing import Annotated, Any
@@ -127,6 +180,12 @@ async def htmx_only(random_number: DependsRandomNumber) -> list[dict[str, str]]:
127180
return [{"name": "Joe"}]
128181
```
129182

183+
See the full working example [here](https://github.com/volfpeter/fasthx/tree/main/examples/custom-rendering).
184+
185+
### External examples
186+
187+
- [FastAPI-HTMX-Tailwind example](https://github.com/volfpeter/fastapi-htmx-tailwind-example): A complex `Jinja2` example with features like active search, lazy-loading, server-sent events, custom server-side HTMX triggers, dialogs, and TailwindCSS and DaisyUI integration.
188+
130189
## Dependencies
131190

132191
The only dependency of this package is `fastapi`.

docs/api/component_selectors.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# ::: fasthx.component_selectors
2+
3+
options:
4+
show_root_heading: true

docs/api/htmy.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# ::: fasthx.htmy
2+
3+
options:
4+
show_root_heading: true

docs/examples/htmy.md

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# HTMY
2+
3+
The primary focus of this example is how to create [htmy](https://volfpeter.github.io/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](https://volfpeter.github.io/htmy/) components, context usage, and route decorators.
4+
5+
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:
6+
7+
```python
8+
import random
9+
from dataclasses import dataclass
10+
from datetime import date
11+
12+
from fastapi import FastAPI
13+
from htmy import Component, Context, html
14+
from pydantic import BaseModel
15+
16+
from fasthx.htmy import HTMY, ComponentHeader, CurrentRequest, RouteParams
17+
18+
19+
class User(BaseModel):
20+
"""User model."""
21+
22+
name: str
23+
birthday: date
24+
```
25+
26+
The main content on the user interface will be a user list, so let's start by creating a simple `UserListItem` component:
27+
28+
```python
29+
@dataclass
30+
class UserListItem:
31+
"""User list item component."""
32+
33+
user: User
34+
35+
def htmy(self, context: Context) -> Component:
36+
return html.li(
37+
html.span(self.user.name, class_="font-semibold"),
38+
html.em(f" (born {self.user.birthday.isoformat()})"),
39+
class_="text-lg",
40+
)
41+
```
42+
43+
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.
44+
45+
The next component we need is the user list itself. This is going to be the most complex part of the example:
46+
47+
- To showcase `htmy` context usage, this component will display some information about the application's state in addition to the list of users.
48+
- We will also add a bit of [HTMX](https://htmx.org/attributes/hx-trigger/) to the component to make it re-render every second.
49+
50+
```python
51+
@dataclass
52+
class UserOverview:
53+
"""
54+
Component that shows a user list and some additional info about the application's state.
55+
56+
The component reloads itself every second.
57+
"""
58+
59+
users: list[User]
60+
ordered: bool = False
61+
62+
def htmy(self, context: Context) -> Component:
63+
# Load the current request from the context.
64+
request = CurrentRequest.from_context(context)
65+
# Load route parameters (resolved dependencies) from the context.
66+
route_params = RouteParams.from_context(context)
67+
# Get the user-agent from the context which is added by a request processor.
68+
user_agent: str = context["user-agent"]
69+
# Get the rerenders query parameter from the route parameters.
70+
rerenders: int = route_params["rerenders"]
71+
72+
# Create the user list item generator.
73+
user_list_items = (UserListItem(u) for u in self.users)
74+
75+
# Create the ordered or unordered user list.
76+
user_list = (
77+
html.ol(*user_list_items, class_="list-decimal list-inside")
78+
if self.ordered
79+
else html.ul(*user_list_items, class_="list-disc list-inside")
80+
)
81+
82+
# Randomly decide whether an ordered or unordered list should be rendered next.
83+
next_variant = random.choice(("ordered", "unordered")) # noqa: S311
84+
85+
return html.div(
86+
# -- Some content about the application state.
87+
html.p(html.span("Last request: ", class_="font-semibold"), str(request.url)),
88+
html.p(html.span("User agent: ", class_="font-semibold"), user_agent),
89+
html.p(html.span("Re-renders: ", class_="font-semibold"), str(rerenders)),
90+
html.hr(),
91+
# -- User list.
92+
user_list,
93+
# -- HTMX directives.
94+
hx_trigger="load delay:1000",
95+
hx_get=f"/users?rerenders={rerenders+1}",
96+
hx_swap="outerHTML",
97+
# Send the next component variant in an X-Component header.
98+
hx_headers=f'{{"X-Component": "{next_variant}"}}',
99+
# -- Styling
100+
class_="flex flex-col gap-4",
101+
)
102+
```
103+
104+
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:
105+
106+
- The use of `CurrentRequest.from_context()` to get access to the current `fastapi.Request` instance.
107+
- The use of `RouteParams.from_context()` to get access to every route parameter (resolved FastAPI dependency) as a mapping.
108+
- The `context["user-agent"]` lookup that accesses a value from the context which will be added by a _request processor_ later in the example.
109+
110+
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.
111+
112+
```python
113+
@dataclass
114+
class IndexPage:
115+
"""Index page with TailwindCSS styling."""
116+
117+
def htmy(self, context: Context) -> Component:
118+
return (
119+
html.DOCTYPE.html,
120+
html.html(
121+
html.head(
122+
# Some metadata
123+
html.title("FastHX + HTMY example"),
124+
html.meta.charset(),
125+
html.meta.viewport(),
126+
# TailwindCSS
127+
html.script(src="https://cdn.tailwindcss.com"),
128+
# HTMX
129+
html.script(src="https://unpkg.com/[email protected]"),
130+
),
131+
html.body(
132+
# Page content: lazy-loaded user list.
133+
html.div(hx_get="/users", hx_trigger="load", hx_swap="outerHTML"),
134+
class_=(
135+
"h-screen w-screen flex flex-col items-center justify-center "
136+
" gap-4 bg-slate-800 text-white"
137+
),
138+
),
139+
),
140+
)
141+
```
142+
143+
With all the components ready, we can now create the `FastAPI` and `fasthx.htmy.HTMY` instances:
144+
145+
```python
146+
# Create the app instance.
147+
app = FastAPI()
148+
149+
# Create the FastHX HTMY instance that renders all route results.
150+
htmy = HTMY(
151+
# Register a request processor that adds a user-agent key to the htmy context.
152+
request_processors=[
153+
lambda request: {"user-agent": request.headers.get("user-agent")},
154+
]
155+
)
156+
```
157+
158+
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.
159+
160+
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.
161+
162+
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:
163+
164+
```python
165+
@app.get("/")
166+
@htmy.page(lambda _: IndexPage())
167+
def index() -> None:
168+
"""The index page of the application."""
169+
...
170+
```
171+
172+
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`.
173+
174+
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).
175+
176+
The full route declaration is as follows:
177+
178+
```python
179+
@app.get("/users")
180+
@htmy.hx(
181+
# Use a header-based component selector that can serve ordered or
182+
# unordered user lists, depending on what the client requests.
183+
ComponentHeader(
184+
"X-Component",
185+
{
186+
"ordered": lambda users: UserOverview(users, True),
187+
"unordered": UserOverview,
188+
},
189+
default=UserOverview,
190+
)
191+
)
192+
def get_users(rerenders: int = 0) -> list[User]:
193+
"""Returns the list of users in random order."""
194+
result = [
195+
User(name="John", birthday=date(1940, 10, 9)),
196+
User(name="Paul", birthday=date(1942, 6, 18)),
197+
User(name="George", birthday=date(1943, 2, 25)),
198+
User(name="Ringo", birthday=date(1940, 7, 7)),
199+
]
200+
random.shuffle(result)
201+
return result
202+
```
203+
204+
We finally have everything, all that remains is running our application. Depending on how you [installed FastAPI](https://fastapi.tiangolo.com/#installation), you can do this for example with:
205+
206+
- the `fastapi` CLI like this: `fastapi dev htmy_app.py`,
207+
- or with `uvicorn` like this: `uvicorn htmy_app:app --reload`.
208+
209+
If everything went well, the application will be available at `http://127.0.0.1:8000`.

docs/examples/index.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
The [custom templating](/examples/custom-templating) and [jinja templating](/examples/jinja-templating) pages offer a glimpse of the capabilities of `FastHX`.
1+
The pages below offer a glimpse of the capabilities of `FastHX`.
22

3-
For complete, but simple examples that showcase the basic use of `FastHX`, please see the [examples](https://github.com/volfpeter/fasthx/tree/main/examples) folder.
3+
For complete, but simple examples that showcase the basic use of `FastHX`, please see the [examples folder](https://github.com/volfpeter/fasthx/tree/main/examples) of the repository.
44

5-
If you're looking for a more complex (`Jinja2`) example with features like active search, lazy-loading, server-sent events, custom server-side HTMX triggers, dialogs, and TailwindCSS and DaisyUI integration, check out this [FastAPI-HTMX-Tailwind example](https://github.com/volfpeter/fastapi-htmx-tailwind-example).
5+
## External examples
6+
7+
- [FastAPI-HTMX-Tailwind example](https://github.com/volfpeter/fastapi-htmx-tailwind-example): A complex `Jinja2` example with features like active search, lazy-loading, server-sent events, custom server-side HTMX triggers, dialogs, and TailwindCSS and DaisyUI integration.

docs/index.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99

1010
# FastHX
1111

12-
FastAPI and HTMX, the right way.
12+
FastAPI server-side rendering with built-in HTMX support.
1313

1414
Key features:
1515

1616
- **Decorator syntax** that works with FastAPI as one would expect, no need for unused or magic dependencies in routes.
17-
- Works with **any templating engine** or server-side rendering library, e.g. `markyp-html` or `dominate`.
18-
- Built-in **Jinja2 templating support** (even with multiple template folders).
17+
- Built for **HTMX**, but can be used without it.
18+
- Works with **any templating engine** or server-side rendering library, e.g. `htmy`, `jinja2`, or `dominate`.
1919
- Gives the rendering engine **access to all dependencies** of the decorated route.
20-
- FastAPI **routes will keep working normally by default** if they receive **non-HTMX** requests, so the same route can serve data and render HTML at the same time.
20+
- HTMX **routes work as expected** if they receive non-HTMX requests, so the same route can serve data and render HTML at the same time.
2121
- **Response headers** you set in your routes are kept after rendering, as you would expect in FastAPI.
2222
- **Correct typing** makes it possible to apply other (typed) decorators to your routes.
2323
- Works with both **sync** and **async routes**.
@@ -30,6 +30,11 @@ The package is available on PyPI and can be installed with:
3030
$ pip install fasthx
3131
```
3232

33+
The package has optional dependencies for the following **official integrations**:
34+
35+
- [htmy](https://volfpeter.github.io/htmy/): `pip install fasthx[htmy]`.
36+
- [jinja][https://jinja.palletsprojects.com/en/stable/]: `pip install fasthx[jinja]`.
37+
3338
## Dependencies
3439

3540
The only dependency of this package is `fastapi`.

0 commit comments

Comments
 (0)