Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4c6f8f3
migrate all htmx v2 -> v4 from fasthtml-examples
dienhoa Mar 30, 2026
4daef6c
Merge branch 'AnswerDotAI:main' into four
dienhoa Mar 30, 2026
6c8fe5e
add missing htmx4src
dienhoa Mar 31, 2026
138b403
HtmxOn and PicoBusy support htmx4
dienhoa Mar 31, 2026
0c0c964
fix metaChar in sid_src
dienhoa Apr 1, 2026
32c2421
remove checking both htmx and htmx4 in def_hdrs
dienhoa Apr 8, 2026
014b027
update htmx to beta1
dienhoa Apr 9, 2026
b3c35f0
fix conflicts ws headers
dienhoa Apr 9, 2026
8c649cb
setting metaChar PicoBusy based on htmx4 directly
dienhoa Apr 9, 2026
d969b20
pass htmx4 to HtmxOn
dienhoa Apr 9, 2026
3f81605
add htmxv4_migration doc
dienhoa Apr 13, 2026
b3882bc
fix conflicts
dienhoa Apr 13, 2026
192705e
typo migration doc
dienhoa Apr 13, 2026
a79e4e7
iframe_scr support htmxv4
dienhoa Apr 14, 2026
b709bf6
update htmx_config_port with htmx4
dienhoa Apr 15, 2026
a9671bb
hx4 beta2
dienhoa Apr 17, 2026
7e49e70
htmx v4 alpha3
dienhoa May 13, 2026
f429da0
Merge branch 'main' into four
dienhoa May 20, 2026
ace2ea7
update import pico
dienhoa May 20, 2026
f15f07c
bump htmx4 beta4
dienhoa May 22, 2026
df92c47
add htmx4 attrs/evts in ft signature
dienhoa May 23, 2026
135729d
add compat checker
dienhoa May 23, 2026
44405c1
Merge branch 'main' into four
dienhoa May 24, 2026
ad7d631
add checker script
dienhoa May 24, 2026
4e19b60
resolve conflicts
dienhoa Jun 18, 2026
c5021e0
upgrade to htmxv4 beta5
dienhoa Jul 20, 2026
b7162da
Merge remote-tracking branch 'origin/main' into four
dienhoa Jul 24, 2026
837c99c
htmxv4 beta 6
dienhoa Jul 24, 2026
5b97a7f
update multipart url
dienhoa Jul 28, 2026
2b4287a
demo hx multipart timelapse
dienhoa Jul 28, 2026
f0fd313
Merge branch 'main' into four
jph00 Aug 10, 2026
d4be4d0
merge main into four
jph00 Aug 10, 2026
f5b8b2f
merge main into four
jph00 Aug 10, 2026
4a15c7c
merge main into four
jph00 Aug 10, 2026
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
37 changes: 23 additions & 14 deletions fasthtml/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/01_components.ipynb.

