3
3
from functools import partial
4
4
from dataclasses import dataclass
5
5
from hmac import compare_digest
6
- from monsterui .all import *
7
-
8
- db = database (':memory:' )
9
6
10
7
8
+ db = database (':memory:' )
11
9
12
10
class User : name :str ; pwd :str
13
11
@@ -26,12 +24,17 @@ def __ft__(self:Todo):
26
24
# which HTMX uses to trigger a GET request.
27
25
# Generally, most of your route handlers in practice (as in this demo app) are likely to be HTMX handlers.
28
26
# For instance, for this demo, we only have two full-page handlers: the '/login' and '/' GET handlers.
29
- show = AX (self .title , f'/todos/{ self .id } ' , 'current-todo' )
30
- edit = AX ('edit' , f'/edit/{ self .id } ' , 'current-todo' )
31
- dt = '✅ ' if self .done else ''
27
+ ### show = AX(self.title, f'/todos/{self.id}', 'current-todo')
28
+ ### edit = AX('edit', f'/edit/{self.id}' , 'current-todo')
29
+ ### dt = '✅ ' if self.done else ''
32
30
# FastHTML provides some shortcuts. For instance, `Hidden` is defined as simply:
33
31
# `return Input(type="hidden", value=value, **kwargs)`
34
- cts = (dt , show , ' | ' , edit , Hidden (id = "id" , value = self .id ), Hidden (id = "priority" , value = "0" ))
32
+ cts = ('✅ ' if self .done else '' ,
33
+ AX (self .title , todo_detail .to (id = self .id ), 'current-todo' ),
34
+ ' | ' ,
35
+ AX ('edit' , todo_edit .to (id = self .id ) , 'current-todo' ),
36
+ Hidden (id = "id" , value = self .id ),
37
+ Hidden (id = "priority" , value = "0" ))
35
38
# Any FT object can take a list of children as positional args, and a dict of attrs as keyword args.
36
39
return Li (* cts , id = f'todo-{ self .id } ' )
37
40
@@ -44,19 +47,30 @@ def user_auth_before(req, sess):
44
47
user_auth_before ,
45
48
skip = [r'/favicon\.ico' , r'/static/.*' , r'.*\.css' , r'.*\.js' , '/login' ]
46
49
)
47
- app , rt = fast_app (hdrs = Theme . blue . headers () + [SortableJS ('.sortable' ),],before = beforeware )
50
+ app , rt = fast_app (hdrs = [SortableJS ('.sortable' ),],before = beforeware )
48
51
49
52
# Authentication
50
53
login_redir = Redirect ('/login' )
51
54
52
55
@rt ('/login' )
53
56
def get ():
57
+ # This creates a form with two input fields, and a submit button.
58
+ # All of these components are `FT` objects. All HTML tags are provided in this form by FastHTML.
59
+ # If you want other custom tags (e.g. `MyTag`), they can be auto-generated by e.g
60
+ # `from fasthtml.components import MyTag`.
61
+ # Alternatively, manually call e.g `ft(tag_name, *children, **attrs)`.
54
62
frm = Form (
55
- LabelInput ("Name" , name = 'name' ),
56
- LabelInput ("Password" , name = 'pwd' , type = 'password' ),
63
+ # Tags with a `name` attr will have `name` auto-set to the same as `id` if not provided
64
+ Input (id = 'name' , placeholder = 'Name' ),
65
+ Input (id = 'pwd' , type = 'password' , placeholder = 'Password' ),
57
66
Button ('login' ),
58
67
action = '/login' , method = 'post' )
59
- return Titled ("Login" , frm , cls = ContainerT .sm )
68
+ # If a user visits the URL directly, FastHTML auto-generates a full HTML page.
69
+ # However, if the URL is accessed by HTMX, then one HTML partial is created for each element of the tuple.
70
+ # To avoid this auto-generation of a full page, return a `HTML` object, or a Starlette `Response`.
71
+ # `Titled` returns a tuple of a `Title` with the first arg and a `Container` with the rest.
72
+ # See the comments for `Title` later for details.
73
+ return Titled ("Login" , frm )
60
74
61
75
@dataclass
62
76
class Login : name :str ; pwd :str
@@ -90,7 +104,7 @@ def index(auth):
90
104
91
105
@rt
92
106
def add_todo (todo :Todo , auth ):
93
- new_inp = LabelInput ( 'Title' , id = "new-title" , name = "title" , placeholder = "New Todo" , hx_swap_oob = 'true' )
107
+ new_inp = Input ( id = "new-title" , name = "title" , placeholder = "New Todo" , hx_swap_oob = 'true' )
94
108
# `insert` returns the inserted todo, which is appended to the start of the list, because we used
95
109
# `hx_swap='afterbegin'` when creating the todo list form.
96
110
return db .todos .insert (todo ), new_inp
@@ -105,5 +119,53 @@ def reorder(id:list[int]):
105
119
# and the server.
106
120
return tuple (db .todos (order_by = 'priority' ))
107
121
122
+ @rt
123
+ def todo_detail (id :int ):
124
+ todo = db .todos [id ]
125
+ # `hx_swap` determines how the update should occur. We use "outerHTML" to replace the entire todo `Li` element.
126
+ btn = Button ('delete' , hx_delete = todos_delete .to (id = id ),
127
+ target_id = f'todo-{ todo .id } ' , hx_swap = "outerHTML" )
128
+ # The "markdown" class is used here because that's the CSS selector we used in the JS earlier.
129
+ # Therefore this will trigger the JS to parse the markdown in the details field.
130
+ # Because `class` is a reserved keyword in Python, we use `cls` instead, which FastHTML auto-converts.
131
+ return Div (H2 (todo .title ), Div (todo .details , cls = "markdown" ), btn )
132
+
133
+ @rt
134
+ def todo_edit (id :int ):
135
+ # The `hx_put` attribute tells HTMX to send a PUT request when the form is submitted.
136
+ # `target_id` specifies which element will be updated with the server's response.
137
+ res = Form (Group (Input (id = "title" ), Button ("Save" )),
138
+ Hidden (id = "id" ), CheckboxX (id = "done" , label = 'Done' ),
139
+ Textarea (id = "details" , name = "details" , rows = 10 ),
140
+ hx_put = "/" , target_id = f'todo-{ id } ' , id = "edit" )
141
+ # `fill_form` populates the form with existing todo data, and returns the result.
142
+ # Indexing into a table (`todos`) queries by primary key, which is `id` here. It also includes
143
+ # `xtra`, so this will only return the id if it belongs to the current user.
144
+ return fill_form (res , db .todos [id ])
145
+
146
+ # Refactoring components in FastHTML is as simple as creating Python functions.
147
+ # The `clr_details` function creates a Div with specific HTMX attributes.
148
+ # `hx_swap_oob='innerHTML'` tells HTMX to swap the inner HTML of the target element out-of-band,
149
+ # meaning it will update this element regardless of where the HTMX request originated from.
150
+ def clr_details (): return Div (hx_swap_oob = 'innerHTML' , id = 'current-todo' )
151
+
152
+ @rt ("/" )
153
+ def put (todo : Todo ):
154
+ # `update` is part of the MiniDataAPI spec.
155
+ # Note that the updated todo is returned. By returning the updated todo, we can update the list directly.
156
+ # Because we return a tuple with `clr_details()`, the details view is also cleared.
157
+ return db .todos .update (todo ), clr_details ()
158
+
159
+
160
+ # This route handler uses a path parameter `{id}` which is automatically parsed and passed as an int.
161
+ @rt (methods = ['DELETE' ])
162
+ def todos_delete (id :int ):
163
+ # The `delete` method is part of the MiniDataAPI spec, removing the item with the given primary key.
164
+ db .todos .delete (id )
165
+ # Returning `clr_details()` ensures the details view is cleared after deletion,
166
+ # leveraging HTMX's out-of-band swap feature.
167
+ # Note that we are not returning *any* FT component that doesn't have an "OOB" swap, so the target element
168
+ # inner HTML is simply deleted. That's why the deleted todo is removed from the list.
169
+ return clr_details ()
108
170
109
171
serve ()
0 commit comments