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