From 4aadd5a94aa3f9c70344255ac4ff7ea9962d881c Mon Sep 17 00:00:00 2001 From: Johan Lindh Date: Fri, 7 Aug 2026 15:32:11 +0200 Subject: [PATCH] fix(ui): keep getter src as initial Img source Emit the getter-backed src before caller and getter attributes so HTML parsing retains the intended initial image. Cover both supported duplicate-attribute paths. Fixes #250 --- lib/ui/html_widgets_test.go | 24 +++++++++++++++++++++++- lib/ui/img.go | 8 +++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/ui/html_widgets_test.go b/lib/ui/html_widgets_test.go index 8726c9fa..4655c52d 100644 --- a/lib/ui/html_widgets_test.go +++ b/lib/ui/html_widgets_test.go @@ -4,6 +4,7 @@ import ( "errors" "html/template" "strings" + "sync" "testing" "github.com/linkdata/jaws" @@ -72,11 +73,32 @@ func TestImg_RenderAndUpdate(t *testing.T) { src := newTestSetter("image.png") ui := NewImg(src) elem, got := renderUI(t, rq, ui, "hidden") - mustMatch(t, `^$`, got) + mustMatch(t, `^$`, got) src.Set("image2.jpg") ui.JawsUpdate(elem) } +func TestImg_RenderGetterSrcTakesPrecedence(t *testing.T) { + _, rq := newCoreRequest(t) + var mu sync.Mutex + src := "getter.png" + getter := bind.New(&mu, &src).InitialHTMLAttr(func(bind.Binder[string], *jaws.Element) (attr template.HTMLAttr) { + attr = `src="hook.png" data-getter="yes"` + return + }) + + _, got := renderUI(t, rq, NewImg(getter), template.HTMLAttr(`src="caller.png" data-caller="yes"`)) + canonical := strings.Index(got, `src="getter.png"`) + caller := strings.Index(got, `src="caller.png"`) + hook := strings.Index(got, `src="hook.png"`) + if canonical < 0 || caller < 0 || hook < 0 || caller < canonical || hook < canonical { + t.Fatalf("getter src does not take precedence: %s", got) + } + if !strings.Contains(got, `data-caller="yes"`) || !strings.Contains(got, `data-getter="yes"`) { + t.Fatalf("rendered image missing non-src attributes: %s", got) + } +} + func TestImg_RenderEscapesSrcAttr(t *testing.T) { _, rq := newCoreRequest(t) src := newTestSetter(`image"©='\` + "\n") diff --git a/lib/ui/img.go b/lib/ui/img.go index 2c085877..9ab4bc04 100644 --- a/lib/ui/img.go +++ b/lib/ui/img.go @@ -2,6 +2,7 @@ package ui import ( "io" + "slices" "github.com/linkdata/jaws" "github.com/linkdata/jaws/lib/bind" @@ -10,6 +11,9 @@ import ( // Img renders an HTML img element whose src is read from a string getter. // +// The getter-derived src takes precedence over any src attribute passed as a +// render param or returned by the getter's [jaws.InitialHTMLAttrHandler]. +// // One Img value may back multiple live [jaws.Element] values. Its getter is // shared by those Elements and must be safe for their render, update and event // calls. @@ -23,7 +27,9 @@ func (u *Img) JawsRender(elem *jaws.Element, w io.Writer, params []any) (err err _, getterAttrs := elem.ApplyGetter(u.Getter) srcAttr := htmlio.Attr("src", u.JawsGet(elem)) attrs := append(elem.ApplyParams(params), getterAttrs...) - attrs = append(attrs, srcAttr) + // HTML parsing keeps the first duplicate attribute, so emit the canonical + // src before caller and getter attributes. + attrs = slices.Insert(attrs, 0, srcAttr) err = htmlio.WriteHTMLInner(w, elem.Jid(), "img", "", "", attrs...) return }