form.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. """Form components."""
  2. from __future__ import annotations
  3. from typing import Any, Dict
  4. from jinja2 import Environment
  5. from reflex.components.component import Component
  6. from reflex.components.libs.chakra import ChakraComponent
  7. from reflex.components.tags import Tag
  8. from reflex.constants 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.utils.serializers import serialize
  13. from reflex.vars import BaseVar, Var, get_unique_variable_name
  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 Form(ChakraComponent):
  29. """A form component."""
  30. tag = "Box"
  31. # What the form renders to.
  32. as_: Var[str] = "form" # type: ignore
  33. # If true, the form will be cleared after submit.
  34. reset_on_submit: Var[bool] = False # type: ignore
  35. # The name used to make this form's submit handler function unique
  36. handle_submit_unique_name: Var[str]
  37. @classmethod
  38. def create(cls, *children, **props) -> Component:
  39. """Create a form component.
  40. Args:
  41. *children: The children of the form.
  42. **props: The properties of the form.
  43. Returns:
  44. The form component.
  45. """
  46. if "handle_submit_unique_name" not in props:
  47. props["handle_submit_unique_name"] = get_unique_variable_name()
  48. return super().create(*children, **props)
  49. def _get_imports(self) -> imports.ImportDict:
  50. return imports.merge_imports(
  51. super()._get_imports(),
  52. {"react": {imports.ImportVar(tag="useCallback")}},
  53. )
  54. def _get_hooks(self) -> str | None:
  55. if EventTriggers.ON_SUBMIT not in self.event_triggers:
  56. return
  57. return HANDLE_SUBMIT_JS_JINJA2.render(
  58. handle_submit_unique_name=self.handle_submit_unique_name,
  59. form_data=FORM_DATA,
  60. field_ref_mapping=serialize(self._get_form_refs()),
  61. on_submit_event_chain=format_event_chain(
  62. self.event_triggers[EventTriggers.ON_SUBMIT]
  63. ),
  64. reset_on_submit=self.reset_on_submit,
  65. )
  66. def _render(self) -> Tag:
  67. render_tag = (
  68. super()
  69. ._render()
  70. .remove_props(
  71. "reset_on_submit",
  72. "handle_submit_unique_name",
  73. to_camel_case(EventTriggers.ON_SUBMIT),
  74. )
  75. )
  76. if EventTriggers.ON_SUBMIT in self.event_triggers:
  77. render_tag.add_props(
  78. **{
  79. EventTriggers.ON_SUBMIT: BaseVar(
  80. _var_name=f"handleSubmit{self.handle_submit_unique_name}",
  81. _var_type=EventChain,
  82. )
  83. }
  84. )
  85. return render_tag
  86. def _get_form_refs(self) -> Dict[str, Any]:
  87. # Send all the input refs to the handler.
  88. form_refs = {}
  89. for ref in self.get_refs():
  90. # when ref start with refs_ it's an array of refs, so we need different method
  91. # to collect data
  92. if ref.startswith("refs_"):
  93. form_refs[ref[5:-3]] = Var.create(
  94. f"getRefValues({ref[:-3]})", _var_is_local=False
  95. )
  96. else:
  97. form_refs[ref[4:]] = Var.create(
  98. f"getRefValue({ref})", _var_is_local=False
  99. )
  100. return form_refs
  101. def get_event_triggers(self) -> Dict[str, Any]:
  102. """Get the event triggers that pass the component's value to the handler.
  103. Returns:
  104. A dict mapping the event trigger to the var that is passed to the handler.
  105. """
  106. return {
  107. **super().get_event_triggers(),
  108. EventTriggers.ON_SUBMIT: lambda e0: [FORM_DATA],
  109. }
  110. class FormControl(ChakraComponent):
  111. """Provide context to form components."""
  112. tag = "FormControl"
  113. # If true, the form control will be disabled.
  114. is_disabled: Var[bool]
  115. # If true, the form control will be invalid.
  116. is_invalid: Var[bool]
  117. # If true, the form control will be readonly
  118. is_read_only: Var[bool]
  119. # If true, the form control will be required.
  120. is_required: Var[bool]
  121. # The label text used to inform users as to what information is requested for a text field.
  122. label: Var[str]
  123. @classmethod
  124. def create(
  125. cls,
  126. *children,
  127. label=None,
  128. input=None,
  129. help_text=None,
  130. error_message=None,
  131. **props,
  132. ) -> Component:
  133. """Create a form control component.
  134. Args:
  135. *children: The children of the form control.
  136. label: The label of the form control.
  137. input: The input of the form control.
  138. help_text: The help text of the form control.
  139. error_message: The error message of the form control.
  140. **props: The properties of the form control.
  141. Raises:
  142. AttributeError: raise an error if missing required kwargs.
  143. Returns:
  144. The form control component.
  145. """
  146. if len(children) == 0:
  147. children = []
  148. if label:
  149. children.append(FormLabel.create(*label))
  150. if not input:
  151. raise AttributeError("input keyword argument is required")
  152. children.append(input)
  153. if help_text:
  154. children.append(FormHelperText.create(*help_text))
  155. if error_message:
  156. children.append(FormErrorMessage.create(*error_message))
  157. return super().create(*children, **props)
  158. class FormHelperText(ChakraComponent):
  159. """A form helper text component."""
  160. tag = "FormHelperText"
  161. class FormLabel(ChakraComponent):
  162. """A form label component."""
  163. tag = "FormLabel"
  164. # Link
  165. html_for: Var[str]
  166. class FormErrorMessage(ChakraComponent):
  167. """A form error message component."""
  168. tag = "FormErrorMessage"