123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- """Radix form component."""
- from __future__ import annotations
- from hashlib import md5
- from typing import Any, Dict, Iterator, Literal
- from jinja2 import Environment
- from reflex.components.component import Component
- from reflex.components.radix.themes.components.textfield 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 .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."""
- library = "@radix-ui/react-form@^0.0.3"
- class FormRoot(FormComponent):
- """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.
- Returns:
- The triggers for event supported by Root.
- """
- 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 | None):
- 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."""
- tag = "Field"
- alias = "RadixFormField"
- # The name of the form field, that is passed down to the control and used to match with validation messages.
- name: Var[str]
- # Flag to mark the form field as invalid, for server side validation.
- server_invalid: Var[bool]
- def _apply_theme(self, theme: Component | None):
- return {
- "display": "grid",
- "margin_bottom": "10px",
- **self.style,
- }
- class FormLabel(FormComponent):
- """A form label component."""
- tag = "Label"
- alias = "RadixFormLabel"
- def _apply_theme(self, theme: Component | None):
- return {
- "font_size": "15px",
- "font_weight": "500",
- "line_height": "35px",
- **self.style,
- }
- class FormControl(FormComponent):
- """A form control component."""
- tag = "Control"
- alias = "RadixFormControl"
- @classmethod
- def create(cls, *children, **props):
- """Create a Form Control component.
- Args:
- *children: The children of the form.
- **props: The properties of the form.
- Raises:
- ValueError: If the number of children is greater than 1.
- TypeError: If a child exists but it is not a TextFieldInput.
- Returns:
- The form control component.
- """
- if len(children) > 1:
- raise ValueError(
- f"FormControl can only have at most one child, got {len(children)} children"
- )
- for child in children:
- if not isinstance(child, TextFieldInput):
- raise TypeError(
- "Only Radix TextFieldInput is allowed as child of FormControl"
- )
- return super().create(*children, **props)
- LiteralMatcher = Literal[
- "badInput",
- "patternMismatch",
- "rangeOverflow",
- "rangeUnderflow",
- "stepMismatch",
- "tooLong",
- "tooShort",
- "typeMismatch",
- "valid",
- "valueMissing",
- ]
- class FormMessage(FormComponent):
- """A form message component."""
- tag = "Message"
- alias = "RadixFormMessage"
- # Used to target a specific field by name when rendering outside of a Field part.
- name: Var[str]
- # Used to indicate on which condition the message should be visible.
- match: Var[LiteralMatcher]
- # Forces the message to be shown. This is useful when using server-side validation.
- force_match: Var[bool]
- def _apply_theme(self, theme: Component | None):
- return {
- "font_size": "13px",
- "opacity": "0.8",
- "color": "white",
- **self.style,
- }
- class FormValidityState(FormComponent):
- """A form validity state component."""
- tag = "ValidityState"
- alias = "RadixFormValidityState"
- class FormSubmit(FormComponent):
- """A form submit component."""
- tag = "Submit"
- alias = "RadixFormSubmit"
- form_root = FormRoot.create
- form_field = FormField.create
- form_label = FormLabel.create
- form_control = FormControl.create
- form_message = FormMessage.create
- form_validity_state = FormValidityState.create
- form_submit = FormSubmit.create
|