Browse Source

allow int and float typing for input elements behind a warning (#5098)

* allow int and float typing for input elements behind a warning

* does that make a change

* handle number range and checkbox

* fix when literal

* fix the test

* fix the test
Khaleel Al-Adhami 1 month ago
parent
commit
b2523ad9e4

+ 1 - 1
pyi_hashes.json

@@ -29,7 +29,7 @@
   "reflex/components/el/element.pyi": "06ac2213b062119323291fa66a1ac19e",
   "reflex/components/el/elements/__init__.pyi": "280ed457675f3720e34b560a3f617739",
   "reflex/components/el/elements/base.pyi": "6e533348b5e1a88cf62fbb5a38dbd795",
-  "reflex/components/el/elements/forms.pyi": "e05f3ed762ea47f37f32550f8b9105e5",
+  "reflex/components/el/elements/forms.pyi": "2e7ab39bc7295b8594f38a2aa59c9610",
   "reflex/components/el/elements/inline.pyi": "33d9d860e75dd8c4769825127ed363bb",
   "reflex/components/el/elements/media.pyi": "addd6872281d65d44a484358b895432f",
   "reflex/components/el/elements/metadata.pyi": "974a86d9f0662f6fc15a5bb4b3a87862",

+ 9 - 0
reflex/.templates/web/utils/state.js

@@ -948,6 +948,15 @@ export const isTrue = (val) => {
   return Boolean(val);
 };
 
+/***
+ * Check if a value is not null or undefined.
+ * @param val The value to check.
+ * @returns True if the value is not null or undefined, false otherwise.
+ */
+export const isNotNullOrUndefined = (val) => {
+  return val ?? undefined !== undefined;
+};
+
 /**
  * Get the value from a ref.
  * @param ref The ref to get the value from.

+ 56 - 18
reflex/components/el/elements/forms.py

@@ -13,14 +13,17 @@ from reflex.constants import Dirs, EventTriggers
 from reflex.event import (
     EventChain,
     EventHandler,
+    checked_input_event,
+    float_input_event,
     input_event,
+    int_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 reflex.vars.number import ternary_operation
 
 from .base import BaseHTML
 
@@ -294,8 +297,8 @@ HTMLInputTypeAttribute = Literal[
 ]
 
 
-class Input(BaseHTML):
-    """Display the input element."""
+class BaseInput(BaseHTML):
+    """A base class for input elements."""
 
     tag = "input"
 
@@ -392,6 +395,42 @@ class Input(BaseHTML):
     # Value of the input
     value: Var[str | int | float]
 
+    # 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]
+
+
+class CheckboxInput(BaseInput):
+    """Display the input element."""
+
+    # Fired when the input value changes
+    on_change: EventHandler[checked_input_event]
+
+    # Fired when the input gains focus
+    on_focus: EventHandler[checked_input_event]
+
+    # Fired when the input loses focus
+    on_blur: EventHandler[checked_input_event]
+
+
+class ValueNumberInput(BaseInput):
+    """Display the input element."""
+
+    # Fired when the input value changes
+    on_change: EventHandler[float_input_event, int_input_event, input_event]
+
+    # Fired when the input gains focus
+    on_focus: EventHandler[float_input_event, int_input_event, input_event]
+
+    # Fired when the input loses focus
+    on_blur: EventHandler[float_input_event, int_input_event, input_event]
+
+
+class Input(BaseInput):
+    """Display the input element."""
+
     # Fired when the input value changes
     on_change: EventHandler[input_event]
 
@@ -401,12 +440,6 @@ class Input(BaseHTML):
     # 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.
@@ -418,20 +451,25 @@ class Input(BaseHTML):
         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
-        ):
+        if value is not None:
+            value_var = Var.create(value)
             props["value"] = ternary_operation(
-                (value_var != Var.create(None))
-                & (value_var != Var(_js_expr="undefined")),
-                value,
-                Var.create(""),
+                value_var.is_not_none(), value_var, Var.create("")
             )
+
+        input_type = props.get("type")
+
+        if input_type == "checkbox":
+            # Checkbox inputs should use the CheckboxInput class
+            return CheckboxInput.create(*children, **props)
+
+        if input_type == "number" or input_type == "range":
+            # Number inputs should use the ValueNumberInput class
+            return ValueNumberInput.create(*children, **props)
+
         return super().create(*children, **props)
 
 

+ 37 - 0
reflex/event.py

@@ -507,6 +507,7 @@ class JavascriptHTMLInputElement:
     """Interface for a Javascript HTMLInputElement https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement."""
 
     value: str = ""
+    checked: bool = False
 
 
 @dataclasses.dataclass(
@@ -545,6 +546,42 @@ def input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[str]]:
     return (e.target.value,)
 
 
+def int_input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[int]]:
+    """Get the value from an input event as an int.
+
+    Args:
+        e: The input event.
+
+    Returns:
+        The value from the input event as an int.
+    """
+    return (Var("Number").to(FunctionVar).call(e.target.value).to(int),)
+
+
+def float_input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[float]]:
+    """Get the value from an input event as a float.
+
+    Args:
+        e: The input event.
+
+    Returns:
+        The value from the input event as a float.
+    """
+    return (Var("Number").to(FunctionVar).call(e.target.value).to(float),)
+
+
+def checked_input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[bool]]:
+    """Get the checked state from an input event.
+
+    Args:
+        e: The input event.
+
+    Returns:
+        The checked state from the input event.
+    """
+    return (e.target.checked,)
+
+
 class KeyInputInfo(TypedDict):
     """Information about a key input event."""
 

+ 13 - 0
reflex/vars/base.py

@@ -731,6 +731,9 @@ class Var(Generic[VAR_TYPE]):
     @overload
     def to(self, output: Type[bool]) -> BooleanVar: ...
 
+    @overload
+    def to(self, output: type[int]) -> NumberVar[int]: ...
+
     @overload
     def to(self, output: type[int] | type[float]) -> NumberVar: ...
 
@@ -1061,6 +1064,16 @@ class Var(Generic[VAR_TYPE]):
 
         return boolify(self)
 
+    def is_not_none(self) -> BooleanVar:
+        """Check if the var is not None.
+
+        Returns:
+            A BooleanVar object representing the result of the check.
+        """
+        from .number import is_not_none_operation
+
+        return is_not_none_operation(self)
+
     def __and__(
         self, other: Var[OTHER_VAR_TYPE] | Any
     ) -> Var[VAR_TYPE | OTHER_VAR_TYPE]:

+ 21 - 0
reflex/vars/number.py

@@ -1057,6 +1057,10 @@ _IS_TRUE_IMPORT: ImportDict = {
     f"$/{Dirs.STATE_PATH}": [ImportVar(tag="isTrue")],
 }
 
+_IS_NOT_NULL_OR_UNDEFINED_IMPORT: ImportDict = {
+    f"$/{Dirs.STATE_PATH}": [ImportVar(tag="isNotNullOrUndefined")],
+}
+
 
 @var_operation
 def boolify(value: Var):
@@ -1075,6 +1079,23 @@ def boolify(value: Var):
     )
 
 
+@var_operation
+def is_not_none_operation(value: Var):
+    """Check if the value is not None.
+
+    Args:
+        value: The value.
+
+    Returns:
+        The boolean value.
+    """
+    return var_operation_return(
+        js_expression=f"isNotNullOrUndefined({value})",
+        var_type=bool,
+        var_data=VarData(imports=_IS_NOT_NULL_OR_UNDEFINED_IMPORT),
+    )
+
+
 T = TypeVar("T")
 U = TypeVar("U")
 

+ 4 - 4
tests/integration/test_call_script.py

@@ -46,7 +46,7 @@ def CallScript():
         inline_counter: rx.Field[int] = rx.field(0)
         external_counter: rx.Field[int] = rx.field(0)
         value: str = "Initial"
-        last_result: int = 0
+        last_result: rx.Field[int] = rx.field(0)
 
         @rx.event
         def call_script_callback(self, result):
@@ -194,12 +194,12 @@ def CallScript():
     def index():
         return rx.vstack(
             rx.input(
-                value=CallScriptState.inline_counter.to(str),
+                value=CallScriptState.inline_counter.to_string(),
                 id="inline_counter",
                 read_only=True,
             ),
             rx.input(
-                value=CallScriptState.external_counter.to(str),
+                value=CallScriptState.external_counter.to_string(),
                 id="external_counter",
                 read_only=True,
             ),
@@ -280,7 +280,7 @@ def CallScript():
             ),
             rx.button("Reset", id="reset", on_click=CallScriptState.reset_),
             rx.input(
-                value=CallScriptState.last_result,
+                value=CallScriptState.last_result.to_string(),
                 id="last_result",
                 read_only=True,
                 on_click=CallScriptState.setvar("last_result", 0),

+ 3 - 1
tests/units/components/core/test_debounce.py

@@ -60,7 +60,9 @@ def test_render_child_props():
     assert "css" in tag.props and isinstance(tag.props["css"], rx.vars.Var)
     for prop in ["foo", "bar", "baz", "quuc"]:
         assert prop in str(tag.props["css"])
-    assert tag.props["value"].equals(LiteralVar.create("real"))
+    assert tag.props["value"].equals(
+        rx.cond(Var.create("real").is_not_none(), "real", "")
+    )
     assert len(tag.props["onChange"].events) == 1
     assert tag.props["onChange"].events[0].handler == S.on_change
     assert tag.contents == ""