form.py 8.5 KB

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