fix(inputs): bind value as the attribute so rebinds survive user edits #6
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/input-value-property"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Root cause of an Orihon bug report: in the page workspace's region editor (one shared form rebound to whichever region is selected), the field the user had just typed in stopped following the selection — click through regions and the touched textarea keeps showing the old region's text; same for the Type select after a by-hand pick.
Mechanism (reproduced in a live browser, DOM vs. render-tree compared):
TextArearendered its value as the element's child text, andSelectmarked selection viaselectedattributes on the options. Both are default-value forms per the HTML spec — once the element's dirty flag is set (real keystrokes in a textarea; a by-hand pick for an option), the browser ignores later updates to them. Blazor's diff was applying the changes; the DOM was discarding them.TextFieldnever had the problem because it bindsvalue="@Value", which Blazor special-cases and writes as the DOM property on every diff.What's in
valueattribute (empty element), the form@binditself compiles to. A comment on the markup names why child text is wrong.<select>carries its ownvalue="@SelectedIndex"(empty string while the placeholder shows). Blazor special-cases select values too, deferring application until the options exist. The per-optionselectedattributes stay for correct first-parse HTML.ValueChangedpaths are byte-identical.Tests
232/232 (+2, 1 adjusted): the adjusted
Renders_label_rows_and_valuenow pins value-as-attribute and empty child text with a comment teaching why; newRebinding_to_another_records_value_replaces_the_attribute(TextArea) andRebinding_to_another_records_value_moves_the_selects_own_value(Select) pin the rebind contract. Note bUnit asserts the render tree, which was always "correct" — the dirty-flag discard only exists in a real DOM, which is why this never showed up in component tests; verified fixed in a live browser against the Orihon page (typed into Notes, clicked the next region, the field followed; bounced back, the saved text returned).Consumers: any bUnit test asserting a
TextArea's content viaTextContentmust switch toGetAttribute("value"). The Orihon submodule-bump PR carries those edits.Independent of PR #5 (different files, no conflicts); merge in either order.
🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagaku.UI - 94.8%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A real live-browser DOM-vs-render-tree bug hunt~ ♡ The dirty-value flag, the default-value forms, the property-vs-attribute distinction — delicious root-cause analysis! And the fix mirrors the sibling that never had the bug. Knowledge excites me, and this PR has understood the problem. Let me look closer~
Verdict: ✅ Looks good to me~
The diagnosis is spot-on and the fix is the minimal correct one. I traced every claim and found nothing wanting.
Why it's right (the short version, since the comments already teach the long one):
TextField.razornever had this bug precisely because it bindsvalue="@Value"on an<input>. Blazor special-casesvalueon form elements — it writes the DOM property (element.value = …) on every diff, which the browser honors even after its dirty flag is set.TextAreawas rendering its value as child text (a default-value form per the HTML spec) andSelectleaned on per-optionselectedattributes (also default-value). Both now carryvalue=and join their sibling. The@oninput/@onchangepaths andInputFieldBase.OnInputAsyncare byte-identical — only the DOM-write mechanism changed. Fufu~ clean~Selectkeeps its per-optionselectedattributes and addsvalue="@(SelectedIndex?.ToString() ?? "")"on the<select>. Belt and suspenders:selectedgives correct first-parse HTML (SSR/no-JS),valuecarries the rebind contract. The?? ""handles the null/placeholder case — when nothing matches,value=""aligns with the disabled placeholder option'svalue="". No drift between the two.Comboboxsibling is unaffected and correctly not touched: its<input value="@_query">already rides the property path, and itsOnParametersSetmirrors_query = SelectedTextwhen closed — it self-heals on rebind by construction.Tests are directional, not tautologies — I checked each assertion does real work:
Rebinding_to_another_records_value_replaces_the_attribute(TextArea): renders "First record.", re-renders with "Second record.", assertsGetAttribute("value")follows. If the attribute binding were still child-text, this fails. ✓Rebinding_to_another_records_value_moves_the_selects_own_value(Select): Pear (index "1") → Plum (index "2"), asserts the select's own value moves. ✓Renders_label_rows_and_valuenow pins bothGetAttribute("value") == "A drifter."andTextContent == ""— proving the value left the child-text position entirely. The comment teaching why bUnit couldn't catch this (render tree vs. real DOM) is exactly the kind of knowledge that belongs in the test. ♡Coverage confirms the new paths fire (local run, CI absent for
d3d12df):TextArea— 100% line / 100% branchSelect\1— 98.5% line / 95.8% branch (the one 50% arm isSelect.razor:84if (Value is null)— **pre-existing**, untouched by this diff; the newvalue=consumer ofSelectedIndexis fully exercised, including the null→""` path via the placeholder test)Build: 0 warnings / 0 errors. Tests: 232/232 pass (matches PR body).
✅ What I liked~
Merge it whenever you like — independently of #5, as noted. ♡
Automated review by Jibril · 2026-07-26
CI/CD: absent for head
d3d12df· Local checks: build 0/0, 232/232 pass, coverage TextArea 100%/100% · Select 98.5%/95.8%