From be4eb9e9cf35c3e62784c4cd2196c0fbf03ca684 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 6 Mar 2024 16:03:20 -0800 Subject: [PATCH 1/3] [REF-1368] Move common form functionality to rx.el.forms Allow plain HTML Form element to have magic on_submit event handler. --- integration/test_form_submit.py | 41 +++++-- reflex/components/el/elements/forms.py | 135 +++++++++++++++++++++++- reflex/components/el/elements/forms.pyi | 36 +++++-- 3 files changed, 192 insertions(+), 20 deletions(-) diff --git a/integration/test_form_submit.py b/integration/test_form_submit.py index ef5631cd7f7..eab0253ca9e 100644 --- a/integration/test_form_submit.py +++ b/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.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 index(): 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 index(): ) -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 index(): 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 index(): @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 diff --git a/reflex/components/el/elements/forms.py b/reflex/components/el/elements/forms.py index bdeff695bff..905030c9f82 100644 --- a/reflex/components/el/elements/forms.py +++ b/reflex/components/el/elements/forms.py @@ -1,12 +1,36 @@ """Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" -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( + """ + 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 +125,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.""" diff --git a/reflex/components/el/elements/forms.pyi b/reflex/components/el/elements/forms.pyi index e179fdb8405..b665dbb3c1c 100644 --- a/reflex/components/el/elements/forms.pyi +++ b/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. """ ... From dec27e14a156e8c882214faba5d34fefcd6c0585 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 6 Mar 2024 16:13:00 -0800 Subject: [PATCH 2/3] 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. --- reflex/components/chakra/forms/form.py | 142 +-------- reflex/components/chakra/forms/form.pyi | 111 ++++++-- reflex/components/radix/primitives/form.py | 129 +-------- reflex/components/radix/primitives/form.pyi | 301 ++++++++++++++++++-- 4 files changed, 383 insertions(+), 300 deletions(-) diff --git a/reflex/components/chakra/forms/form.py b/reflex/components/chakra/forms/form.py index 5d38f66b4b6..f9c9df856b9 100644 --- a/reflex/components/chakra/forms/form.py +++ b/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.""" diff --git a/reflex/components/chakra/forms/form.pyi b/reflex/components/chakra/forms/form.pyi index fd915a7360b..7db40d9e078 100644 --- a/reflex/components/chakra/forms/form.pyi +++ b/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 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 diff --git a/reflex/components/radix/primitives/form.py b/reflex/components/radix/primitives/form.py index d6b57799a89..c11da7b0bf3 100644 --- a/reflex/components/radix/primitives/form.py +++ b/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 @@ def get_event_triggers(self) -> Dict[str, Any]: """ 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.""" diff --git a/reflex/components/radix/primitives/form.pyi b/reflex/components/radix/primitives/form.pyi index e80b4214c99..b14af9ef252 100644 --- a/reflex/components/radix/primitives/form.pyi +++ b/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 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 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 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. From c9e4b25e508ea332ea4071a26448018653fdbcec Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 6 Mar 2024 16:25:26 -0800 Subject: [PATCH 3/3] from __future__ import annotations for py38 --- reflex/components/el/elements/forms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reflex/components/el/elements/forms.py b/reflex/components/el/elements/forms.py index 905030c9f82..26baeacd9c4 100644 --- a/reflex/components/el/elements/forms.py +++ b/reflex/components/el/elements/forms.py @@ -1,4 +1,6 @@ """Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" +from __future__ import annotations + from hashlib import md5 from typing import Any, Dict, Iterator, Union