# %% auto #0
__all__ = ['named', 'html_attrs', 'hx_attrs', 'hx_evts', 'js_evts', 'hx_attrs_annotations', 'hx_evt_attrs', 'js_evt_attrs',
'evt_attrs', 'attrmap_x', 'ft_html', 'ft_hx', 'File', 'show', 'fill_form', 'fill_dataclass', 'find_inputs',
'html2ft', 'sse_message', 'A', 'Abbr', 'Address', 'Area', 'Article', 'Aside', 'Audio', 'B', 'Base', 'Bdi',
'Bdo', 'Blockquote', 'Body', 'Br', 'Button', 'Canvas', 'Caption', 'Cite', 'Code', 'Col', 'Colgroup', 'Data',
'Datalist', 'Dd', 'Del', 'Details', 'Dfn', 'Dialog', 'Div', 'Dl', 'Dt', 'Em', 'Embed', 'Fencedframe',
'Fieldset', 'Figcaption', 'Figure', 'Footer', 'Form', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Head', 'Header',
'Hgroup', 'Hr', 'I', 'Iframe', 'Img', 'Input', 'Ins', 'Kbd', 'Label', 'Legend', 'Li', 'Link', 'Main', 'Map',
'Mark', 'Menu', 'Meta', 'Meter', 'Nav', 'Noscript', 'Object', 'Ol', 'Optgroup', 'Option', 'Output', 'P',
'Picture', 'PortalExperimental', 'Pre', 'Progress', 'Q', 'Rp', 'Rt', 'Ruby', 'S', 'Samp', 'Script', 'Search',
'Section', 'Select', 'Slot', 'Small', 'Source', 'Span', 'Strong', 'Style', 'Sub', 'Summary', 'Sup', 'Table',
'Tbody', 'Td', 'Template', 'Textarea', 'Tfoot', 'Th', 'Thead', 'Time', 'Title', 'Tr', 'Track', 'U', 'Ul',
'Var', 'Video', 'Wbr']
__all__ = ['named', 'html_attrs', 'hx_attrs', 'hx_attrs_4only', 'hx_evts', 'hx_evts_4only', 'js_evts', 'hx_attrs_annotations',
'hx_evt_attrs', 'js_evt_attrs', 'evt_attrs', 'HxPartial', 'attrmap_x', 'ft_html', 'ft_hx', 'File', 'show',
'fill_form', 'fill_dataclass', 'find_inputs', 'html2ft', 'sse_message', 'A', 'Abbr', 'Address', 'Area',
'Article', 'Aside', 'Audio', 'B', 'Base', 'Bdi', 'Bdo', 'Blockquote', 'Body', 'Br', 'Button', 'Canvas',
'Caption', 'Cite', 'Code', 'Col', 'Colgroup', 'Data', 'Datalist', 'Dd', 'Del', 'Details', 'Dfn', 'Dialog',
'Div', 'Dl', 'Dt', 'Em', 'Embed', 'Fencedframe', 'Fieldset', 'Figcaption', 'Figure', 'Footer', 'Form', 'H1',
'H2', 'H3', 'H4', 'H5', 'H6', 'Head', 'Header', 'Hgroup', 'Hr', 'I', 'Iframe', 'Img', 'Input', 'Ins', 'Kbd',
'Label', 'Legend', 'Li', 'Link', 'Main', 'Map', 'Mark', 'Menu', 'Meta', 'Meter', 'Nav', 'Noscript', 'Object',
'Ol', 'Optgroup', 'Option', 'Output', 'P', 'Picture', 'PortalExperimental', 'Pre', 'Progress', 'Q', 'Rp',
'Rt', 'Ruby', 'S', 'Samp', 'Script', 'Search', 'Section', 'Select', 'Slot', 'Small', 'Source', 'Span',
'Strong', 'Style', 'Sub', 'Summary', 'Sup', 'Table', 'Tbody', 'Td', 'Template', 'Textarea', 'Tfoot', 'Th',
'Thead', 'Time', 'Title', 'Tr', 'Track', 'U', 'Ul', 'Var', 'Video', 'Wbr']

# %% ../nbs/api/01_components.ipynb #8e2d405b
from dataclasses import dataclass, asdict, is_dataclass, make_dataclass, replace, astuple, MISSING
Expand Down Expand Up @@ -46,10 +46,13 @@ def __add__(self:FT, b): return f'{self}{b}'
named = set('a button form frame iframe img input map meta object param select textarea'.split())
html_attrs = 'id cls title style accesskey contenteditable dir draggable enterkeyhint hidden inert inputmode lang popover spellcheck tabindex translate'.split()
hx_attrs = 'get post put delete patch trigger target swap swap_oob include select select_oob indicator push_url confirm disable replace_url vals disabled_elt ext headers history history_elt inherit params preserve prompt request sync validate'
hx_attrs_4only = 'action method config ignore' # https://four.htmx.org/docs/get-started/migration#attributes

