123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728 |
- """Forms classes."""
- from __future__ import annotations
- from hashlib import md5
- from typing import Any, Dict, Iterator, Set, Tuple, Union
- from jinja2 import Environment
- from reflex.components.el.element import Element
- from reflex.components.tags.tag import Tag
- from reflex.constants import Dirs, EventTriggers
- from reflex.event import (
- EventChain,
- EventHandler,
- input_event,
- key_event,
- prevent_default,
- )
- from reflex.utils.imports import ImportDict
- from reflex.utils.types import is_optional
- from reflex.vars import VarData
- from reflex.vars.base import LiteralVar, Var
- from .base import BaseHTML
- FORM_DATA = Var(_js_expr="form_data")
- HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
- """
- const handleSubmit_{{ handle_submit_unique_name }} = useCallback((ev) => {
- const $form = ev.target
- ev.preventDefault()
- const {{ form_data }} = {...Object.fromEntries(new FormData($form).entries()), ...{{ field_ref_mapping }}};
- ({{ on_submit_event_chain }}());
- if ({{ reset_on_submit }}) {
- $form.reset()
- }
- })
- """
- )
- class Button(BaseHTML):
- """Display the button element."""
- tag = "button"
- # Automatically focuses the button when the page loads
- auto_focus: Var[Union[str, int, bool]]
- # Disables the button
- disabled: Var[bool]
- # Associates the button with a form (by id)
- form: Var[Union[str, int, bool]]
- # URL to send the form data to (for type="submit" buttons)
- form_action: Var[Union[str, int, bool]]
- # How the form data should be encoded when submitting to the server (for type="submit" buttons)
- form_enc_type: Var[Union[str, int, bool]]
- # HTTP method to use for sending form data (for type="submit" buttons)
- form_method: Var[Union[str, int, bool]]
- # Bypasses form validation when submitting (for type="submit" buttons)
- form_no_validate: Var[Union[str, int, bool]]
- # Specifies where to display the response after submitting the form (for type="submit" buttons)
- form_target: Var[Union[str, int, bool]]
- # Name of the button, used when sending form data
- name: Var[Union[str, int, bool]]
- # Type of the button (submit, reset, or button)
- type: Var[Union[str, int, bool]]
- # Value of the button, used when sending form data
- value: Var[Union[str, int, bool]]
- class Datalist(BaseHTML):
- """Display the datalist element."""
- tag = "datalist"
- class Fieldset(Element):
- """Display the fieldset element."""
- tag = "fieldset"
- # Disables all the form control descendants of the fieldset
- disabled: Var[Union[str, int, bool]]
- # Associates the fieldset with a form (by id)
- form: Var[Union[str, int, bool]]
- # Name of the fieldset, used for scripting
- name: Var[Union[str, int, bool]]
- def on_submit_event_spec() -> Tuple[Var[dict[str, Any]]]:
- """Event handler spec for the on_submit event.
- Returns:
- The event handler spec.
- """
- return (FORM_DATA,)
- def on_submit_string_event_spec() -> Tuple[Var[dict[str, str]]]:
- """Event handler spec for the on_submit event.
- Returns:
- The event handler spec.
- """
- return (FORM_DATA,)
- class Form(BaseHTML):
- """Display the form element."""
- tag = "form"
- # MIME types the server accepts for file upload
- accept: Var[Union[str, int, bool]]
- # Character encodings to be used for form submission
- accept_charset: Var[Union[str, int, bool]]
- # URL where the form's data should be submitted
- action: Var[Union[str, int, bool]]
- # Whether the form should have autocomplete enabled
- auto_complete: Var[Union[str, int, bool]]
- # Encoding type for the form data when submitted
- enc_type: Var[Union[str, int, bool]]
- # HTTP method to use for form submission
- method: Var[Union[str, int, bool]]
- # Name of the form
- name: Var[Union[str, int, bool]]
- # Indicates that the form should not be validated on submit
- no_validate: Var[Union[str, int, bool]]
- # Where to display the response after submitting the form
- target: Var[Union[str, int, bool]]
- # If true, the form will be cleared after submit.
- reset_on_submit: Var[bool] = Var.create(False)
- # The name used to make this form's submit handler function unique.
- handle_submit_unique_name: Var[str]
- # Fired when the form is submitted
- on_submit: EventHandler[on_submit_event_spec, on_submit_string_event_spec]
- @classmethod
- def create(cls, *children, **props):
- """Create a form component.
- Args:
- *children: The children of the form.
- **props: The properties of the form.
- Returns:
- The form component.
- """
- if "on_submit" not in props:
- props["on_submit"] = prevent_default
- if "handle_submit_unique_name" in props:
- return super().create(*children, **props)
- # Render the form hooks and use the hash of the resulting code to create a unique name.
- props["handle_submit_unique_name"] = ""
- form = super().create(*children, **props)
- form.handle_submit_unique_name = md5(
- str(form._get_all_hooks()).encode("utf-8")
- ).hexdigest()
- return form
- def add_imports(self) -> ImportDict:
- """Add imports needed by the form component.
- Returns:
- The imports for the form component.
- """
- return {
- "react": "useCallback",
- f"$/{Dirs.STATE_PATH}": ["getRefValue", "getRefValues"],
- }
- def add_hooks(self) -> list[str]:
- """Add hooks for the form.
- Returns:
- The hooks for the form.
- """
- if EventTriggers.ON_SUBMIT not in self.event_triggers:
- return []
- return [
- HANDLE_SUBMIT_JS_JINJA2.render(
- handle_submit_unique_name=self.handle_submit_unique_name,
- form_data=FORM_DATA,
- field_ref_mapping=str(LiteralVar.create(self._get_form_refs())),
- on_submit_event_chain=str(
- LiteralVar.create(self.event_triggers[EventTriggers.ON_SUBMIT])
- ),
- reset_on_submit=self.reset_on_submit,
- )
- ]
- def _render(self) -> Tag:
- render_tag = super()._render()
- if EventTriggers.ON_SUBMIT in self.event_triggers:
- render_tag.add_props(
- **{
- EventTriggers.ON_SUBMIT: Var(
- _js_expr=f"handleSubmit_{self.handle_submit_unique_name}",
- _var_type=EventChain,
- )
- }
- )
- return render_tag
- def _get_form_refs(self) -> Dict[str, Any]:
- # Send all the input refs to the handler.
- form_refs = {}
- for ref in self._get_all_refs():
- # when ref start with refs_ it's an array of refs, so we need different method
- # to collect data
- if ref.startswith("refs_"):
- ref_var = Var(_js_expr=ref[:-3])._as_ref()
- form_refs[ref[len("refs_") : -3]] = Var(
- _js_expr=f"getRefValues({ref_var!s})",
- _var_data=VarData.merge(ref_var._get_all_var_data()),
- )
- else:
- ref_var = Var(_js_expr=ref)._as_ref()
- form_refs[ref[4:]] = Var(
- _js_expr=f"getRefValue({ref_var!s})",
- _var_data=VarData.merge(ref_var._get_all_var_data()),
- )
- return form_refs
- def _get_vars(
- self, include_children: bool = True, ignore_ids: set[int] | None = None
- ) -> Iterator[Var]:
- yield from super()._get_vars(
- include_children=include_children, ignore_ids=ignore_ids
- )
- yield from self._get_form_refs().values()
- def _exclude_props(self) -> list[str]:
- return [
- *super()._exclude_props(),
- "reset_on_submit",
- "handle_submit_unique_name",
- ]
- class Input(BaseHTML):
- """Display the input element."""
- tag = "input"
- # Accepted types of files when the input is file type
- accept: Var[Union[str, int, bool]]
- # Alternate text for input type="image"
- alt: Var[Union[str, int, bool]]
- # Whether the input should have autocomplete enabled
- auto_complete: Var[Union[str, int, bool]]
- # Automatically focuses the input when the page loads
- auto_focus: Var[Union[str, int, bool]]
- # Captures media from the user (camera or microphone)
- capture: Var[Union[str, int, bool]]
- # Indicates whether the input is checked (for checkboxes and radio buttons)
- checked: Var[Union[str, int, bool]]
- # The initial value (for checkboxes and radio buttons)
- default_checked: Var[bool]
- # The initial value for a text field
- default_value: Var[str]
- # Name part of the input to submit in 'dir' and 'name' pair when form is submitted
- dirname: Var[Union[str, int, bool]]
- # Disables the input
- disabled: Var[Union[str, int, bool]]
- # Associates the input with a form (by id)
- form: Var[Union[str, int, bool]]
- # URL to send the form data to (for type="submit" buttons)
- form_action: Var[Union[str, int, bool]]
- # How the form data should be encoded when submitting to the server (for type="submit" buttons)
- form_enc_type: Var[Union[str, int, bool]]
- # HTTP method to use for sending form data (for type="submit" buttons)
- form_method: Var[Union[str, int, bool]]
- # Bypasses form validation when submitting (for type="submit" buttons)
- form_no_validate: Var[Union[str, int, bool]]
- # Specifies where to display the response after submitting the form (for type="submit" buttons)
- form_target: Var[Union[str, int, bool]]
- # References a datalist for suggested options
- list: Var[Union[str, int, bool]]
- # Specifies the maximum value for the input
- max: Var[Union[str, int, bool]]
- # Specifies the maximum number of characters allowed in the input
- max_length: Var[Union[str, int, bool]]
- # Specifies the minimum number of characters required in the input
- min_length: Var[Union[str, int, bool]]
- # Specifies the minimum value for the input
- min: Var[Union[str, int, bool]]
- # Indicates whether multiple values can be entered in an input of the type email or file
- multiple: Var[Union[str, int, bool]]
- # Name of the input, used when sending form data
- name: Var[Union[str, int, bool]]
- # Regex pattern the input's value must match to be valid
- pattern: Var[Union[str, int, bool]]
- # Placeholder text in the input
- placeholder: Var[Union[str, int, bool]]
- # Indicates whether the input is read-only
- read_only: Var[Union[str, int, bool]]
- # Indicates that the input is required
- required: Var[Union[str, int, bool]]
- # Specifies the visible width of a text control
- size: Var[Union[str, int, bool]]
- # URL for image inputs
- src: Var[Union[str, int, bool]]
- # Specifies the legal number intervals for an input
- step: Var[Union[str, int, bool]]
- # Specifies the type of input
- type: Var[Union[str, int, bool]]
- # Name of the image map used with the input
- use_map: Var[Union[str, int, bool]]
- # Value of the input
- value: Var[Union[str, int, float]]
- # Fired when the input value changes
- on_change: EventHandler[input_event]
- # Fired when the input gains focus
- on_focus: EventHandler[input_event]
- # Fired when the input loses focus
- on_blur: EventHandler[input_event]
- # Fired when a key is pressed down
- on_key_down: EventHandler[key_event]
- # Fired when a key is released
- on_key_up: EventHandler[key_event]
- @classmethod
- def create(cls, *children, **props):
- """Create an Input component.
- Args:
- *children: The children of the component.
- **props: The properties of the component.
- Returns:
- The component.
- """
- from reflex.vars.number import ternary_operation
- value = props.get("value")
- # React expects an empty string(instead of null) for controlled inputs.
- if value is not None and is_optional(
- (value_var := Var.create(value))._var_type
- ):
- props["value"] = ternary_operation(
- (value_var != Var.create(None)) # pyright: ignore [reportArgumentType]
- & (value_var != Var(_js_expr="undefined")),
- value,
- Var.create(""),
- )
- return super().create(*children, **props)
- class Label(BaseHTML):
- """Display the label element."""
- tag = "label"
- # ID of a form control with which the label is associated
- html_for: Var[Union[str, int, bool]]
- # Associates the label with a form (by id)
- form: Var[Union[str, int, bool]]
- class Legend(BaseHTML):
- """Display the legend element."""
- tag = "legend"
- class Meter(BaseHTML):
- """Display the meter element."""
- tag = "meter"
- # Associates the meter with a form (by id)
- form: Var[Union[str, int, bool]]
- # High limit of range (above this is considered high value)
- high: Var[Union[str, int, bool]]
- # Low limit of range (below this is considered low value)
- low: Var[Union[str, int, bool]]
- # Maximum value of the range
- max: Var[Union[str, int, bool]]
- # Minimum value of the range
- min: Var[Union[str, int, bool]]
- # Optimum value in the range
- optimum: Var[Union[str, int, bool]]
- # Current value of the meter
- value: Var[Union[str, int, bool]]
- class Optgroup(BaseHTML):
- """Display the optgroup element."""
- tag = "optgroup"
- # Disables the optgroup
- disabled: Var[Union[str, int, bool]]
- # Label for the optgroup
- label: Var[Union[str, int, bool]]
- class Option(BaseHTML):
- """Display the option element."""
- tag = "option"
- # Disables the option
- disabled: Var[Union[str, int, bool]]
- # Label for the option, if the text is not the label
- label: Var[Union[str, int, bool]]
- # Indicates that the option is initially selected
- selected: Var[Union[str, int, bool]]
- # Value to be sent as form data
- value: Var[Union[str, int, bool]]
- class Output(BaseHTML):
- """Display the output element."""
- tag = "output"
- # Associates the output with one or more elements (by their IDs)
- html_for: Var[Union[str, int, bool]]
- # Associates the output with a form (by id)
- form: Var[Union[str, int, bool]]
- # Name of the output element for form submission
- name: Var[Union[str, int, bool]]
- class Progress(BaseHTML):
- """Display the progress element."""
- tag = "progress"
- # Associates the progress element with a form (by id)
- form: Var[Union[str, int, bool]]
- # Maximum value of the progress indicator
- max: Var[Union[str, int, bool]]
- # Current value of the progress indicator
- value: Var[Union[str, int, bool]]
- class Select(BaseHTML):
- """Display the select element."""
- tag = "select"
- # Whether the form control should have autocomplete enabled
- auto_complete: Var[Union[str, int, bool]]
- # Automatically focuses the select when the page loads
- auto_focus: Var[Union[str, int, bool]]
- # Disables the select control
- disabled: Var[Union[str, int, bool]]
- # Associates the select with a form (by id)
- form: Var[Union[str, int, bool]]
- # Indicates that multiple options can be selected
- multiple: Var[Union[str, int, bool]]
- # Name of the select, used when submitting the form
- name: Var[Union[str, int, bool]]
- # Indicates that the select control must have a selected option
- required: Var[Union[str, int, bool]]
- # Number of visible options in a drop-down list
- size: Var[Union[str, int, bool]]
- # Fired when the select value changes
- on_change: EventHandler[input_event]
- AUTO_HEIGHT_JS = """
- const autoHeightOnInput = (e, is_enabled) => {
- if (is_enabled) {
- const el = e.target;
- el.style.overflowY = "scroll";
- el.style.height = "auto";
- el.style.height = (e.target.scrollHeight) + "px";
- if (el.form && !el.form.data_resize_on_reset) {
- el.form.addEventListener("reset", () => window.setTimeout(() => autoHeightOnInput(e, is_enabled), 0))
- el.form.data_resize_on_reset = true;
- }
- }
- }
- """
- ENTER_KEY_SUBMIT_JS = """
- const enterKeySubmitOnKeyDown = (e, is_enabled) => {
- if (is_enabled && e.which === 13 && !e.shiftKey) {
- e.preventDefault();
- if (!e.repeat) {
- if (e.target.form) {
- e.target.form.requestSubmit();
- }
- }
- }
- }
- """
- class Textarea(BaseHTML):
- """Display the textarea element."""
- tag = "textarea"
- # Whether the form control should have autocomplete enabled
- auto_complete: Var[Union[str, int, bool]]
- # Automatically focuses the textarea when the page loads
- auto_focus: Var[Union[str, int, bool]]
- # Automatically fit the content height to the text (use min-height with this prop)
- auto_height: Var[bool]
- # Visible width of the text control, in average character widths
- cols: Var[Union[str, int, bool]]
- # The default value of the textarea when initially rendered
- default_value: Var[str]
- # Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted
- dirname: Var[Union[str, int, bool]]
- # Disables the textarea
- disabled: Var[Union[str, int, bool]]
- # Enter key submits form (shift-enter adds new line)
- enter_key_submit: Var[bool]
- # Associates the textarea with a form (by id)
- form: Var[Union[str, int, bool]]
- # Maximum number of characters allowed in the textarea
- max_length: Var[Union[str, int, bool]]
- # Minimum number of characters required in the textarea
- min_length: Var[Union[str, int, bool]]
- # Name of the textarea, used when submitting the form
- name: Var[Union[str, int, bool]]
- # Placeholder text in the textarea
- placeholder: Var[Union[str, int, bool]]
- # Indicates whether the textarea is read-only
- read_only: Var[Union[str, int, bool]]
- # Indicates that the textarea is required
- required: Var[Union[str, int, bool]]
- # Visible number of lines in the text control
- rows: Var[Union[str, int, bool]]
- # The controlled value of the textarea, read only unless used with on_change
- value: Var[Union[str, int, bool]]
- # How the text in the textarea is to be wrapped when submitting the form
- wrap: Var[Union[str, int, bool]]
- # Fired when the input value changes
- on_change: EventHandler[input_event]
- # Fired when the input gains focus
- on_focus: EventHandler[input_event]
- # Fired when the input loses focus
- on_blur: EventHandler[input_event]
- # Fired when a key is pressed down
- on_key_down: EventHandler[key_event]
- # Fired when a key is released
- on_key_up: EventHandler[key_event]
- @classmethod
- def create(cls, *children, **props):
- """Create a textarea component.
- Args:
- *children: The children of the textarea.
- **props: The properties of the textarea.
- Returns:
- The textarea component.
- Raises:
- ValueError: when `enter_key_submit` is combined with `on_key_down`.
- """
- enter_key_submit = props.get("enter_key_submit")
- auto_height = props.get("auto_height")
- custom_attrs = props.setdefault("custom_attrs", {})
- if enter_key_submit is not None:
- enter_key_submit = Var.create(enter_key_submit)
- if "on_key_down" in props:
- raise ValueError(
- "Cannot combine `enter_key_submit` with `on_key_down`.",
- )
- custom_attrs["on_key_down"] = Var(
- _js_expr=f"(e) => enterKeySubmitOnKeyDown(e, {enter_key_submit!s})",
- _var_data=VarData.merge(enter_key_submit._get_all_var_data()),
- )
- if auto_height is not None:
- auto_height = Var.create(auto_height)
- custom_attrs["on_input"] = Var(
- _js_expr=f"(e) => autoHeightOnInput(e, {auto_height!s})",
- _var_data=VarData.merge(auto_height._get_all_var_data()),
- )
- return super().create(*children, **props)
- def _exclude_props(self) -> list[str]:
- return [
- *super()._exclude_props(),
- "auto_height",
- "enter_key_submit",
- ]
- def _get_all_custom_code(self) -> Set[str]:
- """Include the custom code for auto_height and enter_key_submit functionality.
- Returns:
- The custom code for the component.
- """
- custom_code = super()._get_all_custom_code()
- if self.auto_height is not None:
- custom_code.add(AUTO_HEIGHT_JS)
- if self.enter_key_submit is not None:
- custom_code.add(ENTER_KEY_SUBMIT_JS)
- return custom_code
- button = Button.create
- datalist = Datalist.create
- fieldset = Fieldset.create
- form = Form.create
- input = Input.create
- label = Label.create
- legend = Legend.create
- meter = Meter.create
- optgroup = Optgroup.create
- option = Option.create
- output = Output.create
- progress = Progress.create
- select = Select.create
- textarea = Textarea.create
|