diff --git a/src/client/packages/idom-client-react/src/components.js b/src/client/packages/idom-client-react/src/components.js index 65e8c8e6c..7ca471c6e 100644 --- a/src/client/packages/idom-client-react/src/components.js +++ b/src/client/packages/idom-client-react/src/components.js @@ -81,7 +81,7 @@ function UserInputElement({ model }) { // order to allow all changes committed by the user to be recorded in the order they // occur. If we don't the user may commit multiple changes before we render next // causing the content of prior changes to be overwritten by subsequent changes. - const value = props.value; + let value = props.value; delete props.value; // Instead of controlling the value, we set it in an effect. @@ -91,6 +91,25 @@ function UserInputElement({ model }) { } }, [ref.current, value]); + // Track a buffer of observed values in order to avoid flicker + const observedValues = React.useState([])[0]; + if (observedValues) { + if (value === observedValues[0]) { + observedValues.shift(); + value = observedValues[observedValues.length - 1]; + } else { + observedValues.length = 0; + } + } + + const givenOnChange = props.onChange; + if (typeof givenOnChange === "function") { + props.onChange = (event) => { + observedValues.push(event.target.value); + givenOnChange(event); + }; + } + // Use createElement here to avoid warning about variable numbers of children not // having keys. Warning about this must now be the responsibility of the server // providing the models instead of the client rendering them. diff --git a/tests/test_client.py b/tests/test_client.py index 017d9abb9..ab0cd7413 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,7 +1,10 @@ +import asyncio +import time from pathlib import Path import idom from idom.testing import ServerMountPoint +from tests.driver_utils import send_keys JS_DIR = Path(__file__).parent / "js" @@ -71,14 +74,44 @@ def ButtonWithChangingColor(): button = driver.find_element("id", "my-button") - assert get_style(button)["background-color"] == "red" + assert _get_style(button)["background-color"] == "red" for color in ["blue", "red"] * 2: button.click() - driver_wait.until(lambda _: get_style(button)["background-color"] == color) + driver_wait.until(lambda _: _get_style(button)["background-color"] == color) -def get_style(element): +def _get_style(element): items = element.get_attribute("style").split(";") pairs = [item.split(":", 1) for item in map(str.strip, items) if item] return {key.strip(): value.strip() for key, value in pairs} + + +def test_slow_server_response_on_input_change(display, driver, driver_wait): + """A delay server-side could cause input values to be overwritten. + + For more info see: https://github.com/idom-team/idom/issues/684 + """ + + delay = 0.2 + + @idom.component + def SomeComponent(): + value, set_value = idom.hooks.use_state("") + + async def handle_change(event): + await asyncio.sleep(delay) + set_value(event["target"]["value"]) + + return idom.html.input({"onChange": handle_change, "id": "test-input"}) + + display(SomeComponent) + + inp = driver.find_element("id", "test-input") + + text = "hello" + send_keys(inp, text) + + time.sleep(delay * len(text) * 1.1) + + driver_wait.until(lambda _: inp.get_attribute("value") == "hello")