[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
This commit is contained in:
Masen Furer 2024-03-07 13:17:54 -08:00 committed by GitHub
parent 8903ebb8b0
commit 5d647a498f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 575 additions and 318 deletions

View File

@ -1,4 +1,5 @@
"""Integration tests for forms."""
import functools
import time
from typing import Generator
@ -10,8 +11,12 @@ from reflex.testing import AppHarness
from reflex.utils import format
def FormSubmit():
"""App with a form using on_submit."""
def FormSubmit(form_component):
"""App with a form using on_submit.
Args:
form_component: The str name of the form component to use.
"""
import reflex as rx
class FormState(rx.State):
@ -32,7 +37,7 @@ def FormSubmit():
is_read_only=True,
id="token",
),
rx.form.root(
eval(form_component)(
rx.vstack(
rx.chakra.input(id="name_input"),
rx.hstack(rx.chakra.pin_input(length=4, id="pin_input")),
@ -63,8 +68,12 @@ def FormSubmit():
)
def FormSubmitName():
"""App with a form using on_submit."""
def FormSubmitName(form_component):
"""App with a form using on_submit.
Args:
form_component: The str name of the form component to use.
"""
import reflex as rx
class FormState(rx.State):
@ -85,7 +94,7 @@ def FormSubmitName():
is_read_only=True,
id="token",
),
rx.form.root(
eval(form_component)(
rx.vstack(
rx.chakra.input(name="name_input"),
rx.hstack(rx.chakra.pin_input(length=4, name="pin_input")),
@ -128,7 +137,23 @@ def FormSubmitName():
@pytest.fixture(
scope="session", params=[FormSubmit, FormSubmitName], ids=["id", "name"]
scope="session",
params=[
functools.partial(FormSubmit, form_component="rx.form.root"),
functools.partial(FormSubmitName, form_component="rx.form.root"),
functools.partial(FormSubmit, form_component="rx.el.form"),
functools.partial(FormSubmitName, form_component="rx.el.form"),
functools.partial(FormSubmit, form_component="rx.chakra.form"),
functools.partial(FormSubmitName, form_component="rx.chakra.form"),
],
ids=[
"id-radix",
"name-radix",
"id-html",
"name-html",
"id-chakra",
"name-chakra",
],
)
def form_submit(request, tmp_path_factory) -> Generator[AppHarness, None, None]:
"""Start FormSubmit app at tmp_path via AppHarness.
@ -140,9 +165,11 @@ def form_submit(request, tmp_path_factory) -> Generator[AppHarness, None, None]:
Yields:
running AppHarness instance
"""
param_id = request._pyfuncitem.callspec.id.replace("-", "_")
with AppHarness.create(
root=tmp_path_factory.mktemp("form_submit"),
app_source=request.param, # type: ignore
app_name=request.param.func.__name__ + f"_{param_id}",
) as harness:
assert harness.app_instance is not None, "app is not running"
yield harness

View File

@ -1,39 +1,13 @@
"""Form components."""
from __future__ import annotations
from hashlib import md5
from typing import Any, Dict, Iterator
from jinja2 import Environment
from reflex.components.chakra import ChakraComponent
from reflex.components.component import Component
from reflex.components.tags import Tag
from reflex.constants import Dirs, EventTriggers
from reflex.event import EventChain
from reflex.utils import imports
from reflex.utils.format import format_event_chain, to_camel_case
from reflex.vars import BaseVar, Var
FORM_DATA = Var.create("form_data")
HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
"""
const handleSubmit_{{ handle_submit_unique_name }} = useCallback((ev) => {
const $form = ev.target
ev.preventDefault()
const {{ form_data }} = {...Object.fromEntries(new FormData($form).entries()), ...{{ field_ref_mapping }}}
{{ on_submit_event_chain }}
if ({{ reset_on_submit }}) {
$form.reset()
}
})
"""
)
from reflex.components.el.elements.forms import Form as HTMLForm
from reflex.vars import Var
class Form(ChakraComponent):
class Form(ChakraComponent, HTMLForm):
"""A form component."""
tag = "Box"
@ -41,112 +15,6 @@ class Form(ChakraComponent):
# What the form renders to.
as_: Var[str] = "form" # type: ignore
# If true, the form will be cleared after submit.
reset_on_submit: Var[bool] = False # type: ignore
# The name used to make this form's submit handler function unique
handle_submit_unique_name: Var[str]
@classmethod
def create(cls, *children, **props) -> Component:
"""Create a form component.
Args:
*children: The children of the form.
**props: The properties of the form.
Returns:
The form component.
"""
if "handle_submit_unique_name" in props:
return super().create(*children, **props)
# Render the form hooks and use the hash of the resulting code to create a unique name.
props["handle_submit_unique_name"] = ""
form = super().create(*children, **props)
code_hash = md5(str(form.get_hooks()).encode("utf-8")).hexdigest()
form.handle_submit_unique_name = code_hash
return form
def _get_imports(self) -> imports.ImportDict:
return imports.merge_imports(
super()._get_imports(),
{
"react": {imports.ImportVar(tag="useCallback")},
f"/{Dirs.STATE_PATH}": {
imports.ImportVar(tag="getRefValue"),
imports.ImportVar(tag="getRefValues"),
},
},
)
def _get_hooks(self) -> str | None:
if EventTriggers.ON_SUBMIT not in self.event_triggers:
return
return HANDLE_SUBMIT_JS_JINJA2.render(
handle_submit_unique_name=self.handle_submit_unique_name,
form_data=FORM_DATA,
field_ref_mapping=str(Var.create_safe(self._get_form_refs())),
on_submit_event_chain=format_event_chain(
self.event_triggers[EventTriggers.ON_SUBMIT]
),
reset_on_submit=self.reset_on_submit,
)
def _render(self) -> Tag:
render_tag = (
super()
._render()
.remove_props(
"reset_on_submit",
"handle_submit_unique_name",
to_camel_case(EventTriggers.ON_SUBMIT),
)
)
if EventTriggers.ON_SUBMIT in self.event_triggers:
render_tag.add_props(
**{
EventTriggers.ON_SUBMIT: BaseVar(
_var_name=f"handleSubmit_{self.handle_submit_unique_name}",
_var_type=EventChain,
)
}
)
return render_tag
def _get_form_refs(self) -> Dict[str, Any]:
# Send all the input refs to the handler.
form_refs = {}
for ref in self.get_refs():
# when ref start with refs_ it's an array of refs, so we need different method
# to collect data
if ref.startswith("refs_"):
ref_var = Var.create_safe(ref[:-3]).as_ref()
form_refs[ref[5:-3]] = Var.create_safe(
f"getRefValues({str(ref_var)})", _var_is_local=False
)._replace(merge_var_data=ref_var._var_data)
else:
ref_var = Var.create_safe(ref).as_ref()
form_refs[ref[4:]] = Var.create_safe(
f"getRefValue({str(ref_var)})", _var_is_local=False
)._replace(merge_var_data=ref_var._var_data)
return form_refs
def get_event_triggers(self) -> Dict[str, Any]:
"""Get the event triggers that pass the component's value to the handler.
Returns:
A dict mapping the event trigger to the var that is passed to the handler.
"""
return {
**super().get_event_triggers(),
EventTriggers.ON_SUBMIT: lambda e0: [FORM_DATA],
}
def _get_vars(self) -> Iterator[Var]:
yield from super()._get_vars()
yield from self._get_form_refs().values()
class FormControl(ChakraComponent):
"""Provide context to form components."""

View File

@ -7,32 +7,85 @@ from typing import Any, Dict, Literal, Optional, Union, overload
from reflex.vars import Var, BaseVar, ComputedVar
from reflex.event import EventChain, EventHandler, EventSpec
from reflex.style import Style
from hashlib import md5
from typing import Any, Dict, Iterator
from jinja2 import Environment
from reflex.components.chakra import ChakraComponent
from reflex.components.component import Component
from reflex.components.tags import Tag
from reflex.constants import Dirs, EventTriggers
from reflex.event import EventChain
from reflex.utils import imports
from reflex.utils.format import format_event_chain, to_camel_case
from reflex.vars import BaseVar, Var
from reflex.components.el.elements.forms import Form as HTMLForm
from reflex.vars import Var
FORM_DATA = Var.create("form_data")
HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
"\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 "
)
class Form(ChakraComponent):
class Form(ChakraComponent, HTMLForm):
@overload
@classmethod
def create( # type: ignore
cls,
*children,
as_: Optional[Union[Var[str], str]] = None,
accept: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
accept_charset: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
action: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
auto_complete: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
enc_type: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
method: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
name: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
no_validate: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
target: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
reset_on_submit: Optional[Union[Var[bool], bool]] = None,
handle_submit_unique_name: Optional[Union[Var[str], str]] = None,
access_key: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
auto_capitalize: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
content_editable: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
context_menu: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
dir: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
draggable: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
enter_key_hint: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
hidden: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
input_mode: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
item_prop: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
lang: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
role: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
slot: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
spell_check: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
tab_index: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
title: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
id: Optional[Any] = None,
@ -94,8 +147,33 @@ class Form(ChakraComponent):
Args:
*children: The children of the form.
as_: What the form renders to.
accept: MIME types the server accepts for file upload
accept_charset: Character encodings to be used for form submission
action: URL where the form's data should be submitted
auto_complete: Whether the form should have autocomplete enabled
enc_type: Encoding type for the form data when submitted
method: HTTP method to use for form submission
name: Name of the form
no_validate: Indicates that the form should not be validated on submit
target: Where to display the response after submitting the form
reset_on_submit: If true, the form will be cleared after submit.
handle_submit_unique_name: The name used to make this form's submit handler function unique
handle_submit_unique_name: The name used to make this form's submit handler function unique.
access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
dir: Defines the text direction. Allowed values are ltr (Left-To-Right) or rtl (Right-To-Left)
draggable: Defines whether the element can be dragged.
enter_key_hint: Hints what media types the media element is able to play.
hidden: Defines whether the element is hidden.
input_mode: Defines the type of the element.
item_prop: Defines the name of the element for metadata purposes.
lang: Defines the language used in the element.
role: Defines the role of the element.
slot: Assigns a slot in a shadow DOM shadow tree to an element.
spell_check: Defines whether the element may be checked for spelling errors.
tab_index: Defines the position of the current element in the tabbing order.
title: Defines a tooltip for the element.
style: The style of the component.
key: A unique key for the component.
id: The id for the component.
@ -108,7 +186,6 @@ class Form(ChakraComponent):
The form component.
"""
...
def get_event_triggers(self) -> Dict[str, Any]: ...
class FormControl(ChakraComponent):
@overload

View File

@ -1,12 +1,38 @@
"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py."""
from typing import Any, Dict, Union
from __future__ import annotations
from hashlib import md5
from typing import Any, Dict, Iterator, Union
from jinja2 import Environment
from reflex.components.el.element import Element
from reflex.constants.event import EventTriggers
from reflex.vars import Var
from reflex.components.tags.tag import Tag
from reflex.constants import Dirs, EventTriggers
from reflex.event import EventChain
from reflex.utils import imports
from reflex.utils.format import format_event_chain
from reflex.vars import BaseVar, Var
from .base import BaseHTML
FORM_DATA = Var.create("form_data")
HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
"""
const handleSubmit_{{ handle_submit_unique_name }} = useCallback((ev) => {
const $form = ev.target
ev.preventDefault()
const {{ form_data }} = {...Object.fromEntries(new FormData($form).entries()), ...{{ field_ref_mapping }}}
{{ on_submit_event_chain }}
if ({{ reset_on_submit }}) {
$form.reset()
}
})
"""
)
class Button(BaseHTML):
"""Display the button element."""
@ -101,6 +127,111 @@ class Form(BaseHTML):
# Where to display the response after submitting the form
target: Var[Union[str, int, bool]]
# If true, the form will be cleared after submit.
reset_on_submit: Var[bool] = False # type: ignore
# The name used to make this form's submit handler function unique.
handle_submit_unique_name: Var[str]
def get_event_triggers(self) -> Dict[str, Any]:
"""Event triggers for radix form root.
Returns:
The triggers for event supported by Root.
"""
return {
**super().get_event_triggers(),
EventTriggers.ON_SUBMIT: lambda e0: [FORM_DATA],
}
@classmethod
def create(cls, *children, **props):
"""Create a form component.
Args:
*children: The children of the form.
**props: The properties of the form.
Returns:
The form component.
"""
if "handle_submit_unique_name" in props:
return super().create(*children, **props)
# Render the form hooks and use the hash of the resulting code to create a unique name.
props["handle_submit_unique_name"] = ""
form = super().create(*children, **props)
form.handle_submit_unique_name = md5(
str(form.get_hooks()).encode("utf-8")
).hexdigest()
return form
def _get_imports(self) -> imports.ImportDict:
return imports.merge_imports(
super()._get_imports(),
{
"react": {imports.ImportVar(tag="useCallback")},
f"/{Dirs.STATE_PATH}": {
imports.ImportVar(tag="getRefValue"),
imports.ImportVar(tag="getRefValues"),
},
},
)
def _get_hooks(self) -> str | None:
if EventTriggers.ON_SUBMIT not in self.event_triggers:
return
return HANDLE_SUBMIT_JS_JINJA2.render(
handle_submit_unique_name=self.handle_submit_unique_name,
form_data=FORM_DATA,
field_ref_mapping=str(Var.create_safe(self._get_form_refs())),
on_submit_event_chain=format_event_chain(
self.event_triggers[EventTriggers.ON_SUBMIT]
),
reset_on_submit=self.reset_on_submit,
)
def _render(self) -> Tag:
render_tag = super()._render()
if EventTriggers.ON_SUBMIT in self.event_triggers:
render_tag.add_props(
**{
EventTriggers.ON_SUBMIT: BaseVar(
_var_name=f"handleSubmit_{self.handle_submit_unique_name}",
_var_type=EventChain,
)
}
)
return render_tag
def _get_form_refs(self) -> Dict[str, Any]:
# Send all the input refs to the handler.
form_refs = {}
for ref in self.get_refs():
# when ref start with refs_ it's an array of refs, so we need different method
# to collect data
if ref.startswith("refs_"):
ref_var = Var.create_safe(ref[:-3]).as_ref()
form_refs[ref[5:-3]] = Var.create_safe(
f"getRefValues({str(ref_var)})", _var_is_local=False
)._replace(merge_var_data=ref_var._var_data)
else:
ref_var = Var.create_safe(ref).as_ref()
form_refs[ref[4:]] = Var.create_safe(
f"getRefValue({str(ref_var)})", _var_is_local=False
)._replace(merge_var_data=ref_var._var_data)
return form_refs
def _get_vars(self) -> Iterator[Var]:
yield from super()._get_vars()
yield from self._get_form_refs().values()
def _exclude_props(self) -> list[str]:
return super()._exclude_props() + [
"reset_on_submit",
"handle_submit_unique_name",
]
class Input(BaseHTML):
"""Display the input element."""

View File

@ -7,12 +7,23 @@ from typing import Any, Dict, Literal, Optional, Union, overload
from reflex.vars import Var, BaseVar, ComputedVar
from reflex.event import EventChain, EventHandler, EventSpec
from reflex.style import Style
from typing import Any, Dict, Union
from hashlib import md5
from typing import Any, Dict, Iterator, Union
from jinja2 import Environment
from reflex.components.el.element import Element
from reflex.constants.event import EventTriggers
from reflex.vars import Var
from reflex.components.tags.tag import Tag
from reflex.constants import Dirs, EventTriggers
from reflex.event import EventChain
from reflex.utils import imports
from reflex.utils.format import format_event_chain
from reflex.vars import BaseVar, Var
from .base import BaseHTML
FORM_DATA = Var.create("form_data")
HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
"\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 "
)
class Button(BaseHTML):
@overload
@classmethod
@ -407,6 +418,7 @@ class Fieldset(Element):
...
class Form(BaseHTML):
def get_event_triggers(self) -> Dict[str, Any]: ...
@overload
@classmethod
def create( # type: ignore
@ -437,6 +449,8 @@ class Form(BaseHTML):
target: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
reset_on_submit: Optional[Union[Var[bool], bool]] = None,
handle_submit_unique_name: Optional[Union[Var[str], str]] = None,
access_key: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
@ -525,15 +539,18 @@ class Form(BaseHTML):
on_scroll: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_submit: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
on_unmount: Optional[
Union[EventHandler, EventSpec, list, function, BaseVar]
] = None,
**props
) -> "Form":
"""Create the component.
"""Create a form component.
Args:
*children: The children of the component.
*children: The children of the form.
accept: MIME types the server accepts for file upload
accept_charset: Character encodings to be used for form submission
action: URL where the form's data should be submitted
@ -543,6 +560,8 @@ class Form(BaseHTML):
name: Name of the form
no_validate: Indicates that the form should not be validated on submit
target: Where to display the response after submitting the form
reset_on_submit: If true, the form will be cleared after submit.
handle_submit_unique_name: The name used to make this form's submit handler function unique.
access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable.
@ -565,13 +584,10 @@ class Form(BaseHTML):
class_name: The class name for the component.
autofocus: Whether the component should take the focus once the page is loaded
custom_attrs: custom attribute
**props: The props of the component.
**props: The properties of the form.
Returns:
The component.
Raises:
TypeError: If an invalid child is passed.
The form component.
"""
...

View File

@ -2,40 +2,16 @@
from __future__ import annotations
from hashlib import md5
from typing import Any, Dict, Iterator, Literal
from jinja2 import Environment
from typing import Any, Dict, Literal
from reflex.components.component import Component, ComponentNamespace
from reflex.components.el.elements.forms import Form as HTMLForm
from reflex.components.radix.themes.components.text_field import TextFieldInput
from reflex.components.tags.tag import Tag
from reflex.constants.base import Dirs
from reflex.constants.event import EventTriggers
from reflex.event import EventChain
from reflex.utils import imports
from reflex.utils.format import format_event_chain, to_camel_case
from reflex.vars import BaseVar, Var
from reflex.vars import Var
from .base import RadixPrimitiveComponentWithClassName
FORM_DATA = Var.create("form_data")
HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
"""
const handleSubmit_{{ handle_submit_unique_name }} = useCallback((ev) => {
const $form = ev.target
ev.preventDefault()
const {{ form_data }} = {...Object.fromEntries(new FormData($form).entries()), ...{{ field_ref_mapping }}}
{{ on_submit_event_chain }}
if ({{ reset_on_submit }}) {
$form.reset()
}
})
"""
)
class FormComponent(RadixPrimitiveComponentWithClassName):
"""Base class for all @radix-ui/react-form components."""
@ -43,19 +19,13 @@ class FormComponent(RadixPrimitiveComponentWithClassName):
library = "@radix-ui/react-form@^0.0.3"
class FormRoot(FormComponent):
class FormRoot(FormComponent, HTMLForm):
"""The root component of a radix form."""
tag = "Root"
alias = "RadixFormRoot"
# If true, the form will be cleared after submit.
reset_on_submit: Var[bool] = False # type: ignore
# The name used to make this form's submit handler function unique.
handle_submit_unique_name: Var[str]
def get_event_triggers(self) -> Dict[str, Any]:
"""Event triggers for radix form root.
@ -64,106 +34,15 @@ class FormRoot(FormComponent):
"""
return {
**super().get_event_triggers(),
EventTriggers.ON_SUBMIT: lambda e0: [FORM_DATA],
EventTriggers.ON_CLEAR_SERVER_ERRORS: lambda: [],
}
@classmethod
def create(cls, *children, **props):
"""Create a form component.
Args:
*children: The children of the form.
**props: The properties of the form.
Returns:
The form component.
"""
if "handle_submit_unique_name" in props:
return super().create(*children, **props)
# Render the form hooks and use the hash of the resulting code to create a unique name.
props["handle_submit_unique_name"] = ""
form = super().create(*children, **props)
form.handle_submit_unique_name = md5(
str(form.get_hooks()).encode("utf-8")
).hexdigest()
return form
def _get_imports(self) -> imports.ImportDict:
return imports.merge_imports(
super()._get_imports(),
{
"react": {imports.ImportVar(tag="useCallback")},
f"/{Dirs.STATE_PATH}": {
imports.ImportVar(tag="getRefValue"),
imports.ImportVar(tag="getRefValues"),
},
},
)
def _get_hooks(self) -> str | None:
if EventTriggers.ON_SUBMIT not in self.event_triggers:
return
return HANDLE_SUBMIT_JS_JINJA2.render(
handle_submit_unique_name=self.handle_submit_unique_name,
form_data=FORM_DATA,
field_ref_mapping=str(Var.create_safe(self._get_form_refs())),
on_submit_event_chain=format_event_chain(
self.event_triggers[EventTriggers.ON_SUBMIT]
),
reset_on_submit=self.reset_on_submit,
)
def _render(self) -> Tag:
render_tag = (
super()
._render()
.remove_props(
"reset_on_submit",
"handle_submit_unique_name",
to_camel_case(EventTriggers.ON_SUBMIT),
)
)
if EventTriggers.ON_SUBMIT in self.event_triggers:
render_tag.add_props(
**{
EventTriggers.ON_SUBMIT: BaseVar(
_var_name=f"handleSubmit_{self.handle_submit_unique_name}",
_var_type=EventChain,
)
}
)
return render_tag
def _get_form_refs(self) -> Dict[str, Any]:
# Send all the input refs to the handler.
form_refs = {}
for ref in self.get_refs():
# when ref start with refs_ it's an array of refs, so we need different method
# to collect data
if ref.startswith("refs_"):
ref_var = Var.create_safe(ref[:-3]).as_ref()
form_refs[ref[5:-3]] = Var.create_safe(
f"getRefValues({str(ref_var)})", _var_is_local=False
)._replace(merge_var_data=ref_var._var_data)
else:
ref_var = Var.create_safe(ref).as_ref()
form_refs[ref[4:]] = Var.create_safe(
f"getRefValue({str(ref_var)})", _var_is_local=False
)._replace(merge_var_data=ref_var._var_data)
return form_refs
def _apply_theme(self, theme: Component):
return {
"width": "260px",
**self.style,
}
def _get_vars(self) -> Iterator[Var]:
yield from super()._get_vars()
yield from self._get_form_refs().values()
class FormField(FormComponent):
"""A form field component."""

View File

@ -7,25 +7,14 @@ from typing import Any, Dict, Literal, Optional, Union, overload
from reflex.vars import Var, BaseVar, ComputedVar
from reflex.event import EventChain, EventHandler, EventSpec
from reflex.style import Style
from hashlib import md5
from typing import Any, Dict, Iterator, Literal
from jinja2 import Environment
from typing import Any, Dict, Literal
from reflex.components.component import Component, ComponentNamespace
from reflex.components.el.elements.forms import Form as HTMLForm
from reflex.components.radix.themes.components.text_field import TextFieldInput
from reflex.components.tags.tag import Tag
from reflex.constants.base import Dirs
from reflex.constants.event import EventTriggers
from reflex.event import EventChain
from reflex.utils import imports
from reflex.utils.format import format_event_chain, to_camel_case
from reflex.vars import BaseVar, Var
from reflex.vars import Var
from .base import RadixPrimitiveComponentWithClassName
FORM_DATA = Var.create("form_data")
HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
"\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 "
)
class FormComponent(RadixPrimitiveComponentWithClassName):
@overload
@classmethod
@ -107,16 +96,81 @@ class FormComponent(RadixPrimitiveComponentWithClassName):
"""
...
class FormRoot(FormComponent):
class FormRoot(FormComponent, HTMLForm):
def get_event_triggers(self) -> Dict[str, Any]: ...
@overload
@classmethod
def create( # type: ignore
cls,
*children,
as_child: Optional[Union[Var[bool], bool]] = None,
accept: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
accept_charset: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
action: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
auto_complete: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
enc_type: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
method: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
name: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
no_validate: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
target: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
reset_on_submit: Optional[Union[Var[bool], bool]] = None,
handle_submit_unique_name: Optional[Union[Var[str], str]] = None,
as_child: Optional[Union[Var[bool], bool]] = None,
access_key: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
auto_capitalize: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
content_editable: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
context_menu: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
dir: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
draggable: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
enter_key_hint: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
hidden: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
input_mode: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
item_prop: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
lang: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
role: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
slot: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
spell_check: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
tab_index: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
title: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
id: Optional[Any] = None,
@ -180,9 +234,34 @@ class FormRoot(FormComponent):
Args:
*children: The children of the form.
as_child: Change the default rendered element for the one passed as a child.
accept: MIME types the server accepts for file upload
accept_charset: Character encodings to be used for form submission
action: URL where the form's data should be submitted
auto_complete: Whether the form should have autocomplete enabled
enc_type: Encoding type for the form data when submitted
method: HTTP method to use for form submission
name: Name of the form
no_validate: Indicates that the form should not be validated on submit
target: Where to display the response after submitting the form
reset_on_submit: If true, the form will be cleared after submit.
handle_submit_unique_name: The name used to make this form's submit handler function unique.
as_child: Change the default rendered element for the one passed as a child.
access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
dir: Defines the text direction. Allowed values are ltr (Left-To-Right) or rtl (Right-To-Left)
draggable: Defines whether the element can be dragged.
enter_key_hint: Hints what media types the media element is able to play.
hidden: Defines whether the element is hidden.
input_mode: Defines the type of the element.
item_prop: Defines the name of the element for metadata purposes.
lang: Defines the language used in the element.
role: Defines the role of the element.
slot: Assigns a slot in a shadow DOM shadow tree to an element.
spell_check: Defines whether the element may be checked for spelling errors.
tab_index: Defines the position of the current element in the tabbing order.
title: Defines a tooltip for the element.
style: The style of the component.
key: A unique key for the component.
id: The id for the component.
@ -743,9 +822,74 @@ class Form(FormRoot):
def create( # type: ignore
cls,
*children,
as_child: Optional[Union[Var[bool], bool]] = None,
accept: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
accept_charset: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
action: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
auto_complete: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
enc_type: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
method: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
name: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
no_validate: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
target: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
reset_on_submit: Optional[Union[Var[bool], bool]] = None,
handle_submit_unique_name: Optional[Union[Var[str], str]] = None,
as_child: Optional[Union[Var[bool], bool]] = None,
access_key: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
auto_capitalize: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
content_editable: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
context_menu: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
dir: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
draggable: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
enter_key_hint: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
hidden: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
input_mode: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
item_prop: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
lang: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
role: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
slot: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
spell_check: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
tab_index: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
title: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
id: Optional[Any] = None,
@ -809,9 +953,34 @@ class Form(FormRoot):
Args:
*children: The children of the form.
as_child: Change the default rendered element for the one passed as a child.
accept: MIME types the server accepts for file upload
accept_charset: Character encodings to be used for form submission
action: URL where the form's data should be submitted
auto_complete: Whether the form should have autocomplete enabled
enc_type: Encoding type for the form data when submitted
method: HTTP method to use for form submission
name: Name of the form
no_validate: Indicates that the form should not be validated on submit
target: Where to display the response after submitting the form
reset_on_submit: If true, the form will be cleared after submit.
handle_submit_unique_name: The name used to make this form's submit handler function unique.
as_child: Change the default rendered element for the one passed as a child.
access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
dir: Defines the text direction. Allowed values are ltr (Left-To-Right) or rtl (Right-To-Left)
draggable: Defines whether the element can be dragged.
enter_key_hint: Hints what media types the media element is able to play.
hidden: Defines whether the element is hidden.
input_mode: Defines the type of the element.
item_prop: Defines the name of the element for metadata purposes.
lang: Defines the language used in the element.
role: Defines the role of the element.
slot: Assigns a slot in a shadow DOM shadow tree to an element.
spell_check: Defines whether the element may be checked for spelling errors.
tab_index: Defines the position of the current element in the tabbing order.
title: Defines a tooltip for the element.
style: The style of the component.
key: A unique key for the component.
id: The id for the component.
@ -837,9 +1006,74 @@ class FormNamespace(ComponentNamespace):
@staticmethod
def __call__(
*children,
as_child: Optional[Union[Var[bool], bool]] = None,
accept: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
accept_charset: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
action: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
auto_complete: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
enc_type: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
method: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
name: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
no_validate: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
target: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
reset_on_submit: Optional[Union[Var[bool], bool]] = None,
handle_submit_unique_name: Optional[Union[Var[str], str]] = None,
as_child: Optional[Union[Var[bool], bool]] = None,
access_key: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
auto_capitalize: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
content_editable: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
context_menu: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
dir: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
draggable: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
enter_key_hint: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
hidden: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
input_mode: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
item_prop: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
lang: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
role: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
slot: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
spell_check: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
tab_index: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
title: Optional[
Union[Var[Union[str, int, bool]], Union[str, int, bool]]
] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
id: Optional[Any] = None,
@ -903,9 +1137,34 @@ class FormNamespace(ComponentNamespace):
Args:
*children: The children of the form.
as_child: Change the default rendered element for the one passed as a child.
accept: MIME types the server accepts for file upload
accept_charset: Character encodings to be used for form submission
action: URL where the form's data should be submitted
auto_complete: Whether the form should have autocomplete enabled
enc_type: Encoding type for the form data when submitted
method: HTTP method to use for form submission
name: Name of the form
no_validate: Indicates that the form should not be validated on submit
target: Where to display the response after submitting the form
reset_on_submit: If true, the form will be cleared after submit.
handle_submit_unique_name: The name used to make this form's submit handler function unique.
as_child: Change the default rendered element for the one passed as a child.
access_key: Provides a hint for generating a keyboard shortcut for the current element.
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
content_editable: Indicates whether the element's content is editable.
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
dir: Defines the text direction. Allowed values are ltr (Left-To-Right) or rtl (Right-To-Left)
draggable: Defines whether the element can be dragged.
enter_key_hint: Hints what media types the media element is able to play.
hidden: Defines whether the element is hidden.
input_mode: Defines the type of the element.
item_prop: Defines the name of the element for metadata purposes.
lang: Defines the language used in the element.
role: Defines the role of the element.
slot: Assigns a slot in a shadow DOM shadow tree to an element.
spell_check: Defines whether the element may be checked for spelling errors.
tab_index: Defines the position of the current element in the tabbing order.
title: Defines a tooltip for the element.
style: The style of the component.
key: A unique key for the component.
id: The id for the component.