forms.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. """Forms classes."""
  2. from __future__ import annotations
  3. from collections.abc import Iterator
  4. from hashlib import md5
  5. from typing import Any, ClassVar, Literal
  6. from jinja2 import Environment
  7. from reflex.components.el.element import Element
  8. from reflex.components.tags.tag import Tag
  9. from reflex.constants import Dirs, EventTriggers
  10. from reflex.event import (
  11. EventChain,
  12. EventHandler,
  13. checked_input_event,
  14. float_input_event,
  15. input_event,
  16. int_input_event,
  17. key_event,
  18. prevent_default,
  19. )
  20. from reflex.utils.imports import ImportDict
  21. from reflex.vars import VarData
  22. from reflex.vars.base import LiteralVar, Var
  23. from reflex.vars.number import ternary_operation
  24. from .base import BaseHTML
  25. FORM_DATA = Var(_js_expr="form_data")
  26. HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
  27. """
  28. const handleSubmit_{{ handle_submit_unique_name }} = useCallback((ev) => {
  29. const $form = ev.target
  30. ev.preventDefault()
  31. const {{ form_data }} = {...Object.fromEntries(new FormData($form).entries()), ...{{ field_ref_mapping }}};
  32. ({{ on_submit_event_chain }}(ev));
  33. if ({{ reset_on_submit }}) {
  34. $form.reset()
  35. }
  36. })
  37. """
  38. )
  39. ButtonType = Literal["submit", "reset", "button"]
  40. class Button(BaseHTML):
  41. """Display the button element."""
  42. tag = "button"
  43. # Automatically focuses the button when the page loads
  44. auto_focus: Var[bool]
  45. # Disables the button
  46. disabled: Var[bool]
  47. # Associates the button with a form (by id)
  48. form: Var[str]
  49. # URL to send the form data to (for type="submit" buttons)
  50. form_action: Var[str]
  51. # How the form data should be encoded when submitting to the server (for type="submit" buttons)
  52. form_enc_type: Var[str]
  53. # HTTP method to use for sending form data (for type="submit" buttons)
  54. form_method: Var[str]
  55. # Bypasses form validation when submitting (for type="submit" buttons)
  56. form_no_validate: Var[bool]
  57. # Specifies where to display the response after submitting the form (for type="submit" buttons)
  58. form_target: Var[str]
  59. # Name of the button, used when sending form data
  60. name: Var[str]
  61. # Type of the button (submit, reset, or button)
  62. type: Var[ButtonType]
  63. # Value of the button, used when sending form data
  64. value: Var[str | int | float]
  65. _invalid_children: ClassVar[list[str]] = ["Button"]
  66. class Datalist(BaseHTML):
  67. """Display the datalist element."""
  68. tag = "datalist"
  69. class Fieldset(Element):
  70. """Display the fieldset element."""
  71. tag = "fieldset"
  72. # Disables all the form control descendants of the fieldset
  73. disabled: Var[bool]
  74. # Associates the fieldset with a form (by id)
  75. form: Var[str]
  76. # Name of the fieldset, used for scripting
  77. name: Var[str]
  78. def on_submit_event_spec() -> tuple[Var[dict[str, Any]]]:
  79. """Event handler spec for the on_submit event.
  80. Returns:
  81. The event handler spec.
  82. """
  83. return (FORM_DATA,)
  84. def on_submit_string_event_spec() -> tuple[Var[dict[str, str]]]:
  85. """Event handler spec for the on_submit event.
  86. Returns:
  87. The event handler spec.
  88. """
  89. return (FORM_DATA,)
  90. class Form(BaseHTML):
  91. """Display the form element."""
  92. tag = "form"
  93. # MIME types the server accepts for file upload
  94. accept: Var[str]
  95. # Character encodings to be used for form submission
  96. accept_charset: Var[str]
  97. # URL where the form's data should be submitted
  98. action: Var[str]
  99. # Whether the form should have autocomplete enabled
  100. auto_complete: Var[str]
  101. # Encoding type for the form data when submitted
  102. enc_type: Var[str]
  103. # HTTP method to use for form submission
  104. method: Var[str]
  105. # Name of the form
  106. name: Var[str]
  107. # Indicates that the form should not be validated on submit
  108. no_validate: Var[bool]
  109. # Where to display the response after submitting the form
  110. target: Var[str]
  111. # If true, the form will be cleared after submit.
  112. reset_on_submit: Var[bool] = Var.create(False)
  113. # The name used to make this form's submit handler function unique.
  114. handle_submit_unique_name: Var[str]
  115. # Fired when the form is submitted
  116. on_submit: EventHandler[on_submit_event_spec, on_submit_string_event_spec]
  117. @classmethod
  118. def create(cls, *children, **props):
  119. """Create a form component.
  120. Args:
  121. *children: The children of the form.
  122. **props: The properties of the form.
  123. Returns:
  124. The form component.
  125. """
  126. if "on_submit" not in props:
  127. props["on_submit"] = prevent_default
  128. if "handle_submit_unique_name" in props:
  129. return super().create(*children, **props)
  130. # Render the form hooks and use the hash of the resulting code to create a unique name.
  131. props["handle_submit_unique_name"] = ""
  132. form = super().create(*children, **props)
  133. form.handle_submit_unique_name = md5(
  134. str(form._get_all_hooks()).encode("utf-8")
  135. ).hexdigest()
  136. return form
  137. def add_imports(self) -> ImportDict:
  138. """Add imports needed by the form component.
  139. Returns:
  140. The imports for the form component.
  141. """
  142. return {
  143. "react": "useCallback",
  144. f"$/{Dirs.STATE_PATH}": ["getRefValue", "getRefValues"],
  145. }
  146. def add_hooks(self) -> list[str]:
  147. """Add hooks for the form.
  148. Returns:
  149. The hooks for the form.
  150. """
  151. if EventTriggers.ON_SUBMIT not in self.event_triggers:
  152. return []
  153. return [
  154. HANDLE_SUBMIT_JS_JINJA2.render(
  155. handle_submit_unique_name=self.handle_submit_unique_name,
  156. form_data=FORM_DATA,
  157. field_ref_mapping=str(LiteralVar.create(self._get_form_refs())),
  158. on_submit_event_chain=str(
  159. LiteralVar.create(self.event_triggers[EventTriggers.ON_SUBMIT])
  160. ),
  161. reset_on_submit=self.reset_on_submit,
  162. )
  163. ]
  164. def _render(self) -> Tag:
  165. render_tag = super()._render()
  166. if EventTriggers.ON_SUBMIT in self.event_triggers:
  167. render_tag.add_props(
  168. **{
  169. EventTriggers.ON_SUBMIT: Var(
  170. _js_expr=f"handleSubmit_{self.handle_submit_unique_name}",
  171. _var_type=EventChain,
  172. )
  173. }
  174. )
  175. return render_tag
  176. def _get_form_refs(self) -> dict[str, Any]:
  177. # Send all the input refs to the handler.
  178. form_refs = {}
  179. for ref in self._get_all_refs():
  180. # when ref start with refs_ it's an array of refs, so we need different method
  181. # to collect data
  182. if ref.startswith("refs_"):
  183. ref_var = Var(_js_expr=ref[:-3])._as_ref()
  184. form_refs[ref[len("refs_") : -3]] = Var(
  185. _js_expr=f"getRefValues({ref_var!s})",
  186. _var_data=VarData.merge(ref_var._get_all_var_data()),
  187. )
  188. else:
  189. ref_var = Var(_js_expr=ref)._as_ref()
  190. form_refs[ref[4:]] = Var(
  191. _js_expr=f"getRefValue({ref_var!s})",
  192. _var_data=VarData.merge(ref_var._get_all_var_data()),
  193. )
  194. return form_refs
  195. def _get_vars(
  196. self, include_children: bool = True, ignore_ids: set[int] | None = None
  197. ) -> Iterator[Var]:
  198. yield from super()._get_vars(
  199. include_children=include_children, ignore_ids=ignore_ids
  200. )
  201. yield from self._get_form_refs().values()
  202. def _exclude_props(self) -> list[str]:
  203. return [
  204. *super()._exclude_props(),
  205. "reset_on_submit",
  206. "handle_submit_unique_name",
  207. ]
  208. HTMLInputTypeAttribute = Literal[
  209. "button",
  210. "checkbox",
  211. "color",
  212. "date",
  213. "datetime-local",
  214. "email",
  215. "file",
  216. "hidden",
  217. "image",
  218. "month",
  219. "number",
  220. "password",
  221. "radio",
  222. "range",
  223. "reset",
  224. "search",
  225. "submit",
  226. "tel",
  227. "text",
  228. "time",
  229. "url",
  230. "week",
  231. ]
  232. class BaseInput(BaseHTML):
  233. """A base class for input elements."""
  234. tag = "input"
  235. # Accepted types of files when the input is file type
  236. accept: Var[str]
  237. # Alternate text for input type="image"
  238. alt: Var[str]
  239. # Whether the input should have autocomplete enabled
  240. auto_complete: Var[str]
  241. # Automatically focuses the input when the page loads
  242. auto_focus: Var[bool]
  243. # Captures media from the user (camera or microphone)
  244. capture: Var[Literal[True, False, "user", "environment"]]
  245. # Indicates whether the input is checked (for checkboxes and radio buttons)
  246. checked: Var[bool]
  247. # The initial value (for checkboxes and radio buttons)
  248. default_checked: Var[bool]
  249. # The initial value for a text field
  250. default_value: Var[str | int | float]
  251. # Disables the input
  252. disabled: Var[bool]
  253. # Associates the input with a form (by id)
  254. form: Var[str]
  255. # URL to send the form data to (for type="submit" buttons)
  256. form_action: Var[str]
  257. # How the form data should be encoded when submitting to the server (for type="submit" buttons)
  258. form_enc_type: Var[str]
  259. # HTTP method to use for sending form data (for type="submit" buttons)
  260. form_method: Var[str]
  261. # Bypasses form validation when submitting (for type="submit" buttons)
  262. form_no_validate: Var[bool]
  263. # Specifies where to display the response after submitting the form (for type="submit" buttons)
  264. form_target: Var[str]
  265. # References a datalist for suggested options
  266. list: Var[str]
  267. # Specifies the maximum value for the input
  268. max: Var[str | int | float]
  269. # Specifies the maximum number of characters allowed in the input
  270. max_length: Var[int | float]
  271. # Specifies the minimum number of characters required in the input
  272. min_length: Var[int | float]
  273. # Specifies the minimum value for the input
  274. min: Var[str | int | float]
  275. # Indicates whether multiple values can be entered in an input of the type email or file
  276. multiple: Var[bool]
  277. # Name of the input, used when sending form data
  278. name: Var[str]
  279. # Regex pattern the input's value must match to be valid
  280. pattern: Var[str]
  281. # Placeholder text in the input
  282. placeholder: Var[str]
  283. # Indicates whether the input is read-only
  284. read_only: Var[bool]
  285. # Indicates that the input is required
  286. required: Var[bool]
  287. # Specifies the visible width of a text control
  288. size: Var[int | float]
  289. # URL for image inputs
  290. src: Var[str]
  291. # Specifies the legal number intervals for an input
  292. step: Var[str | int | float]
  293. # Specifies the type of input
  294. type: Var[HTMLInputTypeAttribute]
  295. # Value of the input
  296. value: Var[str | int | float]
  297. # Fired when a key is pressed down
  298. on_key_down: EventHandler[key_event]
  299. # Fired when a key is released
  300. on_key_up: EventHandler[key_event]
  301. class CheckboxInput(BaseInput):
  302. """Display the input element."""
  303. # Fired when the input value changes
  304. on_change: EventHandler[checked_input_event]
  305. # Fired when the input gains focus
  306. on_focus: EventHandler[checked_input_event]
  307. # Fired when the input loses focus
  308. on_blur: EventHandler[checked_input_event]
  309. class ValueNumberInput(BaseInput):
  310. """Display the input element."""
  311. # Fired when the input value changes
  312. on_change: EventHandler[float_input_event, int_input_event, input_event]
  313. # Fired when the input gains focus
  314. on_focus: EventHandler[float_input_event, int_input_event, input_event]
  315. # Fired when the input loses focus
  316. on_blur: EventHandler[float_input_event, int_input_event, input_event]
  317. class Input(BaseInput):
  318. """Display the input element."""
  319. # Fired when the input value changes
  320. on_change: EventHandler[input_event]
  321. # Fired when the input gains focus
  322. on_focus: EventHandler[input_event]
  323. # Fired when the input loses focus
  324. on_blur: EventHandler[input_event]
  325. @classmethod
  326. def create(cls, *children, **props):
  327. """Create an Input component.
  328. Args:
  329. *children: The children of the component.
  330. **props: The properties of the component.
  331. Returns:
  332. The component.
  333. """
  334. value = props.get("value")
  335. # React expects an empty string(instead of null) for controlled inputs.
  336. if value is not None:
  337. value_var = Var.create(value)
  338. props["value"] = ternary_operation(
  339. value_var.is_not_none(), value_var, Var.create("")
  340. )
  341. if cls is Input:
  342. input_type = props.get("type")
  343. if input_type == "checkbox":
  344. # Checkbox inputs should use the CheckboxInput class
  345. return CheckboxInput.create(*children, **props)
  346. if input_type == "number" or input_type == "range":
  347. # Number inputs should use the ValueNumberInput class
  348. return ValueNumberInput.create(*children, **props)
  349. return super().create(*children, **props)
  350. class Label(BaseHTML):
  351. """Display the label element."""
  352. tag = "label"
  353. # ID of a form control with which the label is associated
  354. html_for: Var[str]
  355. # Associates the label with a form (by id)
  356. form: Var[str]
  357. class Legend(BaseHTML):
  358. """Display the legend element."""
  359. tag = "legend"
  360. class Meter(BaseHTML):
  361. """Display the meter element."""
  362. tag = "meter"
  363. # Associates the meter with a form (by id)
  364. form: Var[str]
  365. # High limit of range (above this is considered high value)
  366. high: Var[int | float]
  367. # Low limit of range (below this is considered low value)
  368. low: Var[int | float]
  369. # Maximum value of the range
  370. max: Var[int | float]
  371. # Minimum value of the range
  372. min: Var[int | float]
  373. # Optimum value in the range
  374. optimum: Var[int | float]
  375. # Current value of the meter
  376. value: Var[int | float]
  377. class Optgroup(BaseHTML):
  378. """Display the optgroup element."""
  379. tag = "optgroup"
  380. # Disables the optgroup
  381. disabled: Var[bool]
  382. # Label for the optgroup
  383. label: Var[str]
  384. class Option(BaseHTML):
  385. """Display the option element."""
  386. tag = "option"
  387. # Disables the option
  388. disabled: Var[bool]
  389. # Label for the option, if the text is not the label
  390. label: Var[str]
  391. # Indicates that the option is initially selected
  392. selected: Var[bool]
  393. # Value to be sent as form data
  394. value: Var[str | int | float]
  395. class Output(BaseHTML):
  396. """Display the output element."""
  397. tag = "output"
  398. # Associates the output with one or more elements (by their IDs)
  399. html_for: Var[str]
  400. # Associates the output with a form (by id)
  401. form: Var[str]
  402. # Name of the output element for form submission
  403. name: Var[str]
  404. class Progress(BaseHTML):
  405. """Display the progress element."""
  406. tag = "progress"
  407. # Associates the progress element with a form (by id)
  408. form: Var[str]
  409. # Maximum value of the progress indicator
  410. max: Var[str | int | float]
  411. # Current value of the progress indicator
  412. value: Var[str | int | float]
  413. class Select(BaseHTML):
  414. """Display the select element."""
  415. tag = "select"
  416. # Whether the form control should have autocomplete enabled
  417. auto_complete: Var[str]
  418. # Automatically focuses the select when the page loads
  419. auto_focus: Var[bool]
  420. # Disables the select control
  421. disabled: Var[bool]
  422. # Associates the select with a form (by id)
  423. form: Var[str]
  424. # Indicates that multiple options can be selected
  425. multiple: Var[bool]
  426. # Name of the select, used when submitting the form
  427. name: Var[str]
  428. # Indicates that the select control must have a selected option
  429. required: Var[bool]
  430. # Number of visible options in a drop-down list
  431. size: Var[int]
  432. # Fired when the select value changes
  433. on_change: EventHandler[input_event]
  434. # The controlled value of the select, read only unless used with on_change
  435. value: Var[str]
  436. # The default value of the select when initially rendered
  437. default_value: Var[str]
  438. AUTO_HEIGHT_JS = """
  439. const autoHeightOnInput = (e, is_enabled) => {
  440. if (is_enabled) {
  441. const el = e.target;
  442. el.style.overflowY = "scroll";
  443. el.style.height = "auto";
  444. el.style.height = (e.target.scrollHeight) + "px";
  445. if (el.form && !el.form.data_resize_on_reset) {
  446. el.form.addEventListener("reset", () => window.setTimeout(() => autoHeightOnInput(e, is_enabled), 0))
  447. el.form.data_resize_on_reset = true;
  448. }
  449. }
  450. }
  451. """
  452. ENTER_KEY_SUBMIT_JS = """
  453. const enterKeySubmitOnKeyDown = (e, is_enabled) => {
  454. if (is_enabled && e.which === 13 && !e.shiftKey) {
  455. e.preventDefault();
  456. if (!e.repeat) {
  457. if (e.target.form) {
  458. e.target.form.requestSubmit();
  459. }
  460. }
  461. }
  462. }
  463. """
  464. class Textarea(BaseHTML):
  465. """Display the textarea element."""
  466. tag = "textarea"
  467. # Whether the form control should have autocomplete enabled
  468. auto_complete: Var[str]
  469. # Automatically focuses the textarea when the page loads
  470. auto_focus: Var[bool]
  471. # Automatically fit the content height to the text (use min-height with this prop)
  472. auto_height: Var[bool]
  473. # Visible width of the text control, in average character widths
  474. cols: Var[int]
  475. # The default value of the textarea when initially rendered
  476. default_value: Var[str]
  477. # Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted
  478. dirname: Var[str]
  479. # Disables the textarea
  480. disabled: Var[bool]
  481. # Enter key submits form (shift-enter adds new line)
  482. enter_key_submit: Var[bool]
  483. # Associates the textarea with a form (by id)
  484. form: Var[str]
  485. # Maximum number of characters allowed in the textarea
  486. max_length: Var[int]
  487. # Minimum number of characters required in the textarea
  488. min_length: Var[int]
  489. # Name of the textarea, used when submitting the form
  490. name: Var[str]
  491. # Placeholder text in the textarea
  492. placeholder: Var[str]
  493. # Indicates whether the textarea is read-only
  494. read_only: Var[bool]
  495. # Indicates that the textarea is required
  496. required: Var[bool]
  497. # Visible number of lines in the text control
  498. rows: Var[int]
  499. # The controlled value of the textarea, read only unless used with on_change
  500. value: Var[str]
  501. # How the text in the textarea is to be wrapped when submitting the form
  502. wrap: Var[str]
  503. # Fired when the input value changes
  504. on_change: EventHandler[input_event]
  505. # Fired when the input gains focus
  506. on_focus: EventHandler[input_event]
  507. # Fired when the input loses focus
  508. on_blur: EventHandler[input_event]
  509. # Fired when a key is pressed down
  510. on_key_down: EventHandler[key_event]
  511. # Fired when a key is released
  512. on_key_up: EventHandler[key_event]
  513. @classmethod
  514. def create(cls, *children, **props):
  515. """Create a textarea component.
  516. Args:
  517. *children: The children of the textarea.
  518. **props: The properties of the textarea.
  519. Returns:
  520. The textarea component.
  521. Raises:
  522. ValueError: when `enter_key_submit` is combined with `on_key_down`.
  523. """
  524. enter_key_submit = props.get("enter_key_submit")
  525. auto_height = props.get("auto_height")
  526. custom_attrs = props.setdefault("custom_attrs", {})
  527. if enter_key_submit is not None:
  528. enter_key_submit = Var.create(enter_key_submit)
  529. if "on_key_down" in props:
  530. raise ValueError(
  531. "Cannot combine `enter_key_submit` with `on_key_down`.",
  532. )
  533. custom_attrs["on_key_down"] = Var(
  534. _js_expr=f"(e) => enterKeySubmitOnKeyDown(e, {enter_key_submit!s})",
  535. _var_data=VarData.merge(enter_key_submit._get_all_var_data()),
  536. )
  537. if auto_height is not None:
  538. auto_height = Var.create(auto_height)
  539. custom_attrs["on_input"] = Var(
  540. _js_expr=f"(e) => autoHeightOnInput(e, {auto_height!s})",
  541. _var_data=VarData.merge(auto_height._get_all_var_data()),
  542. )
  543. return super().create(*children, **props)
  544. def _exclude_props(self) -> list[str]:
  545. return [
  546. *super()._exclude_props(),
  547. "auto_height",
  548. "enter_key_submit",
  549. ]
  550. def _get_all_custom_code(self) -> set[str]:
  551. """Include the custom code for auto_height and enter_key_submit functionality.
  552. Returns:
  553. The custom code for the component.
  554. """
  555. custom_code = super()._get_all_custom_code()
  556. if self.auto_height is not None:
  557. custom_code.add(AUTO_HEIGHT_JS)
  558. if self.enter_key_submit is not None:
  559. custom_code.add(ENTER_KEY_SUBMIT_JS)
  560. return custom_code
  561. button = Button.create
  562. datalist = Datalist.create
  563. fieldset = Fieldset.create
  564. form = Form.create
  565. input = Input.create
  566. label = Label.create
  567. legend = Legend.create
  568. meter = Meter.create
  569. optgroup = Optgroup.create
  570. option = Option.create
  571. output = Output.create
  572. progress = Progress.create
  573. select = Select.create
  574. textarea = Textarea.create