hx_evts = 'abort afterOnLoad afterProcessNode afterRequest afterSettle afterSwap beforeCleanupElement beforeOnLoad beforeProcessNode beforeRequest beforeSwap beforeSend beforeTransition configRequest confirm historyCacheError historyCacheMiss historyCacheMissError historyCacheMissLoad historyRestore beforeHistorySave load noSSESourceError onLoadError oobAfterSwap oobBeforeSwap oobErrorNoTarget prompt pushedIntoHistory replacedInHistory responseError sendAbort sendError sseError sseOpen swapError targetError timeout validation:validate validation:failed validation:halted xhr:abort xhr:loadend xhr:loadstart xhr:progress'
# https://four.htmx.org/docs/get-started/migration#renamed-events
hx_evts_4only = 'after:init before:cleanup before:history:update before:init before:process before:history:restore after:history:push after:history:replace error'
js_evts = "blur change contextmenu focus input invalid reset select submit keydown keypress keyup click dblclick mousedown mouseenter mouseleave mousemove mouseout mouseover mouseup wheel"
hx_attrs = [f'hx_{o}' for o in hx_attrs.split()]
hx_attrs = [f'hx_{o}' for o in hx_attrs.split() + hx_attrs_4only.split()]
hx_attrs_annotations = {
"hx_swap": Literal["innerHTML", "outerHTML", "afterbegin", "beforebegin", "beforeend", "afterend", "delete", "none"] | str,
"hx_swap_oob": Literal["true", "innerHTML", "outerHTML", "afterbegin", "beforebegin", "beforeend", "afterend", "delete", "none"] | str,
Expand All @@ -64,7 +67,7 @@ def __add__(self:FT, b): return f'{self}{b}'
hx_attrs_annotations = {k: Optional[v] for k,v in hx_attrs_annotations.items()}
hx_attrs = html_attrs + hx_attrs

hx_evt_attrs = ['hx_on__'+camel2snake(o).replace(':','_') for o in hx_evts.split()]
hx_evt_attrs = ['hx_on__'+camel2snake(o).replace(':','_') for o in hx_evts.split() + hx_evts_4only.split()]
js_evt_attrs = ['hx_on_'+o for o in js_evts.split()]
evt_attrs = js_evt_attrs+hx_evt_attrs

Expand Down Expand Up @@ -117,6 +120,10 @@ def ft_hx(tag: str, *c, target_id=None, hx_vals=None, hx_target=None, **kwargs):
'Td', 'Template', 'Textarea', 'Tfoot', 'Th', 'Thead', 'Time', 'Title', 'Tr', 'Track', 'U', 'Ul', 'Var', 'Video', 'Wbr']
for o in _all_: _g[o] = partial(ft_hx, o.lower())

# %% ../nbs/api/01_components.ipynb #dfa8f01c
HxPartial = partial(ft_hx, 'hx-partial') # For hx v4, manually mapping because Python doesn't allow hyphens


# %% ../nbs/api/01_components.ipynb #fab04fb3
def File(fname):
"Use the unescaped text in file `fname` directly"
Expand Down Expand Up @@ -243,4 +250,6 @@ def _parse(elm, lvl=0, indent=4):
def sse_message(elm, event='message'):
"Convert element `elm` into a format suitable for SSE streaming"
data = '\n'.join(f'data: {o}' for o in to_xml(elm).splitlines())
# SSE delivers a message with no event field as type 'message', so the line is redundant for the default (and htmx v4 requires its absence for plain swaps)
if event=='message': return f'{data}\n\n'
return f'event: {event}\n{data}\n\n'
66 changes: 48 additions & 18 deletions fasthtml/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/00_core.ipynb.

# %% auto #0
__all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'DEF_MAXPART', 'htmx_exts', 'htmxsrc', 'fhjsscr', 'surrsrc', 'scopesrc',
'viewport', 'charset', 'cors_allow', 'iframe_scr', 'all_meths', 'devtools_loc', 'parsed_date',
__all__ = ['empty', 'htmx_hdrs', 'fh_cfg', 'htmx_resps', 'DEF_MAXPART', 'htmx_exts', 'htmxsrc', 'htmx4src', 'fhjsscr', 'surrsrc',
'scopesrc', 'viewport', 'charset', 'cors_allow', 'iframe_scr', 'all_meths', 'devtools_loc', 'parsed_date',
'snake2hyphens', 'HtmxHeaders', 'HttpHeader', 'HtmxResponseHeaders', 'form2dict', 'parse_form', 'ApiReturn',
'JSONResponse', 'flat_xt', 'Beforeware', 'EventStream', 'signal_shutdown', 'uri', 'decode_uri', 'flat_tuple',
'noop_body', 'respond', 'is_full_page', 'Redirect', 'get_key', 'qp', 'def_hdrs', 'Lifespan', 'FastHTML',
Expand Down Expand Up @@ -64,14 +64,17 @@ def snake2hyphens(s:str):
history_restore_request="HX-History-Restore-Request",
prompt="HX-Prompt",
request="HX-Request",
request_type="HX-Request-Type", # v4: partial or full update
source="HX-Source", # v4: replaces v2 HX-Trigger but with tag#id instead just id
target="HX-Target",
trigger_name="HX-Trigger-Name",
trigger="HX-Trigger")

