form.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. """Radix form component."""
  2. from __future__ import annotations
  3. from hashlib import md5
  4. from types import SimpleNamespace
  5. from typing import Any, Dict, Iterator, Literal
  6. from jinja2 import Environment
  7. from reflex.components.component import Component
  8. from reflex.components.radix.themes.components.textfield import TextFieldInput
  9. from reflex.components.tags.tag import Tag
  10. from reflex.constants.base import Dirs
  11. from reflex.constants.event import EventTriggers
  12. from reflex.event import EventChain
  13. from reflex.utils import imports
  14. from reflex.utils.format import format_event_chain, to_camel_case
  15. from reflex.vars import BaseVar, Var
  16. from .base import RadixPrimitiveComponentWithClassName
  17. FORM_DATA = Var.create("form_data")
  18. HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
  19. """
  20. const handleSubmit_{{ handle_submit_unique_name }} = useCallback((ev) => {
  21. const $form = ev.target
  22. ev.preventDefault()
  23. const {{ form_data }} = {...Object.fromEntries(new FormData($form).entries()), ...{{ field_ref_mapping }}}
  24. {{ on_submit_event_chain }}
  25. if ({{ reset_on_submit }}) {
  26. $form.reset()
  27. }
  28. })
  29. """
  30. )
  31. class FormComponent(RadixPrimitiveComponentWithClassName):
  32. """Base class for all @radix-ui/react-form components."""
  33. library = "@radix-ui/react-form@^0.0.3"
  34. class FormRoot(FormComponent):
  35. """The root component of a radix form."""
  36. tag = "Root"
  37. alias = "RadixFormRoot"
  38. # If true, the form will be cleared after submit.
  39. reset_on_submit: Var[bool] = False # type: ignore
  40. # The name used to make this form's submit handler function unique.
  41. handle_submit_unique_name: Var[str]
  42. def get_event_triggers(self) -> Dict[str, Any]:
  43. """Event triggers for radix form root.
  44. Returns:
  45. The triggers for event supported by Root.
  46. """
  47. return {
  48. **super().get_event_triggers(),
  49. EventTriggers.ON_SUBMIT: lambda e0: [FORM_DATA],
  50. EventTriggers.ON_CLEAR_SERVER_ERRORS: lambda: [],
  51. }
  52. @classmethod
  53. def create(cls, *children, **props):
  54. """Create a form component.
  55. Args:
  56. *children: The children of the form.
  57. **props: The properties of the form.
  58. Returns:
  59. The form component.
  60. """
  61. if "handle_submit_unique_name" in props:
  62. return super().create(*children, **props)
  63. # Render the form hooks and use the hash of the resulting code to create a unique name.
  64. props["handle_submit_unique_name"] = ""
  65. form = super().create(*children, **props)
  66. form.handle_submit_unique_name = md5(
  67. str(form.get_hooks()).encode("utf-8")
  68. ).hexdigest()
  69. return form
  70. def _get_imports(self) -> imports.ImportDict:
  71. return imports.merge_imports(
  72. super()._get_imports(),
  73. {
  74. "react": {imports.ImportVar(tag="useCallback")},
  75. f"/{Dirs.STATE_PATH}": {
  76. imports.ImportVar(tag="getRefValue"),
  77. imports.ImportVar(tag="getRefValues"),
  78. },
  79. },
  80. )
  81. def _get_hooks(self) -> str | None:
  82. if EventTriggers.ON_SUBMIT not in self.event_triggers:
  83. return
  84. return HANDLE_SUBMIT_JS_JINJA2.render(
  85. handle_submit_unique_name=self.handle_submit_unique_name,
  86. form_data=FORM_DATA,
  87. field_ref_mapping=str(Var.create_safe(self._get_form_refs())),
  88. on_submit_event_chain=format_event_chain(
  89. self.event_triggers[EventTriggers.ON_SUBMIT]
  90. ),
  91. reset_on_submit=self.reset_on_submit,
  92. )
  93. def _render(self) -> Tag:
  94. render_tag = (
  95. super()
  96. ._render()
  97. .remove_props(
  98. "reset_on_submit",
  99. "handle_submit_unique_name",
  100. to_camel_case(EventTriggers.ON_SUBMIT),
  101. )
  102. )
  103. if EventTriggers.ON_SUBMIT in self.event_triggers:
  104. render_tag.add_props(
  105. **{
  106. EventTriggers.ON_SUBMIT: BaseVar(
  107. _var_name=f"handleSubmit_{self.handle_submit_unique_name}",
  108. _var_type=EventChain,
  109. )
  110. }
  111. )
  112. return render_tag
  113. def _get_form_refs(self) -> Dict[str, Any]:
  114. # Send all the input refs to the handler.
  115. form_refs = {}
  116. for ref in self.get_refs():
  117. # when ref start with refs_ it's an array of refs, so we need different method
  118. # to collect data
  119. if ref.startswith("refs_"):
  120. ref_var = Var.create_safe(ref[:-3]).as_ref()
  121. form_refs[ref[5:-3]] = Var.create_safe(
  122. f"getRefValues({str(ref_var)})", _var_is_local=False
  123. )._replace(merge_var_data=ref_var._var_data)
  124. else:
  125. ref_var = Var.create_safe(ref).as_ref()
  126. form_refs[ref[4:]] = Var.create_safe(
  127. f"getRefValue({str(ref_var)})", _var_is_local=False
  128. )._replace(merge_var_data=ref_var._var_data)
  129. return form_refs
  130. def _apply_theme(self, theme: Component):
  131. return {
  132. "width": "260px",
  133. **self.style,
  134. }
  135. def _get_vars(self) -> Iterator[Var]:
  136. yield from super()._get_vars()
  137. yield from self._get_form_refs().values()
  138. class FormField(FormComponent):
  139. """A form field component."""
  140. tag = "Field"
  141. alias = "RadixFormField"
  142. # The name of the form field, that is passed down to the control and used to match with validation messages.
  143. name: Var[str]
  144. # Flag to mark the form field as invalid, for server side validation.
  145. server_invalid: Var[bool]
  146. def _apply_theme(self, theme: Component):
  147. return {
  148. "display": "grid",
  149. "margin_bottom": "10px",
  150. **self.style,
  151. }
  152. class FormLabel(FormComponent):
  153. """A form label component."""
  154. tag = "Label"
  155. alias = "RadixFormLabel"
  156. def _apply_theme(self, theme: Component):
  157. return {
  158. "font_size": "15px",
  159. "font_weight": "500",
  160. "line_height": "35px",
  161. **self.style,
  162. }
  163. class FormControl(FormComponent):
  164. """A form control component."""
  165. tag = "Control"
  166. alias = "RadixFormControl"
  167. @classmethod
  168. def create(cls, *children, **props):
  169. """Create a Form Control component.
  170. Args:
  171. *children: The children of the form.
  172. **props: The properties of the form.
  173. Raises:
  174. ValueError: If the number of children is greater than 1.
  175. TypeError: If a child exists but it is not a TextFieldInput.
  176. Returns:
  177. The form control component.
  178. """
  179. if len(children) > 1:
  180. raise ValueError(
  181. f"FormControl can only have at most one child, got {len(children)} children"
  182. )
  183. for child in children:
  184. if not isinstance(child, TextFieldInput):
  185. raise TypeError(
  186. "Only Radix TextFieldInput is allowed as child of FormControl"
  187. )
  188. return super().create(*children, **props)
  189. LiteralMatcher = Literal[
  190. "badInput",
  191. "patternMismatch",
  192. "rangeOverflow",
  193. "rangeUnderflow",
  194. "stepMismatch",
  195. "tooLong",
  196. "tooShort",
  197. "typeMismatch",
  198. "valid",
  199. "valueMissing",
  200. ]
  201. class FormMessage(FormComponent):
  202. """A form message component."""
  203. tag = "Message"
  204. alias = "RadixFormMessage"
  205. # Used to target a specific field by name when rendering outside of a Field part.
  206. name: Var[str]
  207. # Used to indicate on which condition the message should be visible.
  208. match: Var[LiteralMatcher]
  209. # Forces the message to be shown. This is useful when using server-side validation.
  210. force_match: Var[bool]
  211. def _apply_theme(self, theme: Component):
  212. return {
  213. "font_size": "13px",
  214. "opacity": "0.8",
  215. "color": "white",
  216. **self.style,
  217. }
  218. class FormValidityState(FormComponent):
  219. """A form validity state component."""
  220. tag = "ValidityState"
  221. alias = "RadixFormValidityState"
  222. class FormSubmit(FormComponent):
  223. """A form submit component."""
  224. tag = "Submit"
  225. alias = "RadixFormSubmit"
  226. class Form(SimpleNamespace):
  227. """Form components."""
  228. control = staticmethod(FormControl.create)
  229. field = staticmethod(FormField.create)
  230. label = staticmethod(FormLabel.create)
  231. message = staticmethod(FormMessage.create)
  232. root = staticmethod(FormRoot.create)
  233. submit = staticmethod(FormSubmit.create)
  234. validity_state = staticmethod(FormValidityState.create)
  235. form = Form()