Target the right element when a set_focus/set_value id is built from a Var
All Submissions:
- Have you followed the guidelines stated in CONTRIBUTING.md file?
- Have you checked to ensure there aren't any other open Pull Requests for the desired changed?
Type of change
- Bug fix (non-breaking change which fixes an issue)
Changes To Core Features:
- Have you added an explanation of what your changes do and why you'd like us to include them?
- Have you written new tests for your core changes, as applicable?
- Have you successfully ran tests with your changes locally?
Problem
rx.foreach(
S.items,
lambda item: rx.hstack(
rx.input(id=f"rf_{item}"),
rx.button("focus", on_click=rx.set_focus(f"rf_{item}")),
rx.button("fill", on_click=rx.set_value(f"rf_{item}", item)),
),
)
Clicking "focus" or "fill" on the first row acted on the last row's input. rx.set_focus, rx.blur_focus and rx.set_value format the id into a ref name at compile time and drop the Var marker of the f-string, so every row sent ref: "ref_rf_item_rx_state_", a name the last rendered row had registered. Reproduced on main.
Fix
An id built from Vars is only known when the event runs, so it cannot name a ref chosen at compile time.
- Python (
reflex_base/event/__init__.py): for an id that is aVaror an f-string holding one, the three events send the id as it evaluates, in its own payload field (id: ("rf_"+item_rx_state_)). A literal id is unchanged and still sends its registered ref name (ref: "ref_input1"). - Frontend (
utils/state.js):_set_focus,_blur_focusand_set_valueresolverefthrough therefsregistry exactly as onmain, and resolveidwithdocument.getElementByIdonly. The two never cross, so a row id that happens to evaluate toref_alpha, or to a name every object inherits such astoString, cannot be taken for a registered ref.
This is a different route from the one #7838 sketches (registering each row's ref under a key derived from its evaluated id). That needs a hook whose argument reads the loop variable, which is only safe where the hook is guaranteed to compile inside the loop; it is not in an @rx.memo body (#7836) and not for a component the per-item pass leaves unwrapped. Looking the element up by id needs no hook and works in every one of those places. rx.scroll_to already resolves its target with document.getElementById.
As a side effect the three events now accept a plain Var id (rx.set_focus(State.target_id)), which raised a TypeError before.
Not changed
- A component with an f-string id still registers its (shared, unused) static ref; removing it would change what
Formcollects for such controls, so I left it alone. - A backend handler that returns
rx.set_focus(f"rf_{self.selected}")passes a plain string, which is formatted toref_rf_alphaas before and still finds nothing for an input whose id came from a Var. Supporting that needs the raw id in the payload for literal ids too, which changes the payload of every existing use, so I kept it out of this fix. Happy to follow up if you want it.
Tests
tests/units/test_event.py: the exact event the three functions compile to for an f-string id and for a plainVarid.packages/reflex-frontend/tests/state.test.js:reffinds a registered ref by name,idfinds an element by its id (including ids spelledref_namedandtoString), and a missing target still logs the "No element found" error. Fails without thestate.jschange.tests/integration/tests_playwright/test_server_side_event.py: the existing app gets anrx.foreachwhose rows fill and focus their own input. Without the fix the first row's input stays empty.
Ran locally: tests/units (minus reflex_bench, whose browser test cannot start Chromium in my sandbox), npm run check in packages/reflex-frontend (lint, type-check, vitest), the Playwright modules test_server_side_event and test_input, plus ruff check, ruff format and pyright on the changed files.
closes #7838