Skip to content

fix(tkbind): follow toolkit's RangeSlider to MVVM-only, via a new BindTwoWay - #9

Merged
tannevaled merged 1 commit into
mainfrom
fix/rangeslider-mvvm-only
Aug 18, 2026
Merged

fix(tkbind): follow toolkit's RangeSlider to MVVM-only, via a new BindTwoWay#9
tannevaled merged 1 commit into
mainfrom
fix/rangeslider-mvvm-only

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

mvvm no longer compiles against toolkit at HEAD. toolkit v0.202.0 made RangeSlider.Low/High Observables rather than settable fields and dropped OnChange (toolkit#283), so tkbind fails to build with seven errors — and any application depending on both mvvm and a current toolkit fails with it.

tkbind/tkbind.go:24:2:  cannot assign to rs.Low (neither addressable nor a map index expression)
tkbind/tkbind.go:26:13: rs.OnChange undefined (type *toolkit.RangeSlider has no field or method OnChange)
… 7 total

The fix is a primitive, not a patch at the call site

BindField's contract is "a value field plus a callback slot" — which a widget owning an Observable has neither of. What it has is a second property that must agree with the ViewModel's, and that is a symmetric link. So the core package gains BindTwoWay, generic and backend-free:

func BindTwoWay[T any](src, dst *Observable[T], invalidate func()) (unbind func())

src is the source of truth at bind time, matching BindField's rule that the ViewModel wins over whatever the widget was constructed with; afterwards neither side is privileged. BindRange becomes two such links, one per handle.

This matters beyond RangeSlider: toolkit is converting widgets to MVVM-only in a broad sweep (20+ merged today), and every one of them will want this shape rather than a bespoke adapter.

One subtlety, pinned by its own test

Low().Set and High().Set do not clamp — only SetRange and the drag/key paths do. Seeding a slider from a ViewModel holding an out-of-range or inverted band would therefore leave it illegal. SetRange is called after the links exist, so the widget's own invariant travels back to the observables and both sides end up holding the same legal band. TestBindRangeNormalisesThroughTheWidget asserts exactly that: (140, −20) on a [0,100] slider ends as (0, 100) on both sides.

Loop-freedom is counted, not inferred

TestBindTwoWayIsLoopFree asserts the number of notifications per Set, because values agreeing proves nothing about how many round trips it took to agree — that check would pass after a thousand hops.

Gate

gofmt clean, go vet clean, 100.0% statement coverage, -race clean, all four packages green.

🤖 Generated with Claude Code

…dTwoWay

mvvm no longer compiles against toolkit at HEAD. toolkit v0.202.0 made
RangeSlider.Low/High Observables rather than settable fields and dropped
OnChange, so tkbind fails to build with seven errors, and any application
depending on both mvvm and a current toolkit fails with it.

The fix is not a patch at the call site but the primitive the new shape asks
for. BindField's contract is "a value field plus a callback slot", which a
widget owning an Observable has neither of; what it has is a second property
that must agree with the ViewModel's. That is a symmetric link, so the core
package gains BindTwoWay — generic, backend-free, and useful to every widget
toolkit's MVVM-only sweep converts next, not to RangeSlider alone.

BindRange becomes two BindTwoWay links, one per handle.

One subtlety is pinned by its own test: Low().Set and High().Set do NOT clamp
(only SetRange and the drag/key paths do), so seeding a slider from a ViewModel
holding an out-of-range or inverted band would leave it illegal. SetRange is
therefore called AFTER the links exist, letting the widget's own invariant
travel back to the observables — both sides end up holding the same legal band.

Loop-freedom is asserted by COUNTING notifications rather than comparing values:
values agreeing proves nothing about how many round trips it took to agree.

100.0% statement coverage, race-clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tannevaled
tannevaled merged commit d548313 into main Aug 18, 2026
1 check passed
@tannevaled
tannevaled deleted the fix/rangeslider-mvvm-only branch August 18, 2026 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant