-
Notifications
You must be signed in to change notification settings - Fork 263
/
Copy pathadv_app2.py
179 lines (152 loc) · 8.16 KB
/
adv_app2.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
from fasthtml.common import *
from fasthtml.jupyter import *
from functools import partial
from dataclasses import dataclass
from hmac import compare_digest
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)
@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 = ('✅ ' 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}')
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=[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(
# 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')
# 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
@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('/')
@rt
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 = 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
@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'))
@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()