diff --git a/integration/test_input.py b/integration/test_input.py index be143502b..7ac9a1e9d 100644 --- a/integration/test_input.py +++ b/integration/test_input.py @@ -20,12 +20,17 @@ def FullyControlledInput(): @app.add_page def index(): - return rx.debounce_input( - rx.input( + return rx.fragment( + rx.debounce_input( + rx.input( + on_change=State.set_text, id="debounce_input_input" # type: ignore + ), value=State.text, - on_change=State.set_text, # type: ignore + debounce_timeout=0, ), - debounce_timeout=0, + rx.input(value=State.text, id="value_input"), + rx.input(on_change=State.set_text, id="on_change_input"), # type: ignore + rx.button("CLEAR", on_click=rx.set_value("on_change_input", "")), ) app.compile() @@ -65,14 +70,19 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness): )[0] # find the input and wait for it to have the initial state value - text_input = driver.find_element(By.TAG_NAME, "input") - assert fully_controlled_input.poll_for_value(text_input) == "initial" + debounce_input = driver.find_element(By.ID, "debounce_input_input") + value_input = driver.find_element(By.ID, "value_input") + on_change_input = driver.find_element(By.ID, "on_change_input") + clear_button = driver.find_element(By.TAG_NAME, "button") + assert fully_controlled_input.poll_for_value(debounce_input) == "initial" + assert fully_controlled_input.poll_for_value(value_input) == "initial" # move cursor to home, then to the right and type characters - text_input.send_keys(Keys.HOME, Keys.ARROW_RIGHT) - text_input.send_keys("foo") - assert text_input.get_attribute("value") == "ifoonitial" + debounce_input.send_keys(Keys.HOME, Keys.ARROW_RIGHT) + debounce_input.send_keys("foo") + assert debounce_input.get_attribute("value") == "ifoonitial" assert backend_state.text == "ifoonitial" + assert fully_controlled_input.poll_for_value(value_input) == "ifoonitial" # clear the input on the backend backend_state.text = "" @@ -80,12 +90,31 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness): await fully_controlled_input.emit_state_updates() assert backend_state.text == "" assert ( - fully_controlled_input.poll_for_value(text_input, exp_not_equal="ifoonitial") + fully_controlled_input.poll_for_value( + debounce_input, exp_not_equal="ifoonitial" + ) == "" ) # type more characters - text_input.send_keys("getting testing done") + debounce_input.send_keys("getting testing done") time.sleep(0.1) - assert text_input.get_attribute("value") == "getting testing done" + assert debounce_input.get_attribute("value") == "getting testing done" assert backend_state.text == "getting testing done" + assert fully_controlled_input.poll_for_value(value_input) == "getting testing done" + + # type into the on_change input + on_change_input.send_keys("overwrite the state") + time.sleep(0.1) + assert debounce_input.get_attribute("value") == "overwrite the state" + assert on_change_input.get_attribute("value") == "overwrite the state" + assert backend_state.text == "overwrite the state" + assert fully_controlled_input.poll_for_value(value_input) == "overwrite the state" + + clear_button.click() + time.sleep(0.1) + assert on_change_input.get_attribute("value") == "" + # potential bug: clearing the on_change field doesn't itself trigger on_change + # assert backend_state.text == "" + # assert debounce_input.get_attribute("value") == "" + # assert value_input.get_attribute("value") == "" diff --git a/reflex/components/forms/debounce.py b/reflex/components/forms/debounce.py index fa96f3ef2..2f7058f26 100644 --- a/reflex/components/forms/debounce.py +++ b/reflex/components/forms/debounce.py @@ -31,6 +31,9 @@ class DebounceInput(Component): # If true, notify when form control loses focus force_notify_on_blur: Var[bool] = True # type: ignore + # If provided, create a fully-controlled input + value: Var[str] + def _render(self) -> Tag: """Carry first child props directly on this tag. diff --git a/reflex/components/forms/input.py b/reflex/components/forms/input.py index 098cd72ab..0fe3d1b50 100644 --- a/reflex/components/forms/input.py +++ b/reflex/components/forms/input.py @@ -2,7 +2,7 @@ from typing import Dict -from reflex.components.component import EVENT_ARG +from reflex.components.component import EVENT_ARG, Component from reflex.components.libs.chakra import ChakraComponent from reflex.utils import imports from reflex.vars import ImportVar, Var @@ -69,6 +69,28 @@ class Input(ChakraComponent): "on_key_up": EVENT_ARG.key, } + @classmethod + def create(cls, *children, **props) -> Component: + """Create an Input component. + + Args: + children: The children of the component. + props: The properties of the component. + + Returns: + The component. + + Raises: + ValueError: If the value is a state Var. + """ + if isinstance(props.get("value"), Var) and props.get("on_change"): + raise ValueError( + "Input value cannot be bound to a state Var with on_change handler.\n" + "Provide value prop to rx.debounce_input with rx.input as a child " + "component to create a fully controlled input." + ) + return super().create(*children, **props) + class InputGroup(ChakraComponent): """The InputGroup component is a component that is used to group a set of inputs.""" diff --git a/reflex/components/forms/textarea.py b/reflex/components/forms/textarea.py index ca9ac80ed..1ab6f8c3b 100644 --- a/reflex/components/forms/textarea.py +++ b/reflex/components/forms/textarea.py @@ -2,7 +2,7 @@ from typing import Dict -from reflex.components.component import EVENT_ARG +from reflex.components.component import EVENT_ARG, Component from reflex.components.libs.chakra import ChakraComponent from reflex.vars import Var @@ -55,3 +55,25 @@ class TextArea(ChakraComponent): "on_key_down": EVENT_ARG.key, "on_key_up": EVENT_ARG.key, } + + @classmethod + def create(cls, *children, **props) -> Component: + """Create an Input component. + + Args: + children: The children of the component. + props: The properties of the component. + + Returns: + The component. + + Raises: + ValueError: If the value is a state Var. + """ + if isinstance(props.get("value"), Var) and props.get("on_change"): + raise ValueError( + "TextArea value cannot be bound to a state Var with on_change handler.\n" + "Provide value prop to rx.debounce_input with rx.text_area as a child " + "component to create a fully controlled input." + ) + return super().create(*children, **props)