Pārlūkot izejas kodu

[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
Masen Furer 1 gadu atpakaļ
vecāks
revīzija
5d647a498f

+ 34 - 7
integration/test_form_submit.py

@@ -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

+ 5 - 137
reflex/components/chakra/forms/form.py

@@ -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()
-        }
-    })
-    """
-)
-
-
-class Form(ChakraComponent):
+from reflex.components.el.elements.forms import Form as HTMLForm
+from reflex.vars import Var
+
+
+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."""

+ 94 - 17
reflex/components/chakra/forms/form.pyi

@@ -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

+ 134 - 3
reflex/components/el/elements/forms.py

@@ -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."""

+ 26 - 10
reflex/components/el/elements/forms.pyi

@@ -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.
         """
         ...
 

+ 4 - 125
reflex/components/radix/primitives/form.py

@@ -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."""

+ 280 - 21
reflex/components/radix/primitives/form.pyi

@@ -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.