Skip to content

Commit 5d647a4

Browse files
authored
[REF-1368] Move common form functionality to rx.el.forms (#2801)
* [REF-1368] Move common form functionality to rx.el.forms Allow plain HTML Form element to have magic on_submit event handler. * Chakra and Radix forms inherit `on_submit` functionality from rx.el.form Consolidate logic in the basic HTML form and use it in both Radix and Chakra form wrappers. * from __future__ import annotations for py38
1 parent 8903ebb commit 5d647a4

File tree

7 files changed

+577
-320
lines changed

7 files changed

+577
-320
lines changed

integration/test_form_submit.py

+34-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Integration tests for forms."""
2+
import functools
23
import time
34
from typing import Generator
45

@@ -10,8 +11,12 @@
1011
from reflex.utils import format
1112

1213

13-
def FormSubmit():
14-
"""App with a form using on_submit."""
14+
def FormSubmit(form_component):
15+
"""App with a form using on_submit.
16+
17+
Args:
18+
form_component: The str name of the form component to use.
19+
"""
1520
import reflex as rx
1621

1722
class FormState(rx.State):
@@ -32,7 +37,7 @@ def index():
3237
is_read_only=True,
3338
id="token",
3439
),
35-
rx.form.root(
40+
eval(form_component)(
3641
rx.vstack(
3742
rx.chakra.input(id="name_input"),
3843
rx.hstack(rx.chakra.pin_input(length=4, id="pin_input")),
@@ -63,8 +68,12 @@ def index():
6368
)
6469

6570

66-
def FormSubmitName():
67-
"""App with a form using on_submit."""
71+
def FormSubmitName(form_component):
72+
"""App with a form using on_submit.
73+
74+
Args:
75+
form_component: The str name of the form component to use.
76+
"""
6877
import reflex as rx
6978

7079
class FormState(rx.State):
@@ -85,7 +94,7 @@ def index():
8594
is_read_only=True,
8695
id="token",
8796
),
88-
rx.form.root(
97+
eval(form_component)(
8998
rx.vstack(
9099
rx.chakra.input(name="name_input"),
91100
rx.hstack(rx.chakra.pin_input(length=4, name="pin_input")),
@@ -128,7 +137,23 @@ def index():
128137

129138

130139
@pytest.fixture(
131-
scope="session", params=[FormSubmit, FormSubmitName], ids=["id", "name"]
140+
scope="session",
141+
params=[
142+
functools.partial(FormSubmit, form_component="rx.form.root"),
143+
functools.partial(FormSubmitName, form_component="rx.form.root"),
144+
functools.partial(FormSubmit, form_component="rx.el.form"),
145+
functools.partial(FormSubmitName, form_component="rx.el.form"),
146+
functools.partial(FormSubmit, form_component="rx.chakra.form"),
147+
functools.partial(FormSubmitName, form_component="rx.chakra.form"),
148+
],
149+
ids=[
150+
"id-radix",
151+
"name-radix",
152+
"id-html",
153+
"name-html",
154+
"id-chakra",
155+
"name-chakra",
156+
],
132157
)
133158
def form_submit(request, tmp_path_factory) -> Generator[AppHarness, None, None]:
134159
"""Start FormSubmit app at tmp_path via AppHarness.
@@ -140,9 +165,11 @@ def form_submit(request, tmp_path_factory) -> Generator[AppHarness, None, None]:
140165
Yields:
141166
running AppHarness instance
142167
"""
168+
param_id = request._pyfuncitem.callspec.id.replace("-", "_")
143169
with AppHarness.create(
144170
root=tmp_path_factory.mktemp("form_submit"),
145171
app_source=request.param, # type: ignore
172+
app_name=request.param.func.__name__ + f"_{param_id}",
146173
) as harness:
147174
assert harness.app_instance is not None, "app is not running"
148175
yield harness

reflex/components/chakra/forms/form.py

+5-137
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,20 @@
11
"""Form components."""
22
from __future__ import annotations
33

4-
from hashlib import md5
5-
from typing import Any, Dict, Iterator
6-
7-
from jinja2 import Environment
8-
94
from reflex.components.chakra import ChakraComponent
105
from reflex.components.component import Component
11-
from reflex.components.tags import Tag
12-
from reflex.constants import Dirs, EventTriggers
13-
from reflex.event import EventChain
14-
from reflex.utils import imports
15-
from reflex.utils.format import format_event_chain, to_camel_case
16-
from reflex.vars import BaseVar, Var
17-
18-
FORM_DATA = Var.create("form_data")
19-
HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
20-
"""
21-
const handleSubmit_{{ handle_submit_unique_name }} = useCallback((ev) => {
22-
const $form = ev.target
23-
ev.preventDefault()
24-
const {{ form_data }} = {...Object.fromEntries(new FormData($form).entries()), ...{{ field_ref_mapping }}}
25-
26-
{{ on_submit_event_chain }}
27-
28-
if ({{ reset_on_submit }}) {
29-
$form.reset()
30-
}
31-
})
32-
"""
33-
)
34-
35-
36-
class Form(ChakraComponent):
6+
from reflex.components.el.elements.forms import Form as HTMLForm
7+
from reflex.vars import Var
8+
9+
10+
class Form(ChakraComponent, HTMLForm):
3711
"""A form component."""
3812

3913
tag = "Box"
4014

4115
# What the form renders to.
4216
as_: Var[str] = "form" # type: ignore
4317

44-
# If true, the form will be cleared after submit.
45-
reset_on_submit: Var[bool] = False # type: ignore
46-
47-
# The name used to make this form's submit handler function unique
48-
handle_submit_unique_name: Var[str]
49-
50-
@classmethod
51-
def create(cls, *children, **props) -> Component:
52-
"""Create a form component.
53-
54-
Args:
55-
*children: The children of the form.
56-
**props: The properties of the form.
57-
58-
Returns:
59-
The form component.
60-
"""
61-
if "handle_submit_unique_name" in props:
62-
return super().create(*children, **props)
63-
64-
# Render the form hooks and use the hash of the resulting code to create a unique name.
65-
props["handle_submit_unique_name"] = ""
66-
form = super().create(*children, **props)
67-
code_hash = md5(str(form.get_hooks()).encode("utf-8")).hexdigest()
68-
form.handle_submit_unique_name = code_hash
69-
return form
70-
71-
def _get_imports(self) -> imports.ImportDict:
72-
return imports.merge_imports(
73-
super()._get_imports(),
74-
{
75-
"react": {imports.ImportVar(tag="useCallback")},
76-
f"/{Dirs.STATE_PATH}": {
77-
imports.ImportVar(tag="getRefValue"),
78-
imports.ImportVar(tag="getRefValues"),
79-
},
80-
},
81-
)
82-
83-
def _get_hooks(self) -> str | None:
84-
if EventTriggers.ON_SUBMIT not in self.event_triggers:
85-
return
86-
return HANDLE_SUBMIT_JS_JINJA2.render(
87-
handle_submit_unique_name=self.handle_submit_unique_name,
88-
form_data=FORM_DATA,
89-
field_ref_mapping=str(Var.create_safe(self._get_form_refs())),
90-
on_submit_event_chain=format_event_chain(
91-
self.event_triggers[EventTriggers.ON_SUBMIT]
92-
),
93-
reset_on_submit=self.reset_on_submit,
94-
)
95-
96-
def _render(self) -> Tag:
97-
render_tag = (
98-
super()
99-
._render()
100-
.remove_props(
101-
"reset_on_submit",
102-
"handle_submit_unique_name",
103-
to_camel_case(EventTriggers.ON_SUBMIT),
104-
)
105-
)
106-
if EventTriggers.ON_SUBMIT in self.event_triggers:
107-
render_tag.add_props(
108-
**{
109-
EventTriggers.ON_SUBMIT: BaseVar(
110-
_var_name=f"handleSubmit_{self.handle_submit_unique_name}",
111-
_var_type=EventChain,
112-
)
113-
}
114-
)
115-
return render_tag
116-
117-
def _get_form_refs(self) -> Dict[str, Any]:
118-
# Send all the input refs to the handler.
119-
form_refs = {}
120-
for ref in self.get_refs():
121-
# when ref start with refs_ it's an array of refs, so we need different method
122-
# to collect data
123-
if ref.startswith("refs_"):
124-
ref_var = Var.create_safe(ref[:-3]).as_ref()
125-
form_refs[ref[5:-3]] = Var.create_safe(
126-
f"getRefValues({str(ref_var)})", _var_is_local=False
127-
)._replace(merge_var_data=ref_var._var_data)
128-
else:
129-
ref_var = Var.create_safe(ref).as_ref()
130-
form_refs[ref[4:]] = Var.create_safe(
131-
f"getRefValue({str(ref_var)})", _var_is_local=False
132-
)._replace(merge_var_data=ref_var._var_data)
133-
return form_refs
134-
135-
def get_event_triggers(self) -> Dict[str, Any]:
136-
"""Get the event triggers that pass the component's value to the handler.
137-
138-
Returns:
139-
A dict mapping the event trigger to the var that is passed to the handler.
140-
"""
141-
return {
142-
**super().get_event_triggers(),
143-
EventTriggers.ON_SUBMIT: lambda e0: [FORM_DATA],
144-
}
145-
146-
def _get_vars(self) -> Iterator[Var]:
147-
yield from super()._get_vars()
148-
yield from self._get_form_refs().values()
149-
15018

15119
class FormControl(ChakraComponent):
15220
"""Provide context to form components."""

reflex/components/chakra/forms/form.pyi

+94-17
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,85 @@ from typing import Any, Dict, Literal, Optional, Union, overload
77
from reflex.vars import Var, BaseVar, ComputedVar
88
from reflex.event import EventChain, EventHandler, EventSpec
99
from reflex.style import Style
10-
from hashlib import md5
11-
from typing import Any, Dict, Iterator
12-
from jinja2 import Environment
1310
from reflex.components.chakra import ChakraComponent
1411
from reflex.components.component import Component
15-
from reflex.components.tags import Tag
16-
from reflex.constants import Dirs, EventTriggers
17-
from reflex.event import EventChain
18-
from reflex.utils import imports
19-
from reflex.utils.format import format_event_chain, to_camel_case
20-
from reflex.vars import BaseVar, Var
12+
from reflex.components.el.elements.forms import Form as HTMLForm
13+
from reflex.vars import Var
2114

22-
FORM_DATA = Var.create("form_data")
23-
HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
24-
"\n const handleSubmit_{{ handle_submit_unique_name }} = useCallback((ev) => {\n const $form = ev.target\n ev.preventDefault()\n const {{ form_data }} = {...Object.fromEntries(new FormData($form).entries()), ...{{ field_ref_mapping }}}\n\n {{ on_submit_event_chain }}\n\n if ({{ reset_on_submit }}) {\n $form.reset()\n }\n })\n "
25-
)
26-
27-
class Form(ChakraComponent):
15+
class Form(ChakraComponent, HTMLForm):
2816
@overload
2917
@classmethod
3018
def create( # type: ignore
3119
cls,
3220
*children,
3321
as_: Optional[Union[Var[str], str]] = None,
22+
accept: Optional[
23+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
24+
] = None,
25+
accept_charset: Optional[
26+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
27+
] = None,
28+
action: Optional[
29+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
30+
] = None,
31+
auto_complete: Optional[
32+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
33+
] = None,
34+
enc_type: Optional[
35+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
36+
] = None,
37+
method: Optional[
38+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
39+
] = None,
40+
name: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
41+
no_validate: Optional[
42+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
43+
] = None,
44+
target: Optional[
45+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
46+
] = None,
3447
reset_on_submit: Optional[Union[Var[bool], bool]] = None,
3548
handle_submit_unique_name: Optional[Union[Var[str], str]] = None,
49+
access_key: Optional[
50+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
51+
] = None,
52+
auto_capitalize: Optional[
53+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
54+
] = None,
55+
content_editable: Optional[
56+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
57+
] = None,
58+
context_menu: Optional[
59+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
60+
] = None,
61+
dir: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
62+
draggable: Optional[
63+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
64+
] = None,
65+
enter_key_hint: Optional[
66+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
67+
] = None,
68+
hidden: Optional[
69+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
70+
] = None,
71+
input_mode: Optional[
72+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
73+
] = None,
74+
item_prop: Optional[
75+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
76+
] = None,
77+
lang: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
78+
role: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
79+
slot: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
80+
spell_check: Optional[
81+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
82+
] = None,
83+
tab_index: Optional[
84+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
85+
] = None,
86+
title: Optional[
87+
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
88+
] = None,
3689
style: Optional[Style] = None,
3790
key: Optional[Any] = None,
3891
id: Optional[Any] = None,
@@ -94,8 +147,33 @@ class Form(ChakraComponent):
94147
Args:
95148
*children: The children of the form.
96149
as_: What the form renders to.
150+
accept: MIME types the server accepts for file upload
151+
accept_charset: Character encodings to be used for form submission
152+
action: URL where the form's data should be submitted
153+
auto_complete: Whether the form should have autocomplete enabled
154+
enc_type: Encoding type for the form data when submitted
155+
method: HTTP method to use for form submission
156+
name: Name of the form
157+
no_validate: Indicates that the form should not be validated on submit
158+
target: Where to display the response after submitting the form
97159
reset_on_submit: If true, the form will be cleared after submit.
98-
handle_submit_unique_name: The name used to make this form's submit handler function unique
160+
handle_submit_unique_name: The name used to make this form's submit handler function unique.
161+
access_key: Provides a hint for generating a keyboard shortcut for the current element.
162+
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
163+
content_editable: Indicates whether the element's content is editable.
164+
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
165+
dir: Defines the text direction. Allowed values are ltr (Left-To-Right) or rtl (Right-To-Left)
166+
draggable: Defines whether the element can be dragged.
167+
enter_key_hint: Hints what media types the media element is able to play.
168+
hidden: Defines whether the element is hidden.
169+
input_mode: Defines the type of the element.
170+
item_prop: Defines the name of the element for metadata purposes.
171+
lang: Defines the language used in the element.
172+
role: Defines the role of the element.
173+
slot: Assigns a slot in a shadow DOM shadow tree to an element.
174+
spell_check: Defines whether the element may be checked for spelling errors.
175+
tab_index: Defines the position of the current element in the tabbing order.
176+
title: Defines a tooltip for the element.
99177
style: The style of the component.
100178
key: A unique key for the component.
101179
id: The id for the component.
@@ -108,7 +186,6 @@ class Form(ChakraComponent):
108186
The form component.
109187
"""
110188
...
111-
def get_event_triggers(self) -> Dict[str, Any]: ...
112189

113190
class FormControl(ChakraComponent):
114191
@overload

0 commit comments

Comments
 (0)