Skip to content

Commit

Permalink
[REF-1368] Move common form functionality to rx.el.forms (reflex-dev#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
masenf authored and Malte Klemm committed Mar 12, 2024
1 parent 1b93539 commit f6249e7
Show file tree
Hide file tree
Showing 7 changed files with 577 additions and 320 deletions.
41 changes: 34 additions & 7 deletions integration/test_form_submit.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Integration tests for forms."""
import functools
import time
from typing import Generator

Expand All @@ -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):
Expand All @@ -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")),
Expand Down Expand Up @@ -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):
Expand All @@ -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")),
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
142 changes: 5 additions & 137 deletions reflex/components/chakra/forms/form.py
Original file line number Diff line number Diff line change
@@ -1,152 +1,20 @@
"""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"

# 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."""
Expand Down
111 changes: 94 additions & 17 deletions reflex/components/chakra/forms/form.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -108,7 +186,6 @@ class Form(ChakraComponent):
The form component.
"""
...
def get_event_triggers(self) -> Dict[str, Any]: ...

class FormControl(ChakraComponent):
@overload
Expand Down
Loading

0 comments on commit f6249e7

Please sign in to comment.