@dataclass
class HtmxHeaders:
boosted:str|None=None; current_url:str|None=None; history_restore_request:str|None=None; prompt:str|None=None
request:str|None=None; target:str|None=None; trigger_name:str|None=None; trigger:str|None=None
request:str|None=None; request_type:str|None=None; source:str|None=None; target:str|None=None;
trigger_name:str|None=None; trigger:str|None=None
def __bool__(self): return any(hasattr(self,o) for o in htmx_hdrs)

def _get_htmx(h):
Expand Down Expand Up @@ -242,6 +245,7 @@ async def _find_p(conn, data, hdrs, arg:str, p:Parameter):
if res in (empty,None): res = conn.query_params.getlist(arg)
if res==[]: res = None
if res in (empty,None): res = data.get(arg, None)
if res in (empty,None) and isinstance(vals := data.get('values'), dict): res = vals.get(arg) # htmx v4 nests form data under `values`
if res in (empty,None):
if p.default is empty:
if isinstance(conn, Request): raise HTTPException(400, f"Missing required field: {arg}")
Expand Down Expand Up @@ -283,7 +287,8 @@ async def _handle(f, /, *args, **kwargs):

# %% ../nbs/api/00_core.ipynb #ad0f0e87
async def _wrap_ws(ws, data, params):
hdrs = Headers({k.lower():v for k,v in data.pop('HEADERS', {}).items() if v is not None})
raw = data.pop('HEADERS', {}) or (data.pop('headers') if isinstance(data.get('headers'), dict) else {}) # htmx v4 sends lowercase `headers`
hdrs = Headers({k.lower():v for k,v in raw.items() if v is not None})
return await _find_ps(ws, data, hdrs, params)

# %% ../nbs/api/00_core.ipynb #dcc15129
Expand Down Expand Up @@ -504,11 +509,16 @@ async def _wrap_call(f, req, params):
"remove-me": "https://cdn.jsdelivr.net/npm/htmx-ext-remove-me@2.0.0/remove-me.js",
"debug": "https://unpkg.com/htmx.org@1.9.12/dist/ext/debug.js",
"ws": "https://cdn.jsdelivr.net/npm/htmx-ext-ws@2.0.3/ws.js",
"chunked-transfer": "https://cdn.jsdelivr.net/npm/htmx-ext-transfer-encoding-chunked@0.4.0/transfer-encoding-chunked.js"
"ws4": "https://unpkg.com/htmx.org@4.0.0-beta6/dist/ext/hx-ws.js",
"chunked-transfer": "https://cdn.jsdelivr.net/npm/htmx-ext-transfer-encoding-chunked@0.4.0/transfer-encoding-chunked.js",
"sse4": "https://unpkg.com/htmx.org@4.0.0-beta6/dist/ext/hx-sse.js",
"live": "https://unpkg.com/htmx.org@4.0.0-beta6/dist/ext/hx-live.js",
"multipart": "https://cdn.jsdelivr.net/gh/bigskysoftware/htmx@four/src/ext/hx-multipart.min.js"
}

# %% ../nbs/api/00_core.ipynb #60cb52ea
htmxsrc = Script(src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js")
htmx4src = Script(src="https://unpkg.com/htmx.org@4.0.0-beta6/dist/htmx.js")
fhjsscr = Script(src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js")
surrsrc = Script(src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js")
scopesrc = Script(src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js")
Expand Down Expand Up @@ -553,11 +563,17 @@ def _sub(m):
return p + ('?' + urlencode({k:'' if v in (False,None) else v for k,v in kw.items()},doseq=True) if kw else '')

# %% ../nbs/api/00_core.ipynb #f86690c4
def def_hdrs(htmx=True, surreal=True):
def def_hdrs(htmx=True, htmx4=False, surreal=True):
"Default headers for a FastHTML app"
hdrs = []
if surreal: hdrs = [surrsrc,scopesrc] + hdrs
if htmx: hdrs = [htmxsrc,fhjsscr] + hdrs
if htmx4:
# metaCharacter="-" makes htmx4 use dashes instead of colons
meta_cfg = Meta(name="htmx-config", content=json.dumps({"metaCharacter": "-"}))
hdrs = [meta_cfg, htmx4src,fhjsscr] + hdrs
# TODO: Check if fhjsscr works with htmx4
elif htmx:
hdrs = [htmxsrc,fhjsscr] + hdrs
return [charset, viewport] + hdrs

# %% ../nbs/api/00_core.ipynb #2c5285ae
Expand All @@ -568,11 +584,24 @@ def def_hdrs(htmx=True, surreal=True):
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
}

window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};"""))

const ver = window.htmx?.version || '';
const isV4 = ver.startsWith('4.');
const mc = window.htmx?.config?.metaCharacter || ':';

if (isV4) {
document.body.addEventListener(`htmx${mc}after${mc}swap`, sendmsg);
document.body.addEventListener(`htmx${mc}after${mc}ws${mc}message`, sendmsg);
} else {
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
}
};
"""))


# %% ../nbs/api/00_core.ipynb #17ced9a3
class _LifespanCtx:
Expand Down Expand Up @@ -605,19 +634,21 @@ def on_event(self, event_type):
class FastHTML(Starlette):
def __init__(self, debug=False, routes=None, middleware=None, title: str = "FastHTML page", exception_handlers=None,
on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, ftrs=None, exts=None,
before=None, after=None, surreal=True, htmx=True, default_hdrs=True, sess_cls=SessionMiddleware,
before=None, after=None, surreal=True, htmx=True, htmx4=False, default_hdrs=True, sess_cls=SessionMiddleware,
secret_key=None, session_cookie='session_', max_age=365*24*3600, sess_path='/',
same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey',
body_wrap=noop_body, htmlkw=None, nb_hdrs=False, canonical=True, max_part_size=DEF_MAXPART, **bodykw):
middleware,before,after = map(_list, (middleware,before,after))
self.title,self.canonical,self.session_cookie,self.key_fname = title,canonical,session_cookie,key_fname
hdrs,ftrs,exts = map(listify, (hdrs,ftrs,exts))
if htmx4 and exts:
exts = ['ws4' if e in ('ws', 'ws4') else e for e in exts]
exts = {k:htmx_exts[k] for k in exts}
htmlkw = htmlkw or {}
if default_hdrs: hdrs = def_hdrs(htmx, surreal=surreal) + hdrs
if default_hdrs: hdrs = def_hdrs(htmx, htmx4, surreal=surreal) + hdrs
hdrs += [Script(src=ext) for ext in exts.values()]
if IN_NOTEBOOK:
hdrs.append(iframe_scr)
hdrs.append(iframe_scr) # TODO: check iframe_scr work with htmx4
from IPython.display import display,HTML
if nb_hdrs: display(HTML(to_xml(tuple(hdrs))))
middleware.append(cors_allow)
Expand All @@ -627,8 +658,8 @@ def __init__(self, debug=False, routes=None, middleware=None, title: str = "Fast
self.secret_key = get_key(secret_key, key_fname)
if sess_cls:
sess = Middleware(sess_cls, secret_key=self.secret_key,session_cookie=session_cookie,
max_age=max_age, path=sess_path, same_site=same_site,
https_only=sess_https_only, domain=sess_domain)
max_age=max_age, path=sess_path, same_site=same_site,
https_only=sess_https_only, domain=sess_domain)
middleware.append(sess)
exception_handlers = ifnone(exception_handlers, {})
if 404 not in exception_handlers:
Expand Down Expand Up @@ -810,11 +841,10 @@ def serve(
"Run the app in an async server, with live reload set as the default."
from uvicorn import run
bk = inspect.currentframe().f_back
glb = bk.f_globals
code = bk.f_code
glb,back = bk.f_globals,bk.f_back
if not appname:
if glb.get('__name__')=='__main__': appname = Path(glb.get('__file__', '')).stem
elif code.co_name=='main' and bk.f_back.f_globals.get('__name__')=='__main__': appname = inspect.getmodule(bk).__name__
elif back and back.f_globals.get('__name__')=='__main__': appname = inspect.getmodule(bk).__name__
if appname:
if not port: port=int(os.getenv("PORT", default=5001))
link = f'http://{"localhost" if host=="0.0.0.0" else host}:{port}'
Expand Down
4 changes: 2 additions & 2 deletions fasthtml/fastapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def fast_app(
pico:Optional[bool]=None, # Include PicoCSS header?
surreal:Optional[bool]=True, # Include surreal.js/scope headers?
htmx:Optional[bool]=True, # Include HTMX header?
htmx4:Optional[bool]=False,
exts:Optional[list|str]=None, # HTMX extension names to include
canonical:bool=True, # Automatically include canonical link?
secret_key:Optional[str]=None, # Signing key for sessions
Expand All @@ -70,7 +71,7 @@ def fast_app(
app = _app_factory(hdrs=h, ftrs=ftrs, before=before, middleware=middleware, live=live, debug=debug, title=title, routes=routes, exception_handlers=exception_handlers,
on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan, default_hdrs=default_hdrs, secret_key=secret_key, canonical=canonical,
session_cookie=session_cookie, max_age=max_age, sess_path=sess_path, same_site=same_site, sess_https_only=sess_https_only,
sess_domain=sess_domain, key_fname=key_fname, exts=exts, surreal=surreal, htmx=htmx, htmlkw=htmlkw,
sess_domain=sess_domain, key_fname=key_fname, exts=exts, surreal=surreal, htmx=htmx, htmx4=htmx4, htmlkw=htmlkw,
reload_attempts=reload_attempts, reload_interval=reload_interval, body_wrap=body_wrap, nb_hdrs=nb_hdrs, **(bodykw or {}))
app.static_route_exts(static_path=static_path)
if not db_file: return app,app.route
Expand All @@ -86,4 +87,3 @@ def fast_app(
dbtbls = [_get_tbl(db.t, k, v) for k,v in tbls.items()]
if len(dbtbls)==1: dbtbls=dbtbls[0]
return app,app.route,*dbtbls

16 changes: 10 additions & 6 deletions fasthtml/jupyter.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,17 @@ def _repr_html_(self:FT):
def htmx_config_port(port=8000):
display(HTML('''
<script>
document.body.addEventListener('htmx:configRequest', (event) => {
if(event.detail.path.includes('://')) return;
htmx.config.selfRequestsOnly=false;
event.detail.path = `${location.protocol}//${location.hostname}:%s${event.detail.path}`;
});
["htmx:configRequest","htmx-config-request"].forEach(evt => document.body.addEventListener(evt, (event) => {
const ctx = event.detail.ctx;
const path = ctx ? ctx.request.action : event.detail.path;
if (path.includes('://')) return;
const full = `${location.protocol}//${location.hostname}:%s${path}`;
if (ctx) { ctx.request.mode = "cors"; ctx.request.action = full; }
else { htmx.config.selfRequestsOnly = false; event.detail.path = full; }
}));
</script>''' % port))


# %% ../nbs/api/06_jupyter.ipynb #79406618
class JupyUvi:
"Start and stop a Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`"
Expand All @@ -73,7 +77,7 @@ def __init__(self, app, log_level="error", host='0.0.0.0', port=8000, start=True
if live: self._setup_live(app)
if start: self.start()
if not os.environ.get('IN_SOLVEIT'): htmx_config_port(port)

def start(self):
self.server = nb_serve(self.app, log_level=self.log_level, host=self.host, port=self.port,daemon=self.daemon, **self.kwargs)

Expand Down
7 changes: 4 additions & 3 deletions fasthtml/pico.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ def Container(*args, **kwargs)->FT:
"A PicoCSS Container, implemented as a Main with class 'container'"
return Main(*args, cls="container", **kwargs)

# %% ../nbs/api/04_pico.ipynb #138dc298
# %% ../nbs/api/04_pico.ipynb #b8c98614
def PicoBusy():
return (HtmxOn('beforeRequest', "event.detail.elt.setAttribute('aria-busy', 'true' )"),
HtmxOn('afterRequest', "event.detail.elt.setAttribute('aria-busy', 'false')"))
elt = "(event.detail.ctx?.sourceElement || event.detail.elt)"
return (HtmxOn('beforeRequest', f"{elt}.setAttribute('aria-busy', 'true' )"),
HtmxOn('afterRequest', f"{elt}.setAttribute('aria-busy', 'false')"))
Loading