Components

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
from collections import UserDict
from lxml import html as lx
from pprint import pprint

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')
sentence

FastHTML 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
):
ft_html('a', **{'@click.away':1})
ft_html('a', {'@click.away':1})
ft_html('a', UserDict({'@click.away':1}))
c = Div(id='someid')
ft_html('a', id=c)

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
):
ft_hx('a', hx_vals={'a':1})
ft_hx('a', hx_target=c)

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')
a
a(hx_swap_oob='true')
a

show


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

fill_dataclass

def fill_dataclass(
    src, dest
):

Modifies dataclass in-place and returns it

nt = TodoItem('', 0, False, '')
fill_dataclass(todo, nt)
nt
TodoItem(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 () via 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, Comment
test_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=' ')"