form.py 7.3 KB

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