from collections import UserDict
from lxml import html as lx
from pprint import pprintComponents
ft_html and ft_hx functions to add some conveniences to ft, along with a full set of basic HTML components, and functions to work with forms and FT conversion
Str and repr
In notebooks, FT components are rendered as their syntax highlighted XML/HTML:
sentence = P(Strong("FastHTML is ", I("Fast")), id='sentence_id')
sentenceFastHTML is Fast
Elsewhere, they are represented as their underlying data structure:
print(repr(sentence))<p id="sentence_id"><strong>FastHTML is <i>Fast</i></strong></p>
FT.__str__
def __str__():Return str(self).
If they have an id, then that id is used as the componentโs str representation:
f'hx_target=#{sentence}''hx_target=#sentence_id'
FT.__radd__
def __radd__(
b
):'hx_target=#' + sentence'hx_target=#sentence_id'
FT.__add__
def __add__(
b
):sentence + '...''sentence_id...'
fh_html and fh_hx
test_eq(len(hx_attrs), len(set(hx_attrs)))attrmap_x
def attrmap_x(
o
):ft_html
def ft_html(
tag:str, *c, id:NoneType=None, cls:NoneType=None, title:NoneType=None, style:NoneType=None,
attrmap:NoneType=None, valmap:NoneType=None, ft_cls:NoneType=None, **kwargs
):c = Div(id='someid')ft_hx
def ft_hx(
tag:str, *c, target_id:NoneType=None, hx_vals:NoneType=None, hx_target:NoneType=None, id:NoneType=None,
cls:NoneType=None, title:NoneType=None, style:NoneType=None, accesskey:NoneType=None,
contenteditable:NoneType=None, dir:NoneType=None, draggable:NoneType=None, enterkeyhint:NoneType=None,
hidden:NoneType=None, inert:NoneType=None, inputmode:NoneType=None, lang:NoneType=None, popover:NoneType=None,
spellcheck:NoneType=None, tabindex:NoneType=None, translate:NoneType=None, hx_get:NoneType=None,
hx_post:NoneType=None, hx_put:NoneType=None, hx_delete:NoneType=None, hx_patch:NoneType=None,
hx_trigger:NoneType=None, hx_swap:NoneType=None, hx_swap_oob:NoneType=None, hx_include:NoneType=None,
hx_select:NoneType=None, hx_select_oob:NoneType=None, hx_indicator:NoneType=None, hx_push_url:NoneType=None,
hx_confirm:NoneType=None, hx_disable:NoneType=None, hx_replace_url:NoneType=None, hx_disabled_elt:NoneType=None,
hx_ext:NoneType=None, hx_headers:NoneType=None, hx_history:NoneType=None, hx_history_elt:NoneType=None,
hx_inherit:NoneType=None, hx_params:NoneType=None, hx_preserve:NoneType=None, hx_prompt:NoneType=None,
hx_request:NoneType=None, hx_sync:NoneType=None, hx_validate:NoneType=None, hx_action:NoneType=None,
hx_method:NoneType=None, hx_config:NoneType=None, hx_ignore:NoneType=None, hx_on_blur:NoneType=None,
hx_on_change:NoneType=None, hx_on_contextmenu:NoneType=None, hx_on_focus:NoneType=None,
hx_on_input:NoneType=None, hx_on_invalid:NoneType=None, hx_on_reset:NoneType=None, hx_on_select:NoneType=None,
hx_on_submit:NoneType=None, hx_on_keydown:NoneType=None, hx_on_keypress:NoneType=None, hx_on_keyup:NoneType=None,
hx_on_click:NoneType=None, hx_on_dblclick:NoneType=None, hx_on_mousedown:NoneType=None,
hx_on_mouseenter:NoneType=None, hx_on_mouseleave:NoneType=None, hx_on_mousemove:NoneType=None,
hx_on_mouseout:NoneType=None, hx_on_mouseover:NoneType=None, hx_on_mouseup:NoneType=None,
hx_on_wheel:NoneType=None, hx_on__abort:NoneType=None, hx_on__after_on_load:NoneType=None,
hx_on__after_process_node:NoneType=None, hx_on__after_request:NoneType=None, hx_on__after_settle:NoneType=None,
hx_on__after_swap:NoneType=None, hx_on__before_cleanup_element:NoneType=None,
hx_on__before_on_load:NoneType=None, hx_on__before_process_node:NoneType=None,
hx_on__before_request:NoneType=None, hx_on__before_swap:NoneType=None, hx_on__before_send:NoneType=None,
hx_on__before_transition:NoneType=None, hx_on__config_request:NoneType=None, hx_on__confirm:NoneType=None,
hx_on__history_cache_error:NoneType=None, hx_on__history_cache_miss:NoneType=None,
hx_on__history_cache_miss_error:NoneType=None, hx_on__history_cache_miss_load:NoneType=None,
hx_on__history_restore:NoneType=None, hx_on__before_history_save:NoneType=None, hx_on__load:NoneType=None,
hx_on__no_sse_source_error:NoneType=None, hx_on__on_load_error:NoneType=None,
hx_on__oob_after_swap:NoneType=None, hx_on__oob_before_swap:NoneType=None,
hx_on__oob_error_no_target:NoneType=None, hx_on__prompt:NoneType=None, hx_on__pushed_into_history:NoneType=None,
hx_on__replaced_in_history:NoneType=None, hx_on__response_error:NoneType=None, hx_on__send_abort:NoneType=None,
hx_on__send_error:NoneType=None, hx_on__sse_error:NoneType=None, hx_on__sse_open:NoneType=None,
hx_on__swap_error:NoneType=None, hx_on__target_error:NoneType=None, hx_on__timeout:NoneType=None,
hx_on__validation_validate:NoneType=None, hx_on__validation_failed:NoneType=None,
hx_on__validation_halted:NoneType=None, hx_on__xhr_abort:NoneType=None, hx_on__xhr_loadend:NoneType=None,
hx_on__xhr_loadstart:NoneType=None, hx_on__xhr_progress:NoneType=None, hx_on__after_init:NoneType=None,
hx_on__before_cleanup:NoneType=None, hx_on__before_history_update:NoneType=None,
hx_on__before_init:NoneType=None, hx_on__before_process:NoneType=None,
hx_on__before_history_restore:NoneType=None, hx_on__after_history_push:NoneType=None,
hx_on__after_history_replace:NoneType=None, hx_on__error:NoneType=None, **kwargs
):For tags that have a name attribute, it will be set to the value of id if not provided explicitly:
Form(Button(target_id='foo', id='btn'),
hx_post='/', target_id='tgt', id='frm')File
def File(
fname
):Use the unescaped text in file fname directly
a = Input(name='nm')
aa(hx_swap_oob='true')ashow
show
def show(
ft, *rest, iframe:bool=False, height:str='auto', style:NoneType=None
):Renders FT Components into HTML within a Jupyter notebook.
When placed within the show() function, this will render the HTML in Jupyter notebooks.
show(sentence)FastHTML is Fast
You can also display full embedded pages in an iframe:
picocss = "https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css"
picolink = (Link(rel="stylesheet", href=picocss))
fullpage = Html(
Head(picolink),
Body(
H2("Heading 2"),
P("Paragraph")
)
)
show(fullpage, height=100, iframe=True)Plain strings also work:
show("<p>Hi <i>everyone</i></p>")Hi everyone
fill_form and find_inputs
fill_form
def fill_form(
form:fastcore.xml.FT, obj
)->fastcore.xml.FT:Fills named items in form using attributes in obj
@dataclass
class TodoItem:
title:str; id:int; done:bool; details:str; opt:str='a'
todo = TodoItem(id=2, title="Profit", done=True, details="Details", opt='b')
check = Label(Input(type="checkbox", cls="checkboxer", name="done", data_foo="bar"), "Done", cls='px-2')
form = Form(Fieldset(Input(cls="char", id="title", value="a"), check, Input(type="hidden", id="id"),
Select(Option(value='a'), Option(value='b'), name='opt'),
Textarea(id='details'), Button("Save"),
name="stuff"))
form = fill_form(form, todo)
assert '<textarea id="details" name="details">Details</textarea>' in to_xml(form)
form@dataclass
class MultiSelect:
items: list[str]
multiselect = MultiSelect(items=['a', 'c'])
multiform = Form(Select(Option('a', value='a'), Option('b', value='b'), Option('c', value='c'), multiple='1', name='items'))
multiform = fill_form(multiform, multiselect)
assert '<option value="a" selected="1">a</option>' in to_xml(multiform)
assert '<option value="b">b</option>' in to_xml(multiform)
assert '<option value="c" selected="1">c</option>' in to_xml(multiform)
multiform@dataclass
class MultiCheck:
items: list[str]
multicheck = MultiCheck(items=['a', 'c'])
multiform = Form(Fieldset(Label(Input(type='checkbox', name='items', value='a'), 'a'),
Label(Input(type='checkbox', name='items', value='b'), 'b'),
Label(Input(type='checkbox', name='items', value='c'), 'c')))
multiform = fill_form(multiform, multicheck)
assert '<input type="checkbox" name="items" value="a" checked="1">' in to_xml(multiform)
assert '<input type="checkbox" name="items" value="b">' in to_xml(multiform)
assert '<input type="checkbox" name="items" value="c" checked="1">' in to_xml(multiform)
multiformfill_dataclass
def fill_dataclass(
src, dest
):Modifies dataclass in-place and returns it
nt = TodoItem('', 0, False, '')
fill_dataclass(todo, nt)
ntTodoItem(title='Profit', id=2, done=True, details='Details', opt='b')
find_inputs
def find_inputs(
e, tags:str='input', **kw
):Recursively find all elements in e with tags and attrs matching kw
inps = find_inputs(form, id='title')
test_eq(len(inps), 1)
inps[<input value="Profit" id="title" class="char" name="title">]
You can also use lxml for more sophisticated searching:
elem = lx.fromstring(to_xml(form))
test_eq(elem.xpath("//input[@id='title']/@value"), ['Profit'])Undefined exports like MyTag create a matching kebab-case tag (ft_hx. The logic lives in fastcore.xml.mk_getattr; here we just supply ft_hx as the tag factory so custom tags get htmx attribute handling.
from fasthtml.components import BarBaz
test_eq(to_xml(BarBaz(hx_get='/foo')), '<bar-baz hx-get="/foo"></bar-baz>')html2ft
html2ft
def html2ft(
html, attr1st:bool=False
):Convert HTML to an ft expression
h = to_xml(form)
hl_md(html2ft(h), 'python')Form(
Fieldset(
Input(value='Profit', id='title', name='title', cls='char'),
Label(
Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer'),
'Done',
cls='px-2'
),
Input(type='hidden', id='id', name='id', value='2'),
Select(
Option(value='a'),
Option(value='b', selected='1'),
name='opt'
),
Textarea('Details', id='details', name='details'),
Button('Save'),
name='stuff'
)
)hl_md(html2ft(h, attr1st=True), 'python')Form(
Fieldset(name='stuff')(
Input(value='Profit', id='title', name='title', cls='char')(),
Label(cls='px-2')(
Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer')(),
'Done'
),
Input(type='hidden', id='id', name='id', value='2')(),
Select(name='opt')(
Option(value='a')(),
Option(value='b', selected='1')()
),
Textarea(id='details', name='details')('Details'),
Button()('Save')
)
)sse_message
def sse_message(
elm, event:str='message'
):Convert element elm into a format suitable for SSE streaming
print(sse_message(Div(P('hi'), P('there'))))event: message
data: <div>
data: <p>hi</p>
data: <p>there</p>
data: </div>
Tests
from bs4 import BeautifulSoup, Commenttest_html2ft('<input value="Profit" name="title" id="title" class="char">', attr1st=True)
test_html2ft('<input value="Profit" name="title" id="title" class="char">')
test_html2ft('<div id="foo"></div>')
test_html2ft('<div id="foo">hi</div>')
test_html2ft('<div>Howdy <a href="https://answer.ai">answer</a> how are you?</div>')
test_html2ft('<div x-show="open" x-transition:enter="transition duration-300" x-transition:enter-start="opacity-0 scale-90">Hello ๐</div>')
test_html2ft('<div x-transition:enter.scale.80 x-transition:leave.scale.90>hello</div>')assert html2ft('<div id="foo">hi</div>', attr1st=True) == "Div(id='foo')('hi')"
assert html2ft('<div id="foo" hidden>hi</div>', attr1st=True) == "Div(id='foo', hidden=True)('hi')"
assert html2ft("""
<div x-show="open" x-transition:enter="transition duration-300" x-transition:enter-start="opacity-0 scale-90">Hello ๐</div>
""") == "Div('Hello ๐', x_show='open', **{'x-transition:enter': 'transition duration-300', 'x-transition:enter-start': 'opacity-0 scale-90'})"
assert html2ft('<div x-transition:enter.scale.80 x-transition:leave.scale.90>hello</div>') == "Div('hello', **{'x-transition:enter.scale.80': True, 'x-transition:leave.scale.90': True})"
assert html2ft("<img alt=' ' />") == "Img(alt=' ')"