Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REF-1368] Move common form functionality to rx.el.forms #2801

Merged
merged 3 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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[
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to keep all these other props also, do they work? We could delete them in the base el.Form, I think it may add more noise/clutter than benefit

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The radix form for sure supports all of the native HTML Form props... chakra, i'm less sure about. At any rate, i don't think these will show up in the API docs, but I'll check before merging

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The base form props are not rendering in the API docs. going ahead with the merge.

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
Loading