form.py 7.4 KB

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