phihung commited on
Commit
1108474
·
1 Parent(s): 2531445

refactor: Use rt instead of hardcoded uri

Browse files
src/tutorial/_01_click_to_edit.py CHANGED
@@ -8,33 +8,38 @@ Contact = namedtuple("Contact", ["name", "email"])
8
  current = Contact("Joe", "[email protected]")
9
 
10
 
11
- @app.get("/contact/1")
 
 
 
 
 
12
  def get_contact():
13
  return Div(
14
  Div(P(f"Name : {current.name}")),
15
  Div(P(f"Email : {current.email}")),
16
- Button("Click To Edit", hx_get="/contact/1/edit", cls="btn primary"),
17
  hx_target="this",
18
  hx_swap="outerHTML",
19
  cls="container",
20
  )
21
 
22
 
23
- @app.get("/contact/1/edit")
24
  def contact_edit():
25
  return Form(
26
  Div(Label("Name"), Input(type="text", name="name", value=current.name)),
27
  Div(Label("Email"), Input(type="email", name="email", value=current.email)),
28
  Button("Submit", cls="btn"),
29
- Button("Cancel", hx_get="/contact/1", cls="btn"),
30
- hx_put="/contact/1",
31
  hx_target="this",
32
  hx_swap="outerHTML",
33
  cls="container",
34
  )
35
 
36
 
37
- @app.put("/contact/1")
38
  def put_contact(c: Contact):
39
  global current
40
  current = c
 
8
  current = Contact("Joe", "[email protected]")
9
 
10
 
11
+ @app.get
12
+ def page():
13
+ return get_contact()
14
+
15
+
16
+ @app.get("/contact")
17
  def get_contact():
18
  return Div(
19
  Div(P(f"Name : {current.name}")),
20
  Div(P(f"Email : {current.email}")),
21
+ Button("Click To Edit", hx_get=contact_edit.rt(), cls="btn primary"),
22
  hx_target="this",
23
  hx_swap="outerHTML",
24
  cls="container",
25
  )
26
 
27
 
28
+ @app.get("/contact/edit")
29
  def contact_edit():
30
  return Form(
31
  Div(Label("Name"), Input(type="text", name="name", value=current.name)),
32
  Div(Label("Email"), Input(type="email", name="email", value=current.email)),
33
  Button("Submit", cls="btn"),
34
+ Button("Cancel", hx_get=get_contact.rt(), cls="btn"),
35
+ hx_put=put_contact.rt(),
36
  hx_target="this",
37
  hx_swap="outerHTML",
38
  cls="container",
39
  )
40
 
41
 
42
+ @app.put("/contact")
43
  def put_contact(c: Contact):
44
  global current
45
  current = c
src/tutorial/_02_bulk_update.py CHANGED
@@ -16,9 +16,10 @@ css = """\
16
  app, rt = fast_app(hdrs=[Style(css)])
17
 
18
 
19
- @app.get("/users")
20
- def list_users():
21
- return Form(
 
22
  Table(
23
  Thead(Tr(Th("Name"), Th("Email"), Th("Active"))),
24
  Tbody(
@@ -30,17 +31,11 @@ def list_users():
30
  ),
31
  Button("Bulk Update", cls="btn primary"),
32
  Div(id="toast"),
33
- # Bug with default enctype: multipart/form-data
34
- enctype="",
35
- hx_post="/users",
36
- hx_swap="outerHTML settle:3s",
37
- hx_target="#toast",
38
- cls="container",
39
  )
40
 
41
 
42
  @app.post("/users")
43
- def bulk_update(x: dict):
44
  n = len(x)
45
  return Div(f"Activated {n} and deactivated {4-n} users", id="toast", aria_live="polite")
46
 
@@ -54,5 +49,5 @@ The server will bulk-update the statuses based on the values of the checkboxes.
54
  The cool thing is that, because HTML form inputs already manage their own state, we don’t need to re-render any part of the users table. The active users are already checked and the inactive ones unchecked!
55
 
56
  You can see a working example of this code below.
57
- ::list_users bulk_update::
58
  """
 
16
  app, rt = fast_app(hdrs=[Style(css)])
17
 
18
 
19
+ @app.get
20
+ def page():
21
+ # Bug with default enctype: multipart/form-data
22
+ return Form(enctype="", hx_post=update.rt(), hx_swap="outerHTML settle:3s", hx_target="#toast", cls="container")(
23
  Table(
24
  Thead(Tr(Th("Name"), Th("Email"), Th("Active"))),
25
  Tbody(
 
31
  ),
32
  Button("Bulk Update", cls="btn primary"),
33
  Div(id="toast"),
 
 
 
 
 
 
34
  )
35
 
36
 
37
  @app.post("/users")
38
+ def update(x: dict):
39
  n = len(x)
40
  return Div(f"Activated {n} and deactivated {4-n} users", id="toast", aria_live="polite")
41
 
 
49
  The cool thing is that, because HTML form inputs already manage their own state, we don’t need to re-render any part of the users table. The active users are already checked and the inactive ones unchecked!
50
 
51
  You can see a working example of this code below.
52
+ ::page update::
53
  """
src/tutorial/_03_click_to_load.py CHANGED
@@ -4,8 +4,8 @@ app, rt = fast_app()
4
 
5
 
6
  # fmt: off
7
- @app.get("/table")
8
- def contact_table():
9
  return Div(
10
  Table(
11
  Thead(Tr(Th("Name"), Th("ID"))),
@@ -27,7 +27,7 @@ def make_last_row(page):
27
  Td(
28
  Button(
29
  "Load More Agents...",
30
- hx_get=f"/contacts?page={page + 1}",
31
  hx_swap="outerHTML",
32
  cls="btn primary",
33
  ),
 
4
 
5
 
6
  # fmt: off
7
+ @app.get
8
+ def page():
9
  return Div(
10
  Table(
11
  Thead(Tr(Th("Name"), Th("ID"))),
 
27
  Td(
28
  Button(
29
  "Load More Agents...",
30
+ hx_get=load_contacts.rt(page=page + 1),
31
  hx_swap="outerHTML",
32
  cls="btn primary",
33
  ),
src/tutorial/_04_delete_row.py CHANGED
@@ -3,15 +3,15 @@ from fasthtml.common import Button, Div, Style, Table, Tbody, Td, Th, Thead, Tr,
3
  css = """\
4
  tr.htmx-swapping td {
5
  opacity: 0;
6
- transition: opacity 1s ease-out;
7
  }
8
  """
9
 
10
  app, rt = fast_app(hdrs=[Style(css)])
11
 
12
 
13
- @app.get("/table")
14
- def contact_table():
15
  return Div(
16
  Table(
17
  Thead(Tr(Th("Name"), Th("Email"), Th())),
@@ -19,22 +19,22 @@ def contact_table():
19
  Tr(
20
  Td("Joe Smith"),
21
  Td("[email protected]"),
22
- Td(Button("Delete", hx_delete="/contacts/0", cls="btn danger")),
23
  ),
24
  Tr(
25
  Td("Angie MacDowell"),
26
  Td("[email protected]"),
27
- Td(Button("Delete", hx_delete="/contacts/1", cls="btn danger")),
28
  ),
29
  Tr(
30
  Td("Fuqua Tarkenton"),
31
  Td("[email protected]"),
32
- Td(Button("Delete", hx_delete="/contacts/2", cls="btn danger")),
33
  ),
34
  Tr(
35
  Td("Kim Yee"),
36
  Td("[email protected]"),
37
- Td(Button("Delete", hx_delete="/contacts/3", cls="btn danger")),
38
  ),
39
  hx_confirm="Are you sure?",
40
  hx_target="closest tr",
@@ -45,8 +45,8 @@ def contact_table():
45
  )
46
 
47
 
48
- @app.delete("/contacts/{idx}")
49
- def delete_contact(idx: int):
50
  # Delete actual data here
51
  return None
52
 
@@ -74,7 +74,7 @@ Each row has a button with a hx-delete attribute containing the url on which to
74
  Tr(
75
  Td("Angie MacDowell"),
76
  Td("[email protected]"),
77
- Td(Button("Delete", hx_delete="/contacts/1", cls="btn danger")),
78
  )
79
  ```
80
  """
 
3
  css = """\
4
  tr.htmx-swapping td {
5
  opacity: 0;
6
+ transition: opacity 1s ease-out !important;
7
  }
8
  """
9
 
10
  app, rt = fast_app(hdrs=[Style(css)])
11
 
12
 
13
+ @app.get
14
+ def page():
15
  return Div(
16
  Table(
17
  Thead(Tr(Th("Name"), Th("Email"), Th())),
 
19
  Tr(
20
  Td("Joe Smith"),
21
  Td("[email protected]"),
22
+ Td(Button("Delete", hx_delete="/contacts/0", cls="btn secondary")),
23
  ),
24
  Tr(
25
  Td("Angie MacDowell"),
26
  Td("[email protected]"),
27
+ Td(Button("Delete", hx_delete="/contacts/1", cls="btn secondary")),
28
  ),
29
  Tr(
30
  Td("Fuqua Tarkenton"),
31
  Td("[email protected]"),
32
+ Td(Button("Delete", hx_delete="/contacts/2", cls="btn secondary")),
33
  ),
34
  Tr(
35
  Td("Kim Yee"),
36
  Td("[email protected]"),
37
+ Td(Button("Delete", hx_delete="/contacts/3", cls="btn secondary")),
38
  ),
39
  hx_confirm="Are you sure?",
40
  hx_target="closest tr",
 
45
  )
46
 
47
 
48
+ @rt("/contacts/{idx}")
49
+ def delete(idx: int):
50
  # Delete actual data here
51
  return None
52
 
 
74
  Tr(
75
  Td("Angie MacDowell"),
76
  Td("[email protected]"),
77
+ Td(Button("Delete", hx_delete="/contacts/1", cls="btn secondary")),
78
  )
79
  ```
80
  """
src/tutorial/_05_edit_row.py CHANGED
@@ -10,8 +10,8 @@ DATA = [
10
  ]
11
 
12
 
13
- @app.get("/table")
14
- def contact_table():
15
  return Div(
16
  Table(
17
  Thead(Tr(Th("Name"), Th("Email"), Th())),
@@ -25,8 +25,18 @@ def contact_table():
25
  )
26
 
27
 
 
 
 
 
 
 
 
 
 
 
28
  @app.get("/contact/{idx}/edit")
29
- def edit_contact(idx: int):
30
  name, email = DATA[idx]
31
  return Tr(
32
  Td(Input(name="name", value=name)),
@@ -41,16 +51,6 @@ def edit_contact(idx: int):
41
  )
42
 
43
 
44
- @app.get("/contact/{idx}")
45
- def get_contact(idx: int):
46
- name, email = DATA[idx]
47
- return Tr(
48
- Td(name),
49
- Td(email),
50
- Td(Button("Edit", hx_get=f"/contact/{idx}/edit", hx_trigger="edit", onclick=JS)),
51
- )
52
-
53
-
54
  @app.put("/contact/{idx}")
55
  def put_contact(idx: int, x: dict):
56
  DATA[idx] = (x["name"], x["email"])
@@ -88,7 +88,7 @@ htmx.trigger(this, 'edit')
88
  DESC = "Demonstrates how to edit rows in a table"
89
  DOC = """
90
  This example shows how to implement editable rows. First let’s look at the table body:
91
- ::contact_table::
92
  This will tell the requests from within the table to target the closest enclosing row that the request is triggered on and to replace the entire row.
93
 
94
  Here is the HTML for a row:
@@ -102,6 +102,6 @@ We then trigger the edit event on the current element, which triggers the htmx r
102
  Note that if you didn’t care if a user was editing multiple rows, you could omit the hyperscript and custom hx-trigger, and just let the normal click handling work with htmx. You could also implement mutual exclusivity by simply targeting the entire table when the Edit button was clicked. Here we wanted to show how to integrate htmx and JavaScript to solve the problem and narrow down the server interactions a bit, plus we get to use a nice SweetAlert confirm dialog.
103
 
104
  Finally, here is what the row looks like when the data is being edited:
105
- ::edit_contact::
106
  Here we have a few things going on: First off the row itself can respond to the cancel event, which will bring back the read-only version of the row. There is a cancel button that allows cancelling the current edit. Finally, there is a save button that issues a PUT to update the contact. Note that there is an hx-include that includes all the inputs in the closest row. Tables rows are notoriously difficult to use with forms due to HTML constraints (you can’t put a form directly inside a tr) so this makes things a bit nicer to deal with.
107
  """
 
10
  ]
11
 
12
 
13
+ @app.get
14
+ def page():
15
  return Div(
16
  Table(
17
  Thead(Tr(Th("Name"), Th("Email"), Th())),
 
25
  )
26
 
27
 
28
+ @app.get("/contact/{idx}")
29
+ def get_contact(idx: int):
30
+ name, email = DATA[idx]
31
+ return Tr(
32
+ Td(name),
33
+ Td(email),
34
+ Td(Button("Edit", hx_get=f"/contact/{idx}/edit", hx_trigger="edit", onclick=JS)),
35
+ )
36
+
37
+
38
  @app.get("/contact/{idx}/edit")
39
+ def edit_view(idx: int):
40
  name, email = DATA[idx]
41
  return Tr(
42
  Td(Input(name="name", value=name)),
 
51
  )
52
 
53
 
 
 
 
 
 
 
 
 
 
 
54
  @app.put("/contact/{idx}")
55
  def put_contact(idx: int, x: dict):
56
  DATA[idx] = (x["name"], x["email"])
 
88
  DESC = "Demonstrates how to edit rows in a table"
89
  DOC = """
90
  This example shows how to implement editable rows. First let’s look at the table body:
91
+ ::page::
92
  This will tell the requests from within the table to target the closest enclosing row that the request is triggered on and to replace the entire row.
93
 
94
  Here is the HTML for a row:
 
102
  Note that if you didn’t care if a user was editing multiple rows, you could omit the hyperscript and custom hx-trigger, and just let the normal click handling work with htmx. You could also implement mutual exclusivity by simply targeting the entire table when the Edit button was clicked. Here we wanted to show how to integrate htmx and JavaScript to solve the problem and narrow down the server interactions a bit, plus we get to use a nice SweetAlert confirm dialog.
103
 
104
  Finally, here is what the row looks like when the data is being edited:
105
+ ::edit_view::
106
  Here we have a few things going on: First off the row itself can respond to the cancel event, which will bring back the read-only version of the row. There is a cancel button that allows cancelling the current edit. Finally, there is a save button that issues a PUT to update the contact. Note that there is an hx-include that includes all the inputs in the closest row. Tables rows are notoriously difficult to use with forms due to HTML constraints (you can’t put a form directly inside a tr) so this makes things a bit nicer to deal with.
107
  """
src/tutorial/_06_lazy_loading.py CHANGED
@@ -14,18 +14,18 @@ img {
14
  app, rt = fast_app(hdrs=[Style(css)])
15
 
16
 
17
- @app.get("/page")
18
- def main_page():
19
  return Div(
20
  Img(src="/img/bars.svg", alt="Result loading...", cls="htmx-indicator", width="150"),
21
- hx_get="/content",
22
  hx_trigger="load",
23
  cls="container",
24
  )
25
 
26
 
27
- @app.get("/content")
28
- def get_graph():
29
  time.sleep(3)
30
  return Div(
31
  NotStr("<ins>This simple text takes 3s to load!</ins>"),
@@ -37,7 +37,7 @@ DESC = "Demonstrates how to lazy load content"
37
  HTMX_URL = "https://htmx.org/examples/lazy-load/"
38
  DOC = """
39
  This example shows how to lazily load an element on a page. We start with an initial state that looks like this:
40
- ::main_page::
41
  Which shows a progress indicator as we are loading the graph. The graph is then loaded and faded gently into view via a settling CSS transition:
42
  ::css::
43
  """
 
14
  app, rt = fast_app(hdrs=[Style(css)])
15
 
16
 
17
+ @app.get
18
+ def page():
19
  return Div(
20
  Img(src="/img/bars.svg", alt="Result loading...", cls="htmx-indicator", width="150"),
21
+ hx_get=get_content.rt(),
22
  hx_trigger="load",
23
  cls="container",
24
  )
25
 
26
 
27
+ @app.get
28
+ def get_content():
29
  time.sleep(3)
30
  return Div(
31
  NotStr("<ins>This simple text takes 3s to load!</ins>"),
 
37
  HTMX_URL = "https://htmx.org/examples/lazy-load/"
38
  DOC = """
39
  This example shows how to lazily load an element on a page. We start with an initial state that looks like this:
40
+ ::page::
41
  Which shows a progress indicator as we are loading the graph. The graph is then loaded and faded gently into view via a settling CSS transition:
42
  ::css::
43
  """
src/tutorial/_07_inline_validation.py CHANGED
@@ -14,8 +14,8 @@ css = """
14
  app, rt = fast_app(hdrs=[Style(css)])
15
 
16
 
17
- @app.get("/form")
18
- def main_page():
19
  return Div(
20
  H3("Signup Form"),
21
  NotStr(
@@ -39,7 +39,7 @@ def validate_email(email: str):
39
  def make_email_field(value: str, error: bool, touched: bool):
40
  return Div(
41
  Label("Email"),
42
- Input(name="email", hx_post="/contact/email", hx_indicator="#ind", value=value),
43
  Img(id="ind", src="/img/bars.svg", cls="htmx-indicator"),
44
  Span("Please enter a valid email address", style="color:red;") if error else None,
45
  hx_target="this",
@@ -53,7 +53,7 @@ DOC = """
53
  This example shows how to do inline field validation, in this case of an email address. To do this we need to create a form with an input that POSTs back to the server with the value to be validated and updates the DOM with the validation results.
54
 
55
  We start with this form:
56
- ::main_page make_email_field::
57
 
58
  Note that the email div in the form has set itself as the target of the request and specified the outerHTML swap strategy, so it will be replaced entirely by the response. The input then specifies that it will POST to /contact/email for validation, when the changed event occurs (this is the default for inputs). It also specifies an indicator for the request.
59
 
 
14
  app, rt = fast_app(hdrs=[Style(css)])
15
 
16
 
17
+ @app.get
18
+ def page():
19
  return Div(
20
  H3("Signup Form"),
21
  NotStr(
 
39
  def make_email_field(value: str, error: bool, touched: bool):
40
  return Div(
41
  Label("Email"),
42
+ Input(name="email", hx_post=validate_email.rt(), hx_indicator="#ind", value=value),
43
  Img(id="ind", src="/img/bars.svg", cls="htmx-indicator"),
44
  Span("Please enter a valid email address", style="color:red;") if error else None,
45
  hx_target="this",
 
53
  This example shows how to do inline field validation, in this case of an email address. To do this we need to create a form with an input that POSTs back to the server with the value to be validated and updates the DOM with the validation results.
54
 
55
  We start with this form:
56
+ ::page make_email_field::
57
 
58
  Note that the email div in the form has set itself as the target of the request and specified the outerHTML swap strategy, so it will be replaced entirely by the response. The input then specifies that it will POST to /contact/email for validation, when the changed event occurs (this is the default for inputs). It also specifies an indicator for the request.
59
 
src/tutorial/_08_infinite_scroll.py CHANGED
@@ -4,8 +4,8 @@ app, rt = fast_app()
4
 
5
 
6
  # fmt: off
7
- @app.get("/table")
8
- def contact_table():
9
  return Div(
10
  Table(
11
  Thead(Tr(Th("Name"), Th("ID"))),
@@ -16,7 +16,7 @@ def contact_table():
16
  # fmt: on
17
 
18
 
19
- @app.get("/contacts")
20
  def load_contacts(page: int, limit: int = 5):
21
  rows = [Tr(Td("Smith"), Td((page - 1) * limit + i)) for i in range(1, limit)]
22
  return *rows, make_last_row(page, limit)
@@ -28,7 +28,7 @@ def make_last_row(page, limit):
28
  Td(page * limit),
29
  hx_trigger="revealed",
30
  hx_swap="afterend",
31
- hx_get=f"/contacts?page={page + 1}",
32
  )
33
 
34
 
 
4
 
5
 
6
  # fmt: off
7
+ @app.get
8
+ def page():
9
  return Div(
10
  Table(
11
  Thead(Tr(Th("Name"), Th("ID"))),
 
16
  # fmt: on
17
 
18
 
19
+ @app.get
20
  def load_contacts(page: int, limit: int = 5):
21
  rows = [Tr(Td("Smith"), Td((page - 1) * limit + i)) for i in range(1, limit)]
22
  return *rows, make_last_row(page, limit)
 
28
  Td(page * limit),
29
  hx_trigger="revealed",
30
  hx_swap="afterend",
31
+ hx_get=load_contacts.rt(page=page + 1),
32
  )
33
 
34
 
src/tutorial/_09_active_search.py CHANGED
@@ -7,15 +7,15 @@ app, rt = fast_app()
7
 
8
 
9
  # fmt: off
10
- @app.get("/page")
11
- def main_page():
12
  return Div(
13
  H3("Search Contacts"),
14
  Input(
15
  type="search",
16
  name="query",
17
  placeholder="Begin Typing To Search Users...",
18
- hx_post="/search",
19
  hx_trigger="input changed delay:500ms, search",
20
  hx_target="#search-results",
21
  hx_indicator=".htmx-indicator",
@@ -30,7 +30,7 @@ def main_page():
30
  # fmt: on
31
 
32
 
33
- @app.post("/search")
34
  def search(query: str, limit: int = 10):
35
  time.sleep(0.5)
36
  data = [x.split(",") for x in LINES if query.lower() in x.lower()]
@@ -45,7 +45,7 @@ DOC = """
45
  This example actively searches a contacts database as the user enters text.
46
 
47
  We start with a search input and an empty table:
48
- ::main_page::
49
  The input issues a POST to /search on the input event and sets the body of the table to be the resulting content. Note that the keyup event could be used as well, but would not fire if the user pasted text with their mouse (or any other non-keyboard method).
50
 
51
  We add the delay:500ms modifier to the trigger to delay sending the query until the user stops typing. Additionally, we add the changed modifier to the trigger to ensure we don’t send new queries when the user doesn’t change the value of the input (e.g. they hit an arrow key, or pasted the same value).
 
7
 
8
 
9
  # fmt: off
10
+ @app.get
11
+ def page():
12
  return Div(
13
  H3("Search Contacts"),
14
  Input(
15
  type="search",
16
  name="query",
17
  placeholder="Begin Typing To Search Users...",
18
+ hx_post=search.rt(),
19
  hx_trigger="input changed delay:500ms, search",
20
  hx_target="#search-results",
21
  hx_indicator=".htmx-indicator",
 
30
  # fmt: on
31
 
32
 
33
+ @app.post
34
  def search(query: str, limit: int = 10):
35
  time.sleep(0.5)
36
  data = [x.split(",") for x in LINES if query.lower() in x.lower()]
 
45
  This example actively searches a contacts database as the user enters text.
46
 
47
  We start with a search input and an empty table:
48
+ ::page::
49
  The input issues a POST to /search on the input event and sets the body of the table to be the resulting content. Note that the keyup event could be used as well, but would not fire if the user pasted text with their mouse (or any other non-keyboard method).
50
 
51
  We add the delay:500ms modifier to the trigger to delay sending the query until the user stops typing. Additionally, we add the changed modifier to the trigger to ensure we don’t send new queries when the user doesn’t change the value of the input (e.g. they hit an arrow key, or pasted the same value).
src/tutorial/_10_progress_bar.py CHANGED
@@ -31,51 +31,51 @@ app, rt = fast_app(hdrs=[Style(css)])
31
  current = 1
32
 
33
 
34
- @app.get("/page")
35
- def main_page():
36
  return Div(
37
  H3("Start Progress"),
38
- Button("Start Job", hx_post="/start", cls="btn primary"),
39
  hx_target="this",
40
  hx_swap="outerHTML",
41
  )
42
 
43
 
44
- @app.post("/start")
45
- def start_job():
46
  global current
47
  current = 1
48
  return Div(
49
  H3("Running", role="status", id="pblabel", tabindex="-1", autofocus=""),
50
  Div(
51
  get_progress(),
52
- hx_get="/job/progress",
53
  hx_trigger="every 600ms",
54
  hx_target="this",
55
  hx_swap="innerHTML",
56
  ),
57
  hx_trigger="done",
58
- hx_get="/job",
59
  hx_swap="outerHTML",
60
  hx_target="this",
61
  )
62
 
63
 
64
- @app.get("/job/progress")
65
  def get_progress():
66
  global current
67
  if current <= 100:
68
- current += 10
69
- return Div(Div(style=f"width:{current - 10}%", cls="progress-bar"), cls="progress")
70
  return HttpHeader("HX-Trigger", "done")
71
 
72
 
73
- @app.get("/job")
74
- def view_completed():
75
  return Div(
76
  H3("Complete", role="status", id="pblabel", tabindex="-1", autofocus=""),
77
  Div(Div(style="width:100%", cls="progress-bar"), cls="progress"),
78
- Button("Restart Job", hx_post="/start", cls="btn primary show"),
79
  hx_swap="outerHTML",
80
  hx_target="this",
81
  )
@@ -86,9 +86,9 @@ DOC = """
86
  This example shows how to implement a smoothly scrolling progress bar.
87
 
88
  We start with an initial state with a button that issues a POST to /start to begin the job:
89
- ::main_page::
90
  This progress bar is updated every 600 milliseconds, with the “width” style attribute and aria-valuenow attributed set to current progress value. Because there is an id on the progress bar div, htmx will smoothly transition between requests by settling the style attribute into its new value. This, when coupled with CSS transitions, makes the visual transition continuous rather than jumpy.
91
- ::start_job get_progress::
92
  Finally, when the process is complete, a server returns HX-Trigger: done header, which triggers an update of the UI to “Complete” state with a restart button added to the UI:
93
- ::view_completed::
94
  """
 
31
  current = 1
32
 
33
 
34
+ @app.get
35
+ def page():
36
  return Div(
37
  H3("Start Progress"),
38
+ Button("Start Job", hx_post=start.rt(), cls="btn primary"),
39
  hx_target="this",
40
  hx_swap="outerHTML",
41
  )
42
 
43
 
44
+ @app.post
45
+ def start():
46
  global current
47
  current = 1
48
  return Div(
49
  H3("Running", role="status", id="pblabel", tabindex="-1", autofocus=""),
50
  Div(
51
  get_progress(),
52
+ hx_get=get_progress.rt(),
53
  hx_trigger="every 600ms",
54
  hx_target="this",
55
  hx_swap="innerHTML",
56
  ),
57
  hx_trigger="done",
58
+ hx_get=job_finished.rt(),
59
  hx_swap="outerHTML",
60
  hx_target="this",
61
  )
62
 
63
 
64
+ @app.get
65
  def get_progress():
66
  global current
67
  if current <= 100:
68
+ current += 20
69
+ return Div(Div(style=f"width:{current - 20}%", cls="progress-bar"), cls="progress")
70
  return HttpHeader("HX-Trigger", "done")
71
 
72
 
73
+ @app.get
74
+ def job_finished():
75
  return Div(
76
  H3("Complete", role="status", id="pblabel", tabindex="-1", autofocus=""),
77
  Div(Div(style="width:100%", cls="progress-bar"), cls="progress"),
78
+ Button("Restart Job", hx_post=start.rt(), cls="btn primary show"),
79
  hx_swap="outerHTML",
80
  hx_target="this",
81
  )
 
86
  This example shows how to implement a smoothly scrolling progress bar.
87
 
88
  We start with an initial state with a button that issues a POST to /start to begin the job:
89
+ ::page::
90
  This progress bar is updated every 600 milliseconds, with the “width” style attribute and aria-valuenow attributed set to current progress value. Because there is an id on the progress bar div, htmx will smoothly transition between requests by settling the style attribute into its new value. This, when coupled with CSS transitions, makes the visual transition continuous rather than jumpy.
91
+ ::start get_progress::
92
  Finally, when the process is complete, a server returns HX-Trigger: done header, which triggers an update of the UI to “Complete” state with a restart button added to the UI:
93
+ ::job_finished::
94
  """
src/tutorial/_11_cascading_select.py CHANGED
@@ -30,7 +30,7 @@ def page():
30
  )
31
 
32
 
33
- @app.get("/models")
34
  def load_models(make: str, sleep: int = 0):
35
  time.sleep(sleep)
36
  cars = {
 
30
  )
31
 
32
 
33
+ @app.get
34
  def load_models(make: str, sleep: int = 0):
35
  time.sleep(sleep)
36
  cars = {
tests/test_click_to_edit.py CHANGED
@@ -12,12 +12,12 @@ def test_app():
12
  assert c.name in r.text
13
  assert c.email in r.text
14
 
15
- r = client.get("/contact/1")
16
  check(r)
17
 
18
- r = client.get("/contact/1/edit")
19
  check(r)
20
 
21
- r = client.put("/contact/1", data={"name": "AAA", "email": "BB"})
22
  check(r)
23
  assert module.current.name == "AAA"
 
12
  assert c.name in r.text
13
  assert c.email in r.text
14
 
15
+ r = client.get("/contact")
16
  check(r)
17
 
18
+ r = client.get("/contact/edit")
19
  check(r)
20
 
21
+ r = client.put("/contact", data={"name": "AAA", "email": "BB"})
22
  check(r)
23
  assert module.current.name == "AAA"
uv.lock CHANGED
@@ -240,15 +240,15 @@ wheels = [
240
 
241
  [[package]]
242
  name = "fastlite"
243
- version = "0.0.10"
244
  source = { registry = "https://pypi.org/simple" }
245
  dependencies = [
246
  { name = "fastcore" },
247
  { name = "sqlite-minutils" },
248
  ]
249
- sdist = { url = "https://files.pythonhosted.org/packages/dd/5c/c3abd6e7833015a06e58b7a097e7f7c5ba5397c72bca53a2f4331f6d8197/fastlite-0.0.10.tar.gz", hash = "sha256:90f111dc30338b1bbc7461b767b23e207356227d968eac60b0e06d5242f7b593", size = 19387 }
250
  wheels = [
251
- { url = "https://files.pythonhosted.org/packages/69/e4/88b21b443e91f6e80fe8ef98b7d585fa74e3613e44c8bbd936e0f80414de/fastlite-0.0.10-py3-none-any.whl", hash = "sha256:3a4a510fb411e39cda98111d32421838b02c3a0ccaa1c543393c22ddd9f4553d", size = 15607 },
252
  ]
253
 
254
  [[package]]
@@ -895,27 +895,27 @@ wheels = [
895
 
896
  [[package]]
897
  name = "ruff"
898
- version = "0.6.3"
899
- source = { registry = "https://pypi.org/simple" }
900
- sdist = { url = "https://files.pythonhosted.org/packages/5d/f9/0b32e5d1c6f957df49398cd882a011e9488fcbca0d6acfeeea50ccd37a4d/ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983", size = 2463514 }
901
- wheels = [
902
- { url = "https://files.pythonhosted.org/packages/72/68/1da6a1e39a03a229ea57c511691d6225072759cc7764206c3f0989521194/ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3", size = 9696928 },
903
- { url = "https://files.pythonhosted.org/packages/6e/59/3b8b1d3a4271c6eb6ceecd3cef19a6d881639a0f18ad651563d6f619aaae/ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc", size = 9448462 },
904
- { url = "https://files.pythonhosted.org/packages/35/4f/b942ecb8bbebe53aa9b33e9b96df88acd50b70adaaed3070f1d92131a1cb/ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1", size = 9176190 },
905
- { url = "https://files.pythonhosted.org/packages/a0/20/b0bcb29d4ee437f3567b73b6905c034e2e94d29b9b826c66daecc1cf6388/ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1", size = 10108892 },
906
- { url = "https://files.pythonhosted.org/packages/9c/e3/211bc759f424e8823a9937e0f678695ca02113c621dfde1fa756f9f26f6d/ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672", size = 9476471 },
907
- { url = "https://files.pythonhosted.org/packages/b2/a3/2ec35a2d7a554364864206f0e46812b92a074ad8a014b923d821ead532aa/ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1", size = 10294802 },
908
- { url = "https://files.pythonhosted.org/packages/03/8b/56ef687b3489c88886dea48c78fb4969b6b65f18007d0ac450070edd1f58/ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384", size = 11022372 },
909
- { url = "https://files.pythonhosted.org/packages/a5/21/327d147feb442adb88975e81e2263102789eba9ad2afa102c661912a482f/ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a", size = 10596596 },
910
- { url = "https://files.pythonhosted.org/packages/6c/86/ff386de63729da3e08c8099c57f577a00ec9f3eea711b23ac07cf3588dc5/ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500", size = 11572830 },
911
- { url = "https://files.pythonhosted.org/packages/38/5d/b33284c108e3f315ddd09b70296fd76bd28ecf8965a520bc93f3bbd8ac40/ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470", size = 10262577 },
912
- { url = "https://files.pythonhosted.org/packages/29/99/9cdfad0d7f460e66567236eddc691473791afd9aff93a0dfcdef0462a6c7/ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f", size = 10098751 },
913
- { url = "https://files.pythonhosted.org/packages/a8/9f/f801a1619f5549e552f1f722f1db57eb39e7e1d83d482133142781d450de/ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5", size = 9563859 },
914
- { url = "https://files.pythonhosted.org/packages/0b/4d/fb2424faf04ffdb960ae2b3a1d991c5183dd981003de727d2d5cc38abc98/ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351", size = 9914291 },
915
- { url = "https://files.pythonhosted.org/packages/2e/dd/94fddf002a8f6152e8ebfbb51d3f93febc415c1fe694345623c31ce8b33b/ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8", size = 10331549 },
916
- { url = "https://files.pythonhosted.org/packages/b4/73/ca9c2f9237a430ca423b6dca83b77e9a428afeb7aec80596e86c369123fe/ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521", size = 7962163 },
917
- { url = "https://files.pythonhosted.org/packages/55/ce/061c605b1dfb52748d59bc0c7a8507546c178801156415773d18febfd71d/ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb", size = 8800901 },
918
- { url = "https://files.pythonhosted.org/packages/63/28/ae4ffe7d3b6134ca6d31ebef07447ef70097c4a9e8fbbc519b374c5c1559/ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82", size = 8229171 },
919
  ]
920
 
921
  [[package]]
 
240
 
241
  [[package]]
242
  name = "fastlite"
243
+ version = "0.0.11"
244
  source = { registry = "https://pypi.org/simple" }
245
  dependencies = [
246
  { name = "fastcore" },
247
  { name = "sqlite-minutils" },
248
  ]
249
+ sdist = { url = "https://files.pythonhosted.org/packages/48/98/b0024670a63cfdea1561e665edee4f542fc3165eea85850ba68123fe6de7/fastlite-0.0.11.tar.gz", hash = "sha256:3ba61eeb510b14952c24ddc69947bff79324524d0e47dfb91ff1d8fe1492adfe", size = 20300 }
250
  wheels = [
251
+ { url = "https://files.pythonhosted.org/packages/e1/6b/25e0abd3f300a20e39cca2e31ca105b8b66dc6758d09e67ac97dd27b6fcb/fastlite-0.0.11-py3-none-any.whl", hash = "sha256:66984ab849ae41d85d205fba3e057c24e967525184f9ecbd7536761f5551392d", size = 16195 },
252
  ]
253
 
254
  [[package]]
 
895
 
896
  [[package]]
897
  name = "ruff"
898
+ version = "0.6.4"
899
+ source = { registry = "https://pypi.org/simple" }
900
+ sdist = { url = "https://files.pythonhosted.org/packages/a4/55/9f485266e6326cab707369601b13e3e72eb90ba3eee2d6779549a00a0d58/ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212", size = 2469375 }
901
+ wheels = [
902
+ { url = "https://files.pythonhosted.org/packages/e3/78/307591f81d09c8721b5e64539f287c82c81a46f46d16278eb27941ac17f9/ruff-0.6.4-py3-none-linux_armv6l.whl", hash = "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258", size = 9692673 },
903
+ { url = "https://files.pythonhosted.org/packages/69/63/ef398fcacdbd3995618ed30b5a6c809a1ebbf112ba604b3f5b8c3be464cf/ruff-0.6.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60", size = 9481182 },
904
+ { url = "https://files.pythonhosted.org/packages/a6/fd/8784e3bbd79bc17de0a62de05fe5165f494ff7d77cb06630d6428c2f10d2/ruff-0.6.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f", size = 9174356 },
905
+ { url = "https://files.pythonhosted.org/packages/6d/bc/c69db2d68ac7bfbb222c81dc43a86e0402d0063e20b13e609f7d17d81d3f/ruff-0.6.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc", size = 10129365 },
906
+ { url = "https://files.pythonhosted.org/packages/3b/10/8ed14ff60a4e5eb08cac0a04a9b4e8590c72d1ce4d29ef22cef97d19536d/ruff-0.6.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617", size = 9483351 },
907
+ { url = "https://files.pythonhosted.org/packages/a9/69/13316b8d64ffd6a43627cf0753339a7f95df413450c301a60904581bee6e/ruff-0.6.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408", size = 10301099 },
908
+ { url = "https://files.pythonhosted.org/packages/42/00/9623494087272643e8f02187c266638306c6829189a5bf1446968bbe438b/ruff-0.6.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e", size = 11033216 },
909
+ { url = "https://files.pythonhosted.org/packages/c5/31/e0c9d881db42ea1267e075c29aafe0db5a8a3024b131f952747f6234f858/ruff-0.6.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818", size = 10618140 },
910
+ { url = "https://files.pythonhosted.org/packages/5b/35/f1d8b746aedd4c8fde4f83397e940cc4c8fc619860ebbe3073340381a34d/ruff-0.6.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6", size = 11606672 },
911
+ { url = "https://files.pythonhosted.org/packages/c5/70/899b03cbb3eb48ed0507d4b32b6f7aee562bc618ef9ffda855ec98c0461a/ruff-0.6.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa", size = 10288013 },
912
+ { url = "https://files.pythonhosted.org/packages/17/c6/906bf895640521ca5115ccdd857b2bac42bd61facde6620fdc2efc0a4806/ruff-0.6.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6", size = 10109473 },
913
+ { url = "https://files.pythonhosted.org/packages/28/da/1284eb04172f8a5d42eb52fce9d643dd747ac59a4ed6c5d42729f72e934d/ruff-0.6.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d", size = 9568817 },
914
+ { url = "https://files.pythonhosted.org/packages/6c/e2/f8250b54edbb2e9222e22806e1bcc35a192ac18d1793ea556fa4977a843a/ruff-0.6.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa", size = 9910840 },
915
+ { url = "https://files.pythonhosted.org/packages/9c/7c/dcf2c10562346ecdf6f0e5f6669b2ddc9a74a72956c3f419abd6820c2aff/ruff-0.6.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1", size = 10354263 },
916
+ { url = "https://files.pythonhosted.org/packages/f1/94/c39d7ac5729e94788110503d928c98c203488664b0fb92c2b801cb832bec/ruff-0.6.4-py3-none-win32.whl", hash = "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523", size = 7958602 },
917
+ { url = "https://files.pythonhosted.org/packages/6b/d2/2dee8c547bee3d4cfdd897f7b8e38510383acaff2c8130ea783b67631d72/ruff-0.6.4-py3-none-win_amd64.whl", hash = "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58", size = 8795059 },
918
+ { url = "https://files.pythonhosted.org/packages/07/1a/23280818aa4fa89bd0552aab10857154e1d3b90f27b5b745f09ec1ac6ad8/ruff-0.6.4-py3-none-win_arm64.whl", hash = "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14", size = 8239636 },
919
  ]
920
 
921
  [[package]]