pininput.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. """A pin input component."""
  2. from __future__ import annotations
  3. from typing import Any, Optional, Union
  4. from reflex.components.chakra import ChakraComponent, LiteralInputVariant
  5. from reflex.components.component import Component
  6. from reflex.components.tags.tag import Tag
  7. from reflex.constants import EventTriggers
  8. from reflex.utils import format
  9. from reflex.utils.imports import ImportDict, merge_imports
  10. from reflex.vars import Var
  11. class PinInput(ChakraComponent):
  12. """The component that provides context to all the pin-input fields."""
  13. tag = "PinInput"
  14. # State var to bind the the input.
  15. value: Var[str]
  16. # If true, the pin input receives focus on mount
  17. auto_focus: Var[bool]
  18. # The default value of the pin input
  19. default_value: Var[str]
  20. # The border color when the input is invalid.
  21. error_border_color: Var[str]
  22. # The border color when the input is focused.
  23. focus_border_color: Var[str]
  24. # The top-level id string that will be applied to the input fields. The index of the input will be appended to this top-level id.
  25. id_: Var[str]
  26. # The length of the number input.
  27. length: Var[int]
  28. # If true, the pin input component is put in the disabled state
  29. is_disabled: Var[bool]
  30. # If true, the pin input component is put in the invalid state
  31. is_invalid: Var[bool]
  32. # If true, focus will move automatically to the next input once filled
  33. manage_focus: Var[bool]
  34. # If true, the input's value will be masked just like `type=password`
  35. mask: Var[bool]
  36. # The placeholder for the pin input
  37. placeholder: Var[str]
  38. # The type of values the pin-input should allow ("number" | "alphanumeric").
  39. type_: Var[str]
  40. # "outline" | "flushed" | "filled" | "unstyled"
  41. variant: Var[LiteralInputVariant]
  42. # The name of the form field
  43. name: Var[str]
  44. def _get_imports(self) -> ImportDict:
  45. """Include PinInputField explicitly because it may not be a child component at compile time.
  46. Returns:
  47. The merged import dict.
  48. """
  49. range_var = Var.range(0)
  50. return merge_imports(
  51. super()._get_imports(),
  52. PinInputField()._get_all_imports(), # type: ignore
  53. range_var._var_data.imports if range_var._var_data is not None else {},
  54. )
  55. def get_event_triggers(self) -> dict[str, Union[Var, Any]]:
  56. """Get the event triggers that pass the component's value to the handler.
  57. Returns:
  58. A dict mapping the event trigger to the var that is passed to the handler.
  59. """
  60. return {
  61. **super().get_event_triggers(),
  62. EventTriggers.ON_CHANGE: lambda e0: [e0],
  63. EventTriggers.ON_COMPLETE: lambda e0: [e0],
  64. }
  65. def get_ref(self) -> str | None:
  66. """Override ref handling to handle array refs.
  67. PinInputFields may be created dynamically, so it's not possible
  68. to compute their ref at compile time, so we return a cheating
  69. guess if the id is specified.
  70. The `ref` for this outer component will always be stripped off, so what
  71. is returned here only matters for form ref collection purposes.
  72. Returns:
  73. None.
  74. """
  75. if any(isinstance(c, PinInputField) for c in self.children):
  76. return None
  77. if self.id:
  78. return format.format_array_ref(self.id, idx=self.length)
  79. return super().get_ref()
  80. def _get_ref_hook(self) -> Optional[str]:
  81. """Override the base _get_ref_hook to handle array refs.
  82. Returns:
  83. The overrided hooks.
  84. """
  85. if self.id:
  86. ref = format.format_array_ref(self.id, None)
  87. refs_declaration = Var.range(self.length).foreach(
  88. lambda: Var.create_safe("useRef(null)", _var_is_string=False),
  89. )
  90. refs_declaration._var_is_local = True
  91. if ref:
  92. return (
  93. f"const {ref} = {str(refs_declaration)}; "
  94. f"{str(Var.create_safe(ref).as_ref())} = {ref}"
  95. )
  96. return super()._get_ref_hook()
  97. def _render(self) -> Tag:
  98. """Override the base _render to remove the fake get_ref.
  99. Returns:
  100. The rendered component.
  101. """
  102. return super()._render().remove_props("ref")
  103. @classmethod
  104. def create(cls, *children, **props) -> Component:
  105. """Create a pin input component.
  106. If no children are passed in, the component will create a default pin input
  107. based on the length prop.
  108. Args:
  109. *children: The children of the component.
  110. **props: The props of the component.
  111. Returns:
  112. The pin input component.
  113. """
  114. if children:
  115. props.pop("length", None)
  116. elif "length" in props:
  117. field_props = {}
  118. if "id" in props:
  119. field_props["id"] = props["id"]
  120. if "name" in props:
  121. field_props["name"] = props["name"]
  122. children = [
  123. PinInputField.for_length(props["length"], **field_props),
  124. ]
  125. return super().create(*children, **props)
  126. class PinInputField(ChakraComponent):
  127. """The text field that user types in - must be a direct child of PinInput."""
  128. tag = "PinInputField"
  129. # the position of the PinInputField inside the PinInput.
  130. # Default to None because it is assigned by PinInput when created.
  131. index: Optional[Var[int]] = None
  132. # The name of the form field
  133. name: Var[str]
  134. @classmethod
  135. def for_length(cls, length: Var | int, **props) -> Var:
  136. """Create a PinInputField for a PinInput with a given length.
  137. Args:
  138. length: The length of the PinInput.
  139. props: The props of each PinInputField (name will become indexed).
  140. Returns:
  141. The PinInputField.
  142. """
  143. name = props.get("name")
  144. def _create(i):
  145. if name is not None:
  146. props["name"] = f"{name}-{i}"
  147. return PinInputField.create(**props, index=i, key=i)
  148. return Var.range(length).foreach(_create) # type: ignore
  149. def _get_ref_hook(self) -> Optional[str]:
  150. return None
  151. def get_ref(self):
  152. """Get the array ref for the pin input.
  153. Returns:
  154. The array ref.
  155. """
  156. if self.id:
  157. return format.format_array_ref(self.id, self.index)