From e493bffd5e1813850c6185bb84ef1c286f8c235f Mon Sep 17 00:00:00 2001 From: Johan Lindh Date: Mon, 17 Aug 2026 08:16:52 +0200 Subject: [PATCH 1/2] fix(ui): return JsVar root paths as values --- lib/ui/jsvar.go | 14 +++++++++++++- lib/ui/jsvar_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/ui/jsvar.go b/lib/ui/jsvar.go index 27fe3551..d5c2862b 100644 --- a/lib/ui/jsvar.go +++ b/lib/ui/jsvar.go @@ -293,7 +293,10 @@ type JsVar[T any] struct { dirtyTag any // current dirty tag, set during render; read via JawsGetTag } -// JawsGetPath returns the value at jsPath, logging lookup errors on elem when possible. +// JawsGetPath returns the value at jsPath. +// +// A path containing only empty components returns the same logical root value +// as [JsVar.JawsGet]. Lookup errors are logged on elem when possible. func (jsvar *JsVar[T]) JawsGetPath(elem *jaws.Element, jsPath string) (value any) { value, err := jsvar.getPath(jsPath) if elem != nil { @@ -305,6 +308,15 @@ func (jsvar *JsVar[T]) JawsGetPath(elem *jaws.Element, jsPath string) (value any func (jsvar *JsVar[T]) getPath(jsPath string) (value any, err error) { jsvar.RLock() defer jsvar.RUnlock() + if strings.Trim(jsPath, ".") == "" { + if jsvar.Ptr == nil { + var zero T + value = zero + } else { + value = *jsvar.Ptr + } + return + } return jq.Get(jsvar.Ptr, jsPath) } diff --git a/lib/ui/jsvar_test.go b/lib/ui/jsvar_test.go index 2ff686dc..d7f3f360 100644 --- a/lib/ui/jsvar_test.go +++ b/lib/ui/jsvar_test.go @@ -275,6 +275,44 @@ func TestJsVar_RenderSetAndEvent(t *testing.T) { } } +func TestJsVar_GetPathRootReturnsLogicalValue(t *testing.T) { + value := jsVarNilData{Value: "bound"} + tests := []struct { + name string + ptr *jsVarNilData + }{ + {name: "Bound", ptr: &value}, + {name: "Nil"}, + } + paths := []string{"", ".", "..", "..."} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + jsvar := NewJsVar(new(sync.Mutex), tt.ptr) + want := jsvar.JawsGet(nil) + for _, jsPath := range paths { + t.Run(fmt.Sprintf("path_%q", jsPath), func(t *testing.T) { + gotValue := jsvar.JawsGetPath(nil, jsPath) + got, ok := gotValue.(jsVarNilData) + if !ok { + t.Fatalf("JawsGetPath(%q) returned %T, want jsVarNilData", jsPath, gotValue) + } + if got != want { + t.Fatalf("JawsGetPath(%q) = %#v, want JawsGet result %#v", jsPath, got, want) + } + }) + } + if tt.ptr != nil { + if got := jsvar.JawsGetPath(nil, "..value.."); got != want.Value { + t.Fatalf("nested JawsGetPath = %#v, want %q", got, want.Value) + } + } else if got := jsvar.JawsGetPath(nil, "value"); got != nil { + t.Fatalf("nested JawsGetPath with nil Ptr = %#v, want nil", got) + } + }) + } +} + func TestJsVar_RenderInitialHTMLAttrRunsOutsideBindingLock(t *testing.T) { _, rq := newCoreRequest(t) @@ -381,6 +419,9 @@ func testJsVarGetPathLoggerReentry(t *testing.T, bindingLocker jsVarTryLocker) { if got := jsvar.JawsGetPath(elem, "text"); got != state.Text { t.Fatalf("successful JawsGetPath = %#v, want %q", got, state.Text) } + if got := jsvar.JawsGetPath(elem, "."); got != state { + t.Fatalf("successful root JawsGetPath = %#v, want %#v", got, state) + } testSyncLogger(t, jw) select { case result := <-logger.results: From 28ca8a248b51de94716b32d6b31824203d2942f5 Mon Sep 17 00:00:00 2001 From: Johan Lindh Date: Mon, 17 Aug 2026 08:40:36 +0200 Subject: [PATCH 2/2] refactor(ui): reuse JsVar root getter --- lib/ui/jsvar.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/ui/jsvar.go b/lib/ui/jsvar.go index d5c2862b..0f7a10e3 100644 --- a/lib/ui/jsvar.go +++ b/lib/ui/jsvar.go @@ -298,6 +298,9 @@ type JsVar[T any] struct { // A path containing only empty components returns the same logical root value // as [JsVar.JawsGet]. Lookup errors are logged on elem when possible. func (jsvar *JsVar[T]) JawsGetPath(elem *jaws.Element, jsPath string) (value any) { + if strings.Trim(jsPath, ".") == "" { + return jsvar.JawsGet(elem) + } value, err := jsvar.getPath(jsPath) if elem != nil { _ = elem.Jaws.Log(err) @@ -308,15 +311,6 @@ func (jsvar *JsVar[T]) JawsGetPath(elem *jaws.Element, jsPath string) (value any func (jsvar *JsVar[T]) getPath(jsPath string) (value any, err error) { jsvar.RLock() defer jsvar.RUnlock() - if strings.Trim(jsPath, ".") == "" { - if jsvar.Ptr == nil { - var zero T - value = zero - } else { - value = *jsvar.Ptr - } - return - } return jq.Get(jsvar.Ptr, jsPath) }