Skip to content

Commit b2523ad

Browse files
authored
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
1 parent 8f9fc28 commit b2523ad

File tree

8 files changed

+144
-24
lines changed

8 files changed

+144
-24
lines changed

pyi_hashes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"reflex/components/el/element.pyi": "06ac2213b062119323291fa66a1ac19e",
3030
"reflex/components/el/elements/__init__.pyi": "280ed457675f3720e34b560a3f617739",
3131
"reflex/components/el/elements/base.pyi": "6e533348b5e1a88cf62fbb5a38dbd795",
32-
"reflex/components/el/elements/forms.pyi": "e05f3ed762ea47f37f32550f8b9105e5",
32+
"reflex/components/el/elements/forms.pyi": "2e7ab39bc7295b8594f38a2aa59c9610",
3333
"reflex/components/el/elements/inline.pyi": "33d9d860e75dd8c4769825127ed363bb",
3434
"reflex/components/el/elements/media.pyi": "addd6872281d65d44a484358b895432f",
3535
"reflex/components/el/elements/metadata.pyi": "974a86d9f0662f6fc15a5bb4b3a87862",

reflex/.templates/web/utils/state.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,15 @@ export const isTrue = (val) => {
948948
return Boolean(val);
949949
};
950950

951+
/***
952+
* Check if a value is not null or undefined.
953+
* @param val The value to check.
954+
* @returns True if the value is not null or undefined, false otherwise.
955+
*/
956+
export const isNotNullOrUndefined = (val) => {
957+
return val ?? undefined !== undefined;
958+
};
959+
951960
/**
952961
* Get the value from a ref.
953962
* @param ref The ref to get the value from.

reflex/components/el/elements/forms.py

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@
1313
from reflex.event import (
1414
EventChain,
1515
EventHandler,
16+
checked_input_event,
17+
float_input_event,
1618
input_event,
19+
int_input_event,
1720
key_event,
1821
prevent_default,
1922
)
2023
from reflex.utils.imports import ImportDict
21-
from reflex.utils.types import is_optional
2224
from reflex.vars import VarData
2325
from reflex.vars.base import LiteralVar, Var
26+
from reflex.vars.number import ternary_operation
2427

2528
from .base import BaseHTML
2629

@@ -294,8 +297,8 @@ def _exclude_props(self) -> list[str]:
294297
]
295298

296299

297-
class Input(BaseHTML):
298-
"""Display the input element."""
300+
class BaseInput(BaseHTML):
301+
"""A base class for input elements."""
299302

300303
tag = "input"
301304

@@ -392,6 +395,42 @@ class Input(BaseHTML):
392395
# Value of the input
393396
value: Var[str | int | float]
394397

398+
# Fired when a key is pressed down
399+
on_key_down: EventHandler[key_event]
400+
401+
# Fired when a key is released
402+
on_key_up: EventHandler[key_event]
403+
404+
405+
class CheckboxInput(BaseInput):
406+
"""Display the input element."""
407+
408+
# Fired when the input value changes
409+
on_change: EventHandler[checked_input_event]
410+
411+
# Fired when the input gains focus
412+
on_focus: EventHandler[checked_input_event]
413+
414+
# Fired when the input loses focus
415+
on_blur: EventHandler[checked_input_event]
416+
417+
418+
class ValueNumberInput(BaseInput):
419+
"""Display the input element."""
420+
421+
# Fired when the input value changes
422+
on_change: EventHandler[float_input_event, int_input_event, input_event]
423+
424+
# Fired when the input gains focus
425+
on_focus: EventHandler[float_input_event, int_input_event, input_event]
426+
427+
# Fired when the input loses focus
428+
on_blur: EventHandler[float_input_event, int_input_event, input_event]
429+
430+
431+
class Input(BaseInput):
432+
"""Display the input element."""
433+
395434
# Fired when the input value changes
396435
on_change: EventHandler[input_event]
397436

@@ -401,12 +440,6 @@ class Input(BaseHTML):
401440
# Fired when the input loses focus
402441
on_blur: EventHandler[input_event]
403442

404-
# Fired when a key is pressed down
405-
on_key_down: EventHandler[key_event]
406-
407-
# Fired when a key is released
408-
on_key_up: EventHandler[key_event]
409-
410443
@classmethod
411444
def create(cls, *children, **props):
412445
"""Create an Input component.
@@ -418,20 +451,25 @@ def create(cls, *children, **props):
418451
Returns:
419452
The component.
420453
"""
421-
from reflex.vars.number import ternary_operation
422-
423454
value = props.get("value")
424455

425456
# React expects an empty string(instead of null) for controlled inputs.
426-
if value is not None and is_optional(
427-
(value_var := Var.create(value))._var_type
428-
):
457+
if value is not None:
458+
value_var = Var.create(value)
429459
props["value"] = ternary_operation(
430-
(value_var != Var.create(None))
431-
& (value_var != Var(_js_expr="undefined")),
432-
value,
433-
Var.create(""),
460+
value_var.is_not_none(), value_var, Var.create("")
434461
)
462+
463+
input_type = props.get("type")
464+
465+
if input_type == "checkbox":
466+
# Checkbox inputs should use the CheckboxInput class
467+
return CheckboxInput.create(*children, **props)
468+
469+
if input_type == "number" or input_type == "range":
470+
# Number inputs should use the ValueNumberInput class
471+
return ValueNumberInput.create(*children, **props)
472+
435473
return super().create(*children, **props)
436474

437475

reflex/event.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ class JavascriptHTMLInputElement:
507507
"""Interface for a Javascript HTMLInputElement https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement."""
508508

509509
value: str = ""
510+
checked: bool = False
510511

511512

512513
@dataclasses.dataclass(
@@ -545,6 +546,42 @@ def input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[str]]:
545546
return (e.target.value,)
546547

547548

549+
def int_input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[int]]:
550+
"""Get the value from an input event as an int.
551+
552+
Args:
553+
e: The input event.
554+
555+
Returns:
556+
The value from the input event as an int.
557+
"""
558+
return (Var("Number").to(FunctionVar).call(e.target.value).to(int),)
559+
560+
561+
def float_input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[float]]:
562+
"""Get the value from an input event as a float.
563+
564+
Args:
565+
e: The input event.
566+
567+
Returns:
568+
The value from the input event as a float.
569+
"""
570+
return (Var("Number").to(FunctionVar).call(e.target.value).to(float),)
571+
572+
573+
def checked_input_event(e: ObjectVar[JavascriptInputEvent]) -> tuple[Var[bool]]:
574+
"""Get the checked state from an input event.
575+
576+
Args:
577+
e: The input event.
578+
579+
Returns:
580+
The checked state from the input event.
581+
"""
582+
return (e.target.checked,)
583+
584+
548585
class KeyInputInfo(TypedDict):
549586
"""Information about a key input event."""
550587

reflex/vars/base.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,9 @@ def to(self, output: Type[str]) -> StringVar: ...
731731
@overload
732732
def to(self, output: Type[bool]) -> BooleanVar: ...
733733

734+
@overload
735+
def to(self, output: type[int]) -> NumberVar[int]: ...
736+
734737
@overload
735738
def to(self, output: type[int] | type[float]) -> NumberVar: ...
736739

@@ -1061,6 +1064,16 @@ def bool(self) -> BooleanVar:
10611064

10621065
return boolify(self)
10631066

1067+
def is_not_none(self) -> BooleanVar:
1068+
"""Check if the var is not None.
1069+
1070+
Returns:
1071+
A BooleanVar object representing the result of the check.
1072+
"""
1073+
from .number import is_not_none_operation
1074+
1075+
return is_not_none_operation(self)
1076+
10641077
def __and__(
10651078
self, other: Var[OTHER_VAR_TYPE] | Any
10661079
) -> Var[VAR_TYPE | OTHER_VAR_TYPE]:

reflex/vars/number.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,10 @@ def create(cls, value: bool, _var_data: VarData | None = None):
10571057
f"$/{Dirs.STATE_PATH}": [ImportVar(tag="isTrue")],
10581058
}
10591059

1060+
_IS_NOT_NULL_OR_UNDEFINED_IMPORT: ImportDict = {
1061+
f"$/{Dirs.STATE_PATH}": [ImportVar(tag="isNotNullOrUndefined")],
1062+
}
1063+
10601064

10611065
@var_operation
10621066
def boolify(value: Var):
@@ -1075,6 +1079,23 @@ def boolify(value: Var):
10751079
)
10761080

10771081

1082+
@var_operation
1083+
def is_not_none_operation(value: Var):
1084+
"""Check if the value is not None.
1085+
1086+
Args:
1087+
value: The value.
1088+
1089+
Returns:
1090+
The boolean value.
1091+
"""
1092+
return var_operation_return(
1093+
js_expression=f"isNotNullOrUndefined({value})",
1094+
var_type=bool,
1095+
var_data=VarData(imports=_IS_NOT_NULL_OR_UNDEFINED_IMPORT),
1096+
)
1097+
1098+
10781099
T = TypeVar("T")
10791100
U = TypeVar("U")
10801101

tests/integration/test_call_script.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class CallScriptState(rx.State):
4646
inline_counter: rx.Field[int] = rx.field(0)
4747
external_counter: rx.Field[int] = rx.field(0)
4848
value: str = "Initial"
49-
last_result: int = 0
49+
last_result: rx.Field[int] = rx.field(0)
5050

5151
@rx.event
5252
def call_script_callback(self, result):
@@ -194,12 +194,12 @@ def reset_(self):
194194
def index():
195195
return rx.vstack(
196196
rx.input(
197-
value=CallScriptState.inline_counter.to(str),
197+
value=CallScriptState.inline_counter.to_string(),
198198
id="inline_counter",
199199
read_only=True,
200200
),
201201
rx.input(
202-
value=CallScriptState.external_counter.to(str),
202+
value=CallScriptState.external_counter.to_string(),
203203
id="external_counter",
204204
read_only=True,
205205
),
@@ -280,7 +280,7 @@ def index():
280280
),
281281
rx.button("Reset", id="reset", on_click=CallScriptState.reset_),
282282
rx.input(
283-
value=CallScriptState.last_result,
283+
value=CallScriptState.last_result.to_string(),
284284
id="last_result",
285285
read_only=True,
286286
on_click=CallScriptState.setvar("last_result", 0),

tests/units/components/core/test_debounce.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ def test_render_child_props():
6060
assert "css" in tag.props and isinstance(tag.props["css"], rx.vars.Var)
6161
for prop in ["foo", "bar", "baz", "quuc"]:
6262
assert prop in str(tag.props["css"])
63-
assert tag.props["value"].equals(LiteralVar.create("real"))
63+
assert tag.props["value"].equals(
64+
rx.cond(Var.create("real").is_not_none(), "real", "")
65+
)
6466
assert len(tag.props["onChange"].events) == 1
6567
assert tag.props["onChange"].events[0].handler == S.on_change
6668
assert tag.contents == ""

0 commit comments

Comments
 (0)