Skip to content

Commit 95ac506

Browse files
authored
Form designer improvements and cleanup for 0.6.5/0.6.6 (#287)
* Use a label for radio buttons * Use new style rx.Var * Use correct annotation for handle_submit * Handle required fields * Remove quoted_var, no longer necessary * Mark missing required fields when submitting form * ruff check and format * form-designer: add version and logo to home page * Directly register dynamic route args Avoid the hack of adding a page that will fail to evaluate. This doesn't work in recent reflex versions since page evaluation is delayed.
1 parent 9074271 commit 95ac506

File tree

9 files changed

+80
-41
lines changed

9 files changed

+80
-41
lines changed

form-designer/form_designer/components/field_editor.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""Edit Field modal."""
2+
3+
from typing import Any
24
import reflex as rx
35

46
from .. import constants, routes, utils
@@ -19,7 +21,7 @@ class FieldEditorState(AppState):
1921
def _user_has_access(self):
2022
return self.form_owner_id == self.authenticated_user.id or self.is_admin
2123

22-
def handle_submit(self, form_data: dict[str, str]):
24+
def handle_submit(self, form_data: dict[str, Any]):
2325
self.field.name = form_data["field_name"]
2426
self.field.type_ = form_data["type_"]
2527
self.field.required = bool(form_data.get("required"))
@@ -266,20 +268,20 @@ def field_editor_modal():
266268
def field_edit_title():
267269
form_name = rx.cond(
268270
rx.State.form_id == "",
269-
utils.quoted_var("New Form"),
271+
"New Form",
270272
rx.cond(
271273
FormEditorState.form,
272274
FormEditorState.form.name,
273-
utils.quoted_var("Unknown Form"),
275+
"Unknown Form",
274276
),
275277
)
276278
field_name = rx.cond(
277279
rx.State.field_id == "",
278-
utils.quoted_var("New Field"),
280+
"New Field",
279281
rx.cond(
280282
FieldEditorState.field,
281283
FieldEditorState.field.name,
282-
utils.quoted_var("Unknown Field"),
284+
"Unknown Field",
283285
),
284286
)
285287
return f"{constants.TITLE} | {form_name} | {field_name}"

form-designer/form_designer/components/field_view.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
class OptionItemCallable:
77
def __call__(
88
self, *children: rx.Component, value: rx.Var[str], **props
9-
) -> rx.Component:
10-
...
9+
) -> rx.Component: ...
1110

1211

1312
def option_value_label_id(option: Option) -> rx.Component:
@@ -51,9 +50,12 @@ def field_select(field: Field) -> rx.Component:
5150

5251

5352
def radio_item(*children: rx.Component, value: rx.Var[str], **props) -> rx.Component:
54-
return rx.hstack(
55-
rx.radio.item(value=value, **props),
56-
*children,
53+
return rx.el.label(
54+
rx.hstack(
55+
rx.radio.item(value=value, **props),
56+
*children,
57+
align="center",
58+
),
5759
)
5860

5961

@@ -132,14 +134,18 @@ def field_prompt(field: Field, show_name: bool = False):
132134
)
133135

134136

135-
def field_view(field: Field):
136-
return rx.card(
137-
rx.hstack(
138-
field_prompt(field),
139-
rx.text(rx.cond(field.required, "*", "")),
140-
),
141-
rx.hstack(
142-
field_input(field),
143-
flex_wrap="wrap",
137+
def field_view(field: Field, *children: rx.Component, card_props: dict | None = None):
138+
return rx.form.field(
139+
rx.card(
140+
rx.hstack(
141+
field_prompt(field),
142+
rx.text(rx.cond(field.required, "*", "")),
143+
),
144+
rx.hstack(
145+
field_input(field),
146+
flex_wrap="wrap",
147+
),
148+
*children,
149+
**(card_props or {}),
144150
),
145151
)

form-designer/form_designer/components/form_editor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import reflex as rx
22

3-
from .. import constants, routes, utils
3+
from .. import constants, routes
44
from ..models import Field, Form
55
from ..state import AppState
66
from .field_view import field_input, field_prompt
@@ -160,11 +160,11 @@ def form_editor():
160160
def form_edit_title():
161161
form_name = rx.cond(
162162
rx.State.form_id == "",
163-
utils.quoted_var("New Form"),
163+
"New Form",
164164
rx.cond(
165165
FormEditorState.form,
166166
FormEditorState.form.name,
167-
utils.quoted_var("Unknown Form"),
167+
"Unknown Form",
168168
),
169169
)
170170
return f"{constants.TITLE} | {form_name}"

form-designer/form_designer/form_designer.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import reflex_local_auth
55

6-
from . import constants, routes, utils
6+
from . import constants, routes
77
from .components import (
88
FieldEditorState,
99
FormEditorState,
@@ -24,9 +24,8 @@
2424
app = rx.App(theme=rx.theme(accent_color="blue"))
2525
app.add_page(home_page, route="/", title=constants.TITLE)
2626

27-
# Adding a dummy route to register the dynamic route vars.
28-
with contextlib.suppress(ValueError):
29-
app.add_page(lambda: rx.fragment(on_click=rx.event.noop()), route="/_dummy/[form_id]/[field_id]")
27+
# Register the dynamic route vars.
28+
rx.State.setup_dynamic_args(rx.app.get_route_args("/_dummy/[form_id]/[field_id]"))
3029

3130
# Authentication via reflex-local-auth
3231
app.add_page(
@@ -74,7 +73,7 @@
7473
route=routes.FORM_ENTRY,
7574
title=rx.cond(
7675
rx.State.form_id == "",
77-
utils.quoted_var("Unknown Form"),
76+
"Unknown Form",
7877
FormEntryState.form.name,
7978
),
8079
on_load=FormEntryState.load_form,

form-designer/form_designer/pages/form_editor.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from ..components import navbar, form_select, form_editor, field_editor_modal
55

66

7-
87
@utils.require_login
98
def form_editor_page() -> rx.Component:
109
return style.layout(
@@ -33,5 +32,3 @@ def form_editor_page() -> rx.Component:
3332
),
3433
rx.logo(height="3em", margin_bottom="12px"),
3534
)
36-
37-

form-designer/form_designer/pages/form_entry.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
from typing import Any
12
import reflex as rx
23

34
from reflex_local_auth import LocalAuthState
45

56
from .. import routes, style
67
from ..components import field_view, navbar
7-
from ..models import FieldType, FieldValue, Form, Response
8+
from ..models import Field, FieldType, FieldValue, Form, Response
89

910

1011
Missing = object()
@@ -13,13 +14,15 @@
1314
class FormEntryState(rx.State):
1415
form: Form = Form()
1516
client_token: str = rx.Cookie("")
17+
missing_fields: dict[str, bool] = {}
1618

1719
def _ensure_client_token(self):
1820
if self.client_token == "":
1921
self.client_token = self.router.session.client_token
2022
return self.client_token
2123

2224
def load_form(self):
25+
self.missing_fields = {}
2326
if self.form_id != "":
2427
self.load_form_by_id(self.form_id)
2528
else:
@@ -29,27 +32,39 @@ def load_form_by_id(self, id_: int):
2932
with rx.session() as session:
3033
self.form = session.get(Form, id_)
3134

32-
def handle_submit(self, form_data):
35+
def handle_submit(self, form_data: dict[str, Any]):
36+
self.missing_fields = {}
3337
response = Response(
3438
client_token=self._ensure_client_token(), form_id=self.form.id
3539
)
3640
for field in self.form.fields:
3741
value = form_data.get(field.name, Missing)
38-
if value is not Missing:
42+
if value and value is not Missing:
3943
response.field_values.append(
4044
FieldValue(
4145
field_id=field.id,
4246
value=value,
4347
)
4448
)
4549
elif field.type_ == FieldType.checkbox:
50+
field_values = []
4651
for option in field.options:
4752
key = f"{field.name}___{option.value or option.label or option.id}"
4853
value = form_data.get(key, Missing)
4954
if value is not Missing:
50-
response.field_values.append(
55+
field_values.append(
5156
FieldValue(field_id=field.id, value=form_data[key])
5257
)
58+
if field.required and not field_values:
59+
self.missing_fields[field.prompt or field.name] = True
60+
elif field.required:
61+
self.missing_fields[field.prompt or field.name] = True
62+
if self.missing_fields:
63+
if len(self.missing_fields) == 1:
64+
return rx.toast(
65+
f"Required field '{tuple(self.missing_fields)[0]}' is missing a response"
66+
)
67+
return rx.toast("Multiple required fields are missing a response")
5368
with rx.session() as session:
5469
session.add(response)
5570
session.commit()
@@ -66,6 +81,25 @@ def authenticated_navbar(title_suffix: str | None = None):
6681
)
6782

6883

84+
def validated_field_view(field: Field) -> rx.Component:
85+
return field_view(
86+
field,
87+
rx.form.message(
88+
"This field is required.",
89+
match="valueMissing",
90+
force_match=FormEntryState.missing_fields[field.name],
91+
color=rx.color("tomato", 10),
92+
),
93+
card_props={
94+
"--base-card-surface-box-shadow": rx.cond(
95+
FormEntryState.missing_fields[field.name],
96+
f"0 0 0 1px {rx.color('tomato', 10)}",
97+
"inherit",
98+
),
99+
},
100+
)
101+
102+
69103
def form_entry_page():
70104
return style.layout(
71105
authenticated_navbar(title_suffix=f"Preview {FormEntryState.form.id}"),
@@ -74,7 +108,7 @@ def form_entry_page():
74108
rx.center(rx.heading(FormEntryState.form.name)),
75109
rx.foreach(
76110
FormEntryState.form.fields,
77-
field_view,
111+
validated_field_view,
78112
),
79113
rx.button("Submit", type="submit"),
80114
),

form-designer/form_designer/pages/home.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import importlib.metadata
12
from pathlib import Path
23

34
import reflex as rx
@@ -22,4 +23,9 @@ def home_page() -> rx.Component:
2223
rx.markdown(readme_content.read_text()),
2324
margin_y="2em",
2425
),
26+
rx.hstack(
27+
rx.logo(),
28+
rx.text(f"v{importlib.metadata.version('reflex')}", size="1"),
29+
align="center",
30+
),
2531
)

form-designer/form_designer/pages/response.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def response(r: Response):
7575
def responses_title():
7676
form_name = rx.cond(
7777
rx.State.form_id == "",
78-
utils.quoted_var("Unknown Form"),
78+
"Unknown Form",
7979
ResponsesState.form.name,
8080
)
8181
return f"{constants.TITLE} | {form_name} | Responses"

form-designer/form_designer/utils.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@
55
from reflex_local_auth import LoginState
66

77

8-
def quoted_var(value: str) -> rx.Var:
9-
"""Allows a bare string to be used in a page title with other Vars."""
10-
return rx.Var.create_safe(f"'{value}'", _var_is_string=False, _var_is_local=True)
11-
12-
138
def require_login(page: rx.app.ComponentCallable) -> rx.app.ComponentCallable:
149
"""Decorator to require authentication before rendering a page.
1510

0 commit comments

Comments
 (0)