From c94fcf900f454173943fdd6bad94a610a62cf14f Mon Sep 17 00:00:00 2001 From: "Audrey M. Roy Greenfeld" Date: Thu, 27 Mar 2025 12:04:54 +1000 Subject: [PATCH 1/7] Start of updated adv_app.py Co-Authored-By: Isaac Flath --- nbs/explains/IdiomaticApp.ipynb | 165 ++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 nbs/explains/IdiomaticApp.ipynb diff --git a/nbs/explains/IdiomaticApp.ipynb b/nbs/explains/IdiomaticApp.ipynb new file mode 100644 index 00000000..def3c789 --- /dev/null +++ b/nbs/explains/IdiomaticApp.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from fasthtml.common import *\n", + "from fasthtml.jupyter import *\n", + "from functools import partial\n", + "from hmac import compare_digest\n", + "from monsterui.all import *" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db = database('data/utodos.db')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class User:\n", + " name:str; pwd:str" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Todo:\n", + " \"A todo list item\"\n", + " id:int; title:str; done:bool; name:str; details:str; priority:int" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db.users = db.create(User, transform=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db.todos = db.create(Todo, transform=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "app, rt = fast_app(hdrs=Theme.blue.headers())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "login_redir = Redirect('/login')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def before(req, sess):\n", + " auth = req.scope['auth'] = sess.get('auth', None)\n", + " if not auth: return login_redir\n", + " todos.xtra(name=auth)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@rt\n", + "def login():\n", + " frm = Form(\n", + " LabelInput(\"Name\",id='n'),\n", + " LabelInput(\"Password\",id='pwd'),\n", + " Button('login'),\n", + " action='/login', method='post')\n", + " return Titled(\"Login\", frm, cls=ContainerT.sm)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Show = partial(HTMX, app=app, link=true)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "server = JupyUvi(app)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From ef181b49ac649e033ea21294efa1da58793123fd Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Thu, 27 Mar 2025 12:49:28 +1000 Subject: [PATCH 2/7] Migrate to flatfile and get login,logout and basic dashboard functional Co-authored-by: Audrey Roy Greenfeld --- examples/adv_app2.py | 74 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 examples/adv_app2.py diff --git a/examples/adv_app2.py b/examples/adv_app2.py new file mode 100644 index 00000000..d7190565 --- /dev/null +++ b/examples/adv_app2.py @@ -0,0 +1,74 @@ +from fasthtml.common import * +from fasthtml.jupyter import * +from functools import partial +from dataclasses import dataclass +from hmac import compare_digest +from monsterui.all import * + +db = database(':memory:') + +class User: name:str; pwd:str + +class Todo: + id:int; title:str; done:bool; name:str; details:str; priority:int + +db.users = db.create(User, transform=True, pk='name') +db.todos = db.create(Todo, transform=True) + + +def user_auth_before(req, sess): + auth = req.scope['auth'] = sess.get('auth', None) + if not auth: return login_redir + db.todos.xtra(name=auth) + +beforeware = Beforeware( + user_auth_before, + skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login'] +) + + +app, rt = fast_app(hdrs=Theme.blue.headers(),before=beforeware) + +login_redir = Redirect('/login') + + +@rt +def index(auth): + top = Grid(Div(A('logout', href=logout), style='text-align: right')) + new_inp = Input(id="new-title", name="title", placeholder="New Todo") + add = Form(Group(new_inp, Button("Add")), + hx_post="/", target_id='todo-list', hx_swap="afterbegin") + frm = Form(*db.todos(order_by='priority'), + id='todo-list', cls='sortable', hx_post="/reorder", hx_trigger="end") + + card = Card(Ul(frm), header=add, footer=Div(id='current-todo')) + return Titled(f"{auth}'s Todo list", Container(top, card)) + + +@rt('/login') +def get(): + frm = Form( + LabelInput("Name", name='name'), + LabelInput("Password", name='pwd'), + Button('login'), + action='/login', method='post') + return Titled("Login", frm, cls=ContainerT.sm) + +@dataclass +class Login: name:str; pwd:str + +@rt("/login") +def post(login:Login, sess): + if not login.name or not login.pwd: return login_redir + try: u = db.users[login.name] + except NotFoundError: u = db.users.insert(login) + if not compare_digest(u.pwd.encode("utf-8"), login.pwd.encode("utf-8")): return login_redir + sess['auth'] = u.name + return Redirect('/') + +@app.get("/logout") +def logout(sess): + del sess['auth'] + return login_redir + +serve() \ No newline at end of file From bd5885f851ca44ac18cb8739ccd7dc1d6ed699ef Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Thu, 27 Mar 2025 12:58:20 +1000 Subject: [PATCH 3/7] New handler for adding todo Co-authored-by: Audrey Roy Greenfeld --- examples/adv_app2.py | 55 +++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/examples/adv_app2.py b/examples/adv_app2.py index d7190565..7b6ebf1f 100644 --- a/examples/adv_app2.py +++ b/examples/adv_app2.py @@ -15,6 +15,23 @@ class Todo: db.users = db.create(User, transform=True, pk='name') db.todos = db.create(Todo, transform=True) +@patch +def __ft__(self:Todo): + # Some FastHTML tags have an 'X' suffix, which means they're "extended" in some way. + # For instance, here `AX` is an extended `A` tag, which takes 3 positional arguments: + # `(text, hx_get, target_id)`. + # All underscores in FT attrs are replaced with hyphens, so this will create an `hx-get` attr, + # which HTMX uses to trigger a GET request. + # Generally, most of your route handlers in practice (as in this demo app) are likely to be HTMX handlers. + # For instance, for this demo, we only have two full-page handlers: the '/login' and '/' GET handlers. + show = AX(self.title, f'/todos/{self.id}', 'current-todo') + edit = AX('edit', f'/edit/{self.id}' , 'current-todo') + dt = '✅ ' if self.done else '' + # FastHTML provides some shortcuts. For instance, `Hidden` is defined as simply: + # `return Input(type="hidden", value=value, **kwargs)` + cts = (dt, show, ' | ', edit, Hidden(id="id", value=self.id), Hidden(id="priority", value="0")) + # Any FT object can take a list of children as positional args, and a dict of attrs as keyword args. + return Li(*cts, id=f'todo-{self.id}') def user_auth_before(req, sess): auth = req.scope['auth'] = sess.get('auth', None) @@ -25,26 +42,11 @@ def user_auth_before(req, sess): user_auth_before, skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login'] ) - - app, rt = fast_app(hdrs=Theme.blue.headers(),before=beforeware) +# Authentication login_redir = Redirect('/login') - -@rt -def index(auth): - top = Grid(Div(A('logout', href=logout), style='text-align: right')) - new_inp = Input(id="new-title", name="title", placeholder="New Todo") - add = Form(Group(new_inp, Button("Add")), - hx_post="/", target_id='todo-list', hx_swap="afterbegin") - frm = Form(*db.todos(order_by='priority'), - id='todo-list', cls='sortable', hx_post="/reorder", hx_trigger="end") - - card = Card(Ul(frm), header=add, footer=Div(id='current-todo')) - return Titled(f"{auth}'s Todo list", Container(top, card)) - - @rt('/login') def get(): frm = Form( @@ -71,4 +73,25 @@ def logout(sess): del sess['auth'] return login_redir +# Dashboard and control handlers +@rt +def index(auth): + top = Grid(Div(A('logout', href=logout), style='text-align: right')) + new_inp = Input(id="new-title", name="title", placeholder="New Todo") + add = Form(Group(new_inp, Button("Add")), + hx_post=add_todo, target_id='todo-list', hx_swap="afterbegin") + frm = Form(*db.todos(order_by='priority'), + id='todo-list', cls='sortable', hx_post="/reorder", hx_trigger="end") + + card = Card(Ul(frm), header=add, footer=Div(id='current-todo')) + return Titled(f"{auth}'s Todo list", Container(top, card)) + +@rt +def add_todo(todo:Todo, auth): + new_inp = LabelInput('Title', id="new-title", name="title", placeholder="New Todo", hx_swap_oob='true') + # `insert` returns the inserted todo, which is appended to the start of the list, because we used + # `hx_swap='afterbegin'` when creating the todo list form. + return db.todos.insert(todo), new_inp + + serve() \ No newline at end of file From 21269f87656d8606a45b89c15769fd0d8f19e083 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Thu, 27 Mar 2025 13:08:19 +1000 Subject: [PATCH 4/7] Remove idiomatic notebook for now --- nbs/explains/IdiomaticApp.ipynb | 165 -------------------------------- 1 file changed, 165 deletions(-) delete mode 100644 nbs/explains/IdiomaticApp.ipynb diff --git a/nbs/explains/IdiomaticApp.ipynb b/nbs/explains/IdiomaticApp.ipynb deleted file mode 100644 index def3c789..00000000 --- a/nbs/explains/IdiomaticApp.ipynb +++ /dev/null @@ -1,165 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from fasthtml.common import *\n", - "from fasthtml.jupyter import *\n", - "from functools import partial\n", - "from hmac import compare_digest\n", - "from monsterui.all import *" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "db = database('data/utodos.db')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class User:\n", - " name:str; pwd:str" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class Todo:\n", - " \"A todo list item\"\n", - " id:int; title:str; done:bool; name:str; details:str; priority:int" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "db.users = db.create(User, transform=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "db.todos = db.create(Todo, transform=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "app, rt = fast_app(hdrs=Theme.blue.headers())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "login_redir = Redirect('/login')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def before(req, sess):\n", - " auth = req.scope['auth'] = sess.get('auth', None)\n", - " if not auth: return login_redir\n", - " todos.xtra(name=auth)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@rt\n", - "def login():\n", - " frm = Form(\n", - " LabelInput(\"Name\",id='n'),\n", - " LabelInput(\"Password\",id='pwd'),\n", - " Button('login'),\n", - " action='/login', method='post')\n", - " return Titled(\"Login\", frm, cls=ContainerT.sm)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Show = partial(HTMX, app=app, link=true)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "server = JupyUvi(app)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From 67b96650de16c92d6c42e1f4481b71459ded4fd1 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Fri, 28 Mar 2025 06:48:16 +1000 Subject: [PATCH 5/7] Add reorder --- examples/adv_app2.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/examples/adv_app2.py b/examples/adv_app2.py index 7b6ebf1f..4c7c081d 100644 --- a/examples/adv_app2.py +++ b/examples/adv_app2.py @@ -7,6 +7,8 @@ db = database(':memory:') + + class User: name:str; pwd:str class Todo: @@ -42,7 +44,7 @@ def user_auth_before(req, sess): user_auth_before, skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login'] ) -app, rt = fast_app(hdrs=Theme.blue.headers(),before=beforeware) +app, rt = fast_app(hdrs=Theme.blue.headers()+[SortableJS('.sortable'),],before=beforeware) # Authentication login_redir = Redirect('/login') @@ -68,7 +70,7 @@ def post(login:Login, sess): sess['auth'] = u.name return Redirect('/') -@app.get("/logout") +@rt def logout(sess): del sess['auth'] return login_redir @@ -81,7 +83,7 @@ def index(auth): add = Form(Group(new_inp, Button("Add")), hx_post=add_todo, target_id='todo-list', hx_swap="afterbegin") frm = Form(*db.todos(order_by='priority'), - id='todo-list', cls='sortable', hx_post="/reorder", hx_trigger="end") + id='todo-list', cls='sortable', hx_post=reorder, hx_trigger="end") card = Card(Ul(frm), header=add, footer=Div(id='current-todo')) return Titled(f"{auth}'s Todo list", Container(top, card)) @@ -91,7 +93,17 @@ def add_todo(todo:Todo, auth): new_inp = LabelInput('Title', id="new-title", name="title", placeholder="New Todo", hx_swap_oob='true') # `insert` returns the inserted todo, which is appended to the start of the list, because we used # `hx_swap='afterbegin'` when creating the todo list form. - return db.todos.insert(todo), new_inp + return db.todos.insert(todo), new_inp + +@rt +def reorder(id:list[int]): + for i,id_ in enumerate(id): db.todos.update({'priority':i}, id_) + # HTMX by default replaces the inner HTML of the calling element, which in this case is the todo list form. + # Therefore, we return the list of todos, now in the correct order, which will be auto-converted to FT for us. + # In this case, it's not strictly necessary, because sortable.js has already reorder the DOM elements. + # However, by returning the updated data, we can be assured that there aren't sync issues between the DOM + # and the server. + return tuple(db.todos(order_by='priority')) serve() \ No newline at end of file From f0815957c011c5f85d8238263c12e6f0054374a4 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Fri, 28 Mar 2025 06:56:38 +1000 Subject: [PATCH 6/7] db and reorder --- examples/adv_app2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/adv_app2.py b/examples/adv_app2.py index 4c7c081d..4a6a08dd 100644 --- a/examples/adv_app2.py +++ b/examples/adv_app2.py @@ -53,7 +53,7 @@ def user_auth_before(req, sess): def get(): frm = Form( LabelInput("Name", name='name'), - LabelInput("Password", name='pwd'), + LabelInput("Password", name='pwd', type='password'), Button('login'), action='/login', method='post') return Titled("Login", frm, cls=ContainerT.sm) From 8740bb86cc5d6505940bf798859301cb4ce26358 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Fri, 18 Apr 2025 17:42:45 +0800 Subject: [PATCH 7/7] Finish working functionality --- examples/adv_app2.py | 94 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 82 insertions(+), 12 deletions(-) diff --git a/examples/adv_app2.py b/examples/adv_app2.py index 4a6a08dd..0c1042fd 100644 --- a/examples/adv_app2.py +++ b/examples/adv_app2.py @@ -3,11 +3,9 @@ from functools import partial from dataclasses import dataclass from hmac import compare_digest -from monsterui.all import * - -db = database(':memory:') +db = database(':memory:') class User: name:str; pwd:str @@ -26,12 +24,17 @@ def __ft__(self:Todo): # which HTMX uses to trigger a GET request. # Generally, most of your route handlers in practice (as in this demo app) are likely to be HTMX handlers. # For instance, for this demo, we only have two full-page handlers: the '/login' and '/' GET handlers. - show = AX(self.title, f'/todos/{self.id}', 'current-todo') - edit = AX('edit', f'/edit/{self.id}' , 'current-todo') - dt = '✅ ' if self.done else '' + ### show = AX(self.title, f'/todos/{self.id}', 'current-todo') + ### edit = AX('edit', f'/edit/{self.id}' , 'current-todo') + ### dt = '✅ ' if self.done else '' # FastHTML provides some shortcuts. For instance, `Hidden` is defined as simply: # `return Input(type="hidden", value=value, **kwargs)` - cts = (dt, show, ' | ', edit, Hidden(id="id", value=self.id), Hidden(id="priority", value="0")) + cts = ('✅ ' if self.done else '', + AX(self.title, todo_detail.to(id=self.id), 'current-todo'), + ' | ', + AX('edit', todo_edit.to(id=self.id) , 'current-todo'), + Hidden(id="id", value=self.id), + Hidden(id="priority", value="0")) # Any FT object can take a list of children as positional args, and a dict of attrs as keyword args. return Li(*cts, id=f'todo-{self.id}') @@ -44,19 +47,30 @@ def user_auth_before(req, sess): user_auth_before, skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', r'.*\.js', '/login'] ) -app, rt = fast_app(hdrs=Theme.blue.headers()+[SortableJS('.sortable'),],before=beforeware) +app, rt = fast_app(hdrs=[SortableJS('.sortable'),],before=beforeware) # Authentication login_redir = Redirect('/login') @rt('/login') def get(): + # This creates a form with two input fields, and a submit button. + # All of these components are `FT` objects. All HTML tags are provided in this form by FastHTML. + # If you want other custom tags (e.g. `MyTag`), they can be auto-generated by e.g + # `from fasthtml.components import MyTag`. + # Alternatively, manually call e.g `ft(tag_name, *children, **attrs)`. frm = Form( - LabelInput("Name", name='name'), - LabelInput("Password", name='pwd', type='password'), + # Tags with a `name` attr will have `name` auto-set to the same as `id` if not provided + Input(id='name', placeholder='Name'), + Input(id='pwd', type='password', placeholder='Password'), Button('login'), action='/login', method='post') - return Titled("Login", frm, cls=ContainerT.sm) + # If a user visits the URL directly, FastHTML auto-generates a full HTML page. + # However, if the URL is accessed by HTMX, then one HTML partial is created for each element of the tuple. + # To avoid this auto-generation of a full page, return a `HTML` object, or a Starlette `Response`. + # `Titled` returns a tuple of a `Title` with the first arg and a `Container` with the rest. + # See the comments for `Title` later for details. + return Titled("Login", frm) @dataclass class Login: name:str; pwd:str @@ -90,7 +104,7 @@ def index(auth): @rt def add_todo(todo:Todo, auth): - new_inp = LabelInput('Title', id="new-title", name="title", placeholder="New Todo", hx_swap_oob='true') + new_inp = Input(id="new-title", name="title", placeholder="New Todo", hx_swap_oob='true') # `insert` returns the inserted todo, which is appended to the start of the list, because we used # `hx_swap='afterbegin'` when creating the todo list form. return db.todos.insert(todo), new_inp @@ -105,5 +119,61 @@ def reorder(id:list[int]): # and the server. return tuple(db.todos(order_by='priority')) +@rt +def todo_detail(id:int): + todo = db.todos[id] + # `hx_swap` determines how the update should occur. We use "outerHTML" to replace the entire todo `Li` element. + btn = Button('delete', hx_delete=todos_delete.to(id=id), + target_id=f'todo-{todo.id}', hx_swap="outerHTML") + # The "markdown" class is used here because that's the CSS selector we used in the JS earlier. + # Therefore this will trigger the JS to parse the markdown in the details field. + # Because `class` is a reserved keyword in Python, we use `cls` instead, which FastHTML auto-converts. + return Div(H2(todo.title), Div(todo.details, cls="markdown"), btn) + +@rt +def todo_edit(id:int): + # The `hx_put` attribute tells HTMX to send a PUT request when the form is submitted. + # `target_id` specifies which element will be updated with the server's response. + res = Form(Group(Input(id="title"), Button("Save")), + Hidden(id="id"), CheckboxX(id="done", label='Done'), + Textarea(id="details", name="details", rows=10), + hx_put="/", target_id=f'todo-{id}', id="edit") + # `fill_form` populates the form with existing todo data, and returns the result. + # Indexing into a table (`todos`) queries by primary key, which is `id` here. It also includes + # `xtra`, so this will only return the id if it belongs to the current user. + return fill_form(res, db.todos[id]) + +# Refactoring components in FastHTML is as simple as creating Python functions. +# The `clr_details` function creates a Div with specific HTMX attributes. +# `hx_swap_oob='innerHTML'` tells HTMX to swap the inner HTML of the target element out-of-band, +# meaning it will update this element regardless of where the HTMX request originated from. +def clr_details(): return Div(hx_swap_oob='innerHTML', id='current-todo') + +@rt("/") +def put(todo: Todo): + # `update` is part of the MiniDataAPI spec. + # Note that the updated todo is returned. By returning the updated todo, we can update the list directly. + # Because we return a tuple with `clr_details()`, the details view is also cleared. + return db.todos.update(todo), clr_details() + + +# This route handler uses a path parameter `{id}` which is automatically parsed and passed as an int. +@rt(methods=['DELETE']) +def todos_delete(id:int): + # The `delete` method is part of the MiniDataAPI spec, removing the item with the given primary key. + db.todos.delete(id) + # Returning `clr_details()` ensures the details view is cleared after deletion, + # leveraging HTMX's out-of-band swap feature. + # Note that we are not returning *any* FT component that doesn't have an "OOB" swap, so the target element + # inner HTML is simply deleted. That's why the deleted todo is removed from the list. + return clr_details() + + +Where HTTP methods in allcaps on a handler name constrains that handler to only respond to that method. + +@rt_delete +def todos(id:int): # Constrained to just HTTP DELETE + db.todos.delete(id) + return clr_details() serve() \ No newline at end of file