Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions fasthtml/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Cards: ... # class route group: each verb-named method (get, patch, .
A('More', href=foo.to(a=1)) # <a href="/foo?a=1"> `.to()` fills path params; the rest become query params.
Div('...', hx_get=foo) # route functions work directly as htmx attr values: hx-get="/foo"

Handlers return FT trees (from `fastcore.xml`), rendered as HTML automatically: a full page for regular requests, a bare fragment for HTMX requests. In a full page, top-level `Title`, `Meta`, `Link`, and `Style` items go in `<head>`; the rest goes in `<body>`. Strings are sent as HTML, dicts as JSON, and a Starlette `Response` is sent untouched. A return tuple may mix content with special items: any `HttpHeader` (e.g. from `cookie()` or `HtmxResponseHeaders()`) becomes a response header, any `BackgroundTask` runs after the response is sent, and the rest renders as content. `Redirect` picks the right redirect mechanism for HTMX vs regular requests, and `FtResponse` wraps FT content when you need the status code or headers.
Handlers return FT trees (from `fastcore.xml`), rendered as HTML automatically: a full page for regular requests and htmx 4 `full` requests, a bare fragment for other HTMX requests. In a full page, top-level `Title`, `Meta`, `Link`, and `Style` items go in `<head>`; the rest goes in `<body>`. Strings are sent as HTML, dicts as JSON, and a Starlette `Response` is sent untouched. A return tuple may mix content with special items: any `HttpHeader` (e.g. from `cookie()` or `HtmxResponseHeaders()`) becomes a response header, any `BackgroundTask` runs after the response is sent, and the rest renders as content. `Redirect` picks the right redirect mechanism for HTMX vs regular requests, and `FtResponse` wraps FT content when you need the status code or headers.

Handler parameters are filled from the request: each is looked up in path, cookie, header (snake_case names match Hyphen-Case headers), query, then form data, and cast by calling its annotation on the value. Hyphens in form/query names match underscores in handler parameters and typed body fields. Exact names take precedence within each source. Dictionary and request access retain the original keys. `bool`, `int`, and `date` get smart string parsing; `UploadFile` passes through. Repeated params take the last value unless annotated `list[T]`, which collects them all. A param with no annotation and no special name is ignored, with a warning. A dataclass, TypedDict, namedtuple, or any annotated class collects the whole form body; a `__from_request__` classmethod customizes construction. A missing required param is a 400, a failed cast a 404. These special names need no annotation:

Expand Down Expand Up @@ -482,7 +482,7 @@ def respond(req, heads, bdy):
def is_full_page(req, resp):
"Check if response should be rendered as full page or fragment"
if resp and any(getattr(o, 'tag', '')=='html' for o in resp): return True
return 'hx-request' in req.headers and 'hx-history-restore-request' not in req.headers
return 'hx-request' in req.headers and req.headers.get('hx-request-type') != 'full' and 'hx-history-restore-request' not in req.headers

# %% ../nbs/api/00_core.ipynb #358badec
def _part_resp(req, resp):
Expand All @@ -491,7 +491,7 @@ def _part_resp(req, resp):
resp = resp + tuple(getattr(req, 'injects', ()))
http_hdrs,resp = partition(resp, risinstance(HttpHeader))
tasks,resp = partition(resp, risinstance(BackgroundTask))
kw = {"headers": {"vary": "HX-Request, HX-History-Restore-Request"}}
kw = {"headers": {"vary": "HX-Request, HX-Request-Type, HX-History-Restore-Request"}}
if http_hdrs: kw['headers'] |= {o.k:str(o.v) for o in http_hdrs}
if tasks:
ts = BackgroundTasks()
Expand Down
13 changes: 8 additions & 5 deletions nbs/api/00_core.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
" A('More', href=foo.to(a=1)) # <a href=\"/foo?a=1\"> `.to()` fills path params; the rest become query params.\n",
" Div('...', hx_get=foo) # route functions work directly as htmx attr values: hx-get=\"/foo\"\n",
"\n",
"Handlers return FT trees (from `fastcore.xml`), rendered as HTML automatically: a full page for regular requests, a bare fragment for HTMX requests. In a full page, top-level `Title`, `Meta`, `Link`, and `Style` items go in `<head>`; the rest goes in `<body>`. Strings are sent as HTML, dicts as JSON, and a Starlette `Response` is sent untouched. A return tuple may mix content with special items: any `HttpHeader` (e.g. from `cookie()` or `HtmxResponseHeaders()`) becomes a response header, any `BackgroundTask` runs after the response is sent, and the rest renders as content. `Redirect` picks the right redirect mechanism for HTMX vs regular requests, and `FtResponse` wraps FT content when you need the status code or headers.\n",
"Handlers return FT trees (from `fastcore.xml`), rendered as HTML automatically: a full page for regular requests and htmx 4 `full` requests, a bare fragment for other HTMX requests. In a full page, top-level `Title`, `Meta`, `Link`, and `Style` items go in `<head>`; the rest goes in `<body>`. Strings are sent as HTML, dicts as JSON, and a Starlette `Response` is sent untouched. A return tuple may mix content with special items: any `HttpHeader` (e.g. from `cookie()` or `HtmxResponseHeaders()`) becomes a response header, any `BackgroundTask` runs after the response is sent, and the rest renders as content. `Redirect` picks the right redirect mechanism for HTMX vs regular requests, and `FtResponse` wraps FT content when you need the status code or headers.\n",
"\n",
"Handler parameters are filled from the request: each is looked up in path, cookie, header (snake_case names match Hyphen-Case headers), query, then form data, and cast by calling its annotation on the value. Hyphens in form/query names match underscores in handler parameters and typed body fields. Exact names take precedence within each source. Dictionary and request access retain the original keys. `bool`, `int`, and `date` get smart string parsing; `UploadFile` passes through. Repeated params take the last value unless annotated `list[T]`, which collects them all. A param with no annotation and no special name is ignored, with a warning. A dataclass, TypedDict, namedtuple, or any annotated class collects the whole form body; a `__from_request__` classmethod customizes construction. A missing required param is a 400, a failed cast a 404. These special names need no annotation:\n",
"\n",
Expand Down Expand Up @@ -1555,8 +1555,8 @@
"id": "cfae0a56",
"metadata": {},
"source": [
"Render fragment if `HX-Request` header is _present_ and\n",
" `HX-History-Restore-Request` header is _absent._"
"Render a fragment if `HX-Request` is present, `HX-Request-Type` is not `full`,\n",
"and `HX-History-Restore-Request` is absent."
]
},
{
Expand All @@ -1570,7 +1570,7 @@
"def is_full_page(req, resp):\n",
" \"Check if response should be rendered as full page or fragment\"\n",
" if resp and any(getattr(o, 'tag', '')=='html' for o in resp): return True\n",
" return 'hx-request' in req.headers and 'hx-history-restore-request' not in req.headers"
" return 'hx-request' in req.headers and req.headers.get('hx-request-type') != 'full' and 'hx-history-restore-request' not in req.headers"
]
},
{
Expand All @@ -1587,7 +1587,7 @@
" resp = resp + tuple(getattr(req, 'injects', ()))\n",
" http_hdrs,resp = partition(resp, risinstance(HttpHeader))\n",
" tasks,resp = partition(resp, risinstance(BackgroundTask))\n",
" kw = {\"headers\": {\"vary\": \"HX-Request, HX-History-Restore-Request\"}}\n",
" kw = {\"headers\": {\"vary\": \"HX-Request, HX-Request-Type, HX-History-Restore-Request\"}}\n",
" if http_hdrs: kw['headers'] |= {o.k:str(o.v) for o in http_hdrs}\n",
" if tasks:\n",
" ts = BackgroundTasks()\n",
Expand Down Expand Up @@ -3133,6 +3133,9 @@
"assert '<title>FastHTML page</title>' in txt and '<h1>bar</h1>' in txt and '<html>' in txt\n",
"\n",
"assert cli.get('/xt2', **hxhdr).text.strip() == '<h1>bar</h1>'\n",
"assert cli.get('/xt2', headers={'hx-request':\"1\", 'hx-request-type':\"partial\"}).text.strip() == '<h1>bar</h1>'\n",
"r = cli.get('/xt2', headers={'hx-request':\"1\", 'hx-request-type':\"full\"})\n",
"assert '<html>' in r.text and 'HX-Request-Type' in r.headers['vary']\n",
"\n",
"@rt('/xt3')\n",
"def get(): return Html(Head(Title('hi')), Body(P('there')))\n",
Expand Down
Loading