Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTMY integration #43

Merged
merged 23 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
825860e
ignore the dev folder
volfpeter Nov 19, 2024
fe9ddbb
added the htmy module
volfpeter Nov 19, 2024
43cdec4
added an htmy example
volfpeter Nov 19, 2024
783d73a
updated the docs
volfpeter Nov 19, 2024
28ac551
added component_selectors module, a generic ComponentHeader selector…
volfpeter Nov 21, 2024
729bc31
fixed typo and added a typed ComponentHeader to htmy, refs #42
volfpeter Nov 21, 2024
b074ce2
added tests for the htmy integration (including the ComponentHeader s…
volfpeter Nov 21, 2024
5f7e865
added ComponentHeader usage to the htmy example app, refs #42
volfpeter Nov 21, 2024
1c96ce0
fix linter error
volfpeter Nov 22, 2024
d1f9632
added tests for htmy context processors, refs #42
volfpeter Nov 23, 2024
2e845a9
renamed htmy context processor to request processor to avoid confusio…
volfpeter Nov 24, 2024
67816ce
removed some code duplication in tests
volfpeter Nov 24, 2024
d727d97
remove unused import
volfpeter Nov 24, 2024
d8a773b
added a request processor to the htmy example app, refs #42
volfpeter Nov 24, 2024
2b74929
removed dummy props from IndexPage in the htmy example, refs #42
volfpeter Nov 24, 2024
b42f38e
htmy example app: readability improvements and more comments
volfpeter Nov 26, 2024
4970147
nitpicking
volfpeter Nov 26, 2024
2f28eb0
more example htmy app simplification, refs #42
volfpeter Nov 26, 2024
113a77f
expose the ComponentSelector type directly through the fasthx package
volfpeter Nov 26, 2024
cfd6460
more example htmy app simplification, refs #42
volfpeter Nov 26, 2024
2408074
added an htmy example to the docs, refs #42
volfpeter Nov 26, 2024
f92b752
bump minor version, refs #42
volfpeter Nov 26, 2024
46b7e15
updated project description
volfpeter Nov 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# Untracked folder for dev stuff
dev
73 changes: 66 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@

# FastHX

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

Key features:

- **Decorator syntax** that works with FastAPI as one would expect, no need for unused or magic dependencies in routes.
- Works with **any templating engine** or server-side rendering library, e.g. `markyp-html` or `dominate`.
- Built-in **Jinja2 templating support** (even with multiple template folders).
- Built for **HTMX**, but can be used without it.
- Works with **any templating engine** or server-side rendering library, e.g. `htmy`, `jinja2`, or `dominate`.
- Gives the rendering engine **access to all dependencies** of the decorated route.
- 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.
- 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.
- **Response headers** you set in your routes are kept after rendering, as you would expect in FastAPI.
- **Correct typing** makes it possible to apply other (typed) decorators to your routes.
- Works with both **sync** and **async routes**.
Expand All @@ -30,15 +30,62 @@ The package is available on PyPI and can be installed with:
$ pip install fasthx
```

The package has optional dependencies for the following **official integrations**:

- [htmy](https://volfpeter.github.io/htmy/): `pip install fasthx[htmy]`.
- [jinja][https://jinja.palletsprojects.com/en/stable/]: `pip install fasthx[jinja]`.

## Examples

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.

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).
### HTMY templating

Requires: `pip install fasthx[htmy]`.

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.

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).

```python
from datetime import date

from fastapi import FastAPI
from pydantic import BaseModel

from fasthx.htmy import HTMY

# Pydantic model for the application
class User(BaseModel):
name: str
birthday: date

# Create the FastAPI application.
app = FastAPI()

# Create the FastHX HTMY instance that renders all route results.
htmy = HTMY()

@app.get("/users")
@htmy.hx(UserList) # Render the result using the UserList component.
def get_users(rerenders: int = 0) -> list[User]:
return [
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)),
]

@app.get("/")
@htmy.page(IndexPage) # Render the index page.
def index() -> None: ...
```

### Jinja2 templating

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:
Requires: `pip install fasthx[jinja]`.

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:

```python
from fastapi import FastAPI
Expand Down Expand Up @@ -79,9 +126,15 @@ def htmx_only() -> list[User]:
return [User(first_name="Billy", last_name="Shears")]
```

See the full working example [here](https://github.com/volfpeter/fasthx/tree/main/examples/jinja-rendering).

### Custom templating

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:
Requires: `pip install fasthx`.

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.

Similarly to the Jinja case, `hx()` only triggers HTML rendering for HTMX requests, while `page()` unconditionally renders HTML. See the example code below:

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

See the full working example [here](https://github.com/volfpeter/fasthx/tree/main/examples/custom-rendering).

### External examples

- [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.

## Dependencies

The only dependency of this package is `fastapi`.
Expand Down
4 changes: 4 additions & 0 deletions docs/api/component_selectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# ::: fasthx.component_selectors

options:
show_root_heading: true
4 changes: 4 additions & 0 deletions docs/api/htmy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# ::: fasthx.htmy

options:
show_root_heading: true
209 changes: 209 additions & 0 deletions docs/examples/htmy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# HTMY

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.

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:

```python
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:

```python
@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](https://htmx.org/attributes/hx-trigger/) to the component to make it re-render every second.

```python
@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.

```python
@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:

```python
# 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:

```python
@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:

```python
@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](https://fastapi.tiangolo.com/#installation), 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`.
8 changes: 5 additions & 3 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
The [custom templating](/examples/custom-templating) and [jinja templating](/examples/jinja-templating) pages offer a glimpse of the capabilities of `FastHX`.
The pages below offer a glimpse of the capabilities of `FastHX`.

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.
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.

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).
## External examples

- [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.
13 changes: 9 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@

# FastHX

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

Key features:

- **Decorator syntax** that works with FastAPI as one would expect, no need for unused or magic dependencies in routes.
- Works with **any templating engine** or server-side rendering library, e.g. `markyp-html` or `dominate`.
- Built-in **Jinja2 templating support** (even with multiple template folders).
- Built for **HTMX**, but can be used without it.
- Works with **any templating engine** or server-side rendering library, e.g. `htmy`, `jinja2`, or `dominate`.
- Gives the rendering engine **access to all dependencies** of the decorated route.
- 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.
- 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.
- **Response headers** you set in your routes are kept after rendering, as you would expect in FastAPI.
- **Correct typing** makes it possible to apply other (typed) decorators to your routes.
- Works with both **sync** and **async routes**.
Expand All @@ -30,6 +30,11 @@ The package is available on PyPI and can be installed with:
$ pip install fasthx
```

The package has optional dependencies for the following **official integrations**:

- [htmy](https://volfpeter.github.io/htmy/): `pip install fasthx[htmy]`.
- [jinja][https://jinja.palletsprojects.com/en/stable/]: `pip install fasthx[jinja]`.

## Dependencies

The only dependency of this package is `fastapi`.
Expand Down
Loading