Skip to content

Commit 760bd8a

Browse files
committed
Render UI templates through wrapper divs
1 parent 1b33ac1 commit 760bd8a

10 files changed

Lines changed: 274 additions & 344 deletions

File tree

.agents/skills/jaws/SKILL.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ These are the two usual building blocks for widget handlers passed to `$.Button`
7979
## Template-dot and tag rules
8080

8181
- `ui.Template` expands `Dot` into tags via `tag.TagExpand` (package `github.com/linkdata/jaws/lib/tag`, imported as `tag`); the root dot is part of identity/tag behavior.
82+
- `ui.Template` is for partial templates only; full document/page templates should be rendered through `ui.Handler`.
8283
- Prefer comparable root dots (pointers or small comparable structs).
8384
- If root dot is non-comparable, implement `JawsGetTag(tag.Context) any` and return a comparable tag.
8485
- Do not use plain `string`, numeric, `bool`, `template.HTML`, or `template.HTMLAttr` as tags; `tag.TagExpand` rejects them.
@@ -94,7 +95,8 @@ JaWS parses template params as:
9495
Implications:
9596
- Non-comparable handlers are not auto-tagged unless they implement `tag.TagGetter`.
9697
- Pass explicit tags when dirty targeting depends on them.
97-
- Include wrapper markup attributes via `{{$.Attrs}}`.
98+
- HTML attributes passed to `$.Template(...)` are applied to the generated template wrapper.
99+
- Template bodies used with `$.Template(...)` must be partials, not full documents.
98100
- For dynamic button text, avoid passing plain static strings if the value must change after render; use getter-based values so updates reflect new state.
99101

100102
## Event handling model
@@ -110,13 +112,14 @@ The handler candidate is asked via `JawsClick` / `JawsContextMenu` / `JawsInput`
110112
For clickable content rendering:
111113
- Prefer a template dot with `JawsClick` over passing redundant explicit click handlers.
112114
- Use explicit click handler params only when dot-owned handling is not viable.
113-
- Wrapper template root should include `id="{{$.Jid}}" {{$.Attrs}}`.
114-
- Add interaction semantics where needed, for example `role="button" tabindex="0"`.
115+
- `ui.Template` creates the outer JaWS wrapper; template roots should not declare the JaWS ID or carry forwarded wrapper attributes.
116+
- Add interaction semantics where needed through Template params, for example `role="button"` and `tabindex="0"`.
115117
- Keep body partials presentational; attach behavior at wrapper/dot level.
116118

117119
## Rendering and update rules
118120

119121
- Keep HTML structure in templates; avoid manual HTML string assembly in Go.
122+
- `ui.Template.JawsUpdate` re-renders the template data into the generated wrapper; custom `Dot.JawsUpdate` methods are not called by Template.
120123
- HTML getter paths must not mutate domain state, but they may call element update methods (`SetClass`, `RemoveClass`, `SetAttr`, `RemoveAttr`, etc.) on the passed-in `*Element` to co-ordinate wrapper class/attribute changes with the inner-HTML refresh. No custom `JawsUpdate` is needed for that case — the queued wrapper updates flush alongside the `SetInner` from `HTMLInner.JawsUpdate`.
121124
- Use a custom `JawsUpdate` only when the widget's update logic diverges from "render the getter again" — e.g. to compare against a stored last-value and skip the update (as the input widgets do).
122125
- `Element.SetAttr/RemoveAttr/SetClass/RemoveClass/SetInner/SetValue/Append/Order/Remove/Replace` are update-time operations; call them only from render/update processing.

jaws_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,7 @@ func TestJaws_Session(t *testing.T) {
875875
}
876876

877877
h.ServeHTTP(&rr, r)
878-
if got := buf.String(); got != `<div id="Jid.1" >123</div>` {
878+
if got := buf.String(); got != `<div id="Jid.1" data-jawstemplate>123</div>` {
879879
t.Error(got)
880880
}
881881

lib/assets/jaws.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
[data-jawstemplate]:not([hidden]) {
2+
display: contents;
3+
}
4+
15
.jaws-lost {
26
display: flex;
37
position: relative;
@@ -13,4 +17,3 @@
1317
background-color: red;
1418
color: white;
1519
}
16-

lib/ui/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ This package is the home of JaWS widget implementations.
1313

1414
`ui.RequestWriter` exposes helper methods like `rw.Span(...)`,
1515
`rw.Text(...)`, and `rw.Select(...)` for concise template use.
16+
`rw.Template(...)` renders partial templates inside a generated JaWS wrapper,
17+
so template bodies should let that wrapper own JaWS identity and wrapper-level
18+
attributes. Attribute params passed to `rw.Template(...)` are applied to the
19+
generated wrapper. Template bodies used with `rw.Template(...)` must be
20+
partials; full page templates should be rendered through `ui.Handler`.
1621

1722
You can also use explicit constructors through:
1823

lib/ui/handler.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,43 @@
11
package ui
22

33
import (
4+
"io"
45
"net/http"
56

67
"github.com/linkdata/jaws"
78
)
89

9-
// uiHandler is an http.uiHandler that renders a template for every request.
10+
// uiHandler is an http.Handler that renders a template for every request.
1011
//
1112
// It wires the incoming HTTP request through the JaWS rendering pipeline by
12-
// creating a Request, instantiating the configured Template and streaming the
13-
// resulting HTML to the caller. Applications typically construct handlers with
14-
// Handler.
13+
// creating a Request, instantiating the configured page template and streaming
14+
// the resulting HTML to the caller. Applications typically construct handlers
15+
// with Handler.
1516
type uiHandler struct {
1617
*jaws.Jaws
18+
Template pageTemplate
19+
}
20+
21+
type pageTemplate struct {
1722
Template
1823
}
1924

25+
func (tmpl pageTemplate) JawsRender(elem *jaws.Element, w io.Writer, params []any) (err error) {
26+
err = tmpl.Template.render(elem, w, params, false)
27+
return
28+
}
29+
30+
func (pageTemplate) JawsUpdate(*jaws.Element) {}
31+
2032
func (h uiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
2133
_ = h.Log(h.NewRequest(r).NewElement(h.Template).JawsRender(w, nil))
2234
}
2335

2436
// Handler returns an http.Handler that renders the named template.
2537
//
2638
// The returned handler can be registered directly with a router. Each request
27-
// results in the template being looked up through the configured Template
28-
// lookupers and rendered with dot as the template data.
39+
// results in the template being looked up through the configured template
40+
// lookupers and rendered directly with dot as the template data.
2941
func Handler(jw *jaws.Jaws, name string, dot any) http.Handler {
30-
return uiHandler{Jaws: jw, Template: Template{Name: name, Dot: dot}}
42+
return uiHandler{Jaws: jw, Template: pageTemplate{Template: Template{Name: name, Dot: dot}}}
3143
}

lib/ui/template.go

Lines changed: 73 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
package ui
22

33
import (
4-
"bytes"
54
"fmt"
65
"html/template"
76
"io"
87
"strings"
9-
"text/template/parse"
108

11-
"github.com/linkdata/deadlock"
129
"github.com/linkdata/jaws"
1310
"github.com/linkdata/jaws/lib/tag"
1411
)
@@ -17,8 +14,10 @@ import (
1714
//
1815
// The Name field identifies the template to execute and Dot contains the data
1916
// that will be exposed to the template through the [With] structure constructed
20-
// during rendering. Additional tag bindings and event handlers can be supplied
21-
// at render time through the [RequestWriter.Template] helper.
17+
// during rendering. Templates are rendered inside a generated wrapper element
18+
// that receives the JaWS ID and any HTML attributes supplied at render time
19+
// through the [RequestWriter.Template] helper. The referenced template must be
20+
// a partial template, not a full HTML document.
2221
type Template struct {
2322
Name string // Template name to be looked up using Jaws.LookupTemplate.
2423
Dot any // Dot value to place in With.
@@ -29,133 +28,98 @@ var _ jaws.ClickHandler = Template{} // statically ensure interface is def
2928
var _ jaws.ContextMenuHandler = Template{} // statically ensure interface is defined
3029
var _ jaws.InputHandler = Template{} // statically ensure interface is defined
3130

31+
type templateRenderMode uint8
32+
33+
const (
34+
templateRenderWrapped templateRenderMode = iota
35+
templateRenderDirect
36+
)
37+
3238
// String returns a debug representation of t.
3339
func (tmpl Template) String() string {
3440
return fmt.Sprintf("{%q, %s}", tmpl.Name, tag.TagString(tmpl.Dot))
3541
}
3642

37-
func findJidOrJsOrHTMLNode(node parse.Node) (found bool) {
38-
isJidOrJs := func(s string) bool {
39-
return (s == "Jid") || (s == "JsFunc") || (s == "JsVar")
43+
func (tmpl Template) lookup(elem *jaws.Element) (lookedUp *template.Template, err error) {
44+
err = errMissingTemplate(tmpl.Name)
45+
if lookedUp = elem.Request.Jaws.LookupTemplate(tmpl.Name); lookedUp != nil {
46+
err = nil
4047
}
41-
switch node := node.(type) {
42-
case *parse.TextNode:
43-
if node != nil {
44-
found = found || bytes.Contains(node.Text, []byte("</html>"))
45-
}
46-
case *parse.ListNode:
47-
if node != nil {
48-
for _, n := range node.Nodes {
49-
found = found || findJidOrJsOrHTMLNode(n)
50-
}
51-
}
52-
case *parse.ActionNode:
53-
if node != nil {
54-
found = findJidOrJsOrHTMLNode(node.Pipe)
55-
}
56-
case *parse.WithNode:
57-
if node != nil {
58-
found = findJidOrJsOrHTMLNode(&node.BranchNode)
59-
}
60-
case *parse.BranchNode:
61-
if node != nil {
62-
found = findJidOrJsOrHTMLNode(node.Pipe)
63-
found = found || findJidOrJsOrHTMLNode(node.List)
64-
found = found || findJidOrJsOrHTMLNode(node.ElseList)
65-
}
66-
case *parse.IfNode:
67-
if node != nil {
68-
found = findJidOrJsOrHTMLNode(node.Pipe)
69-
found = found || findJidOrJsOrHTMLNode(node.List)
70-
found = found || findJidOrJsOrHTMLNode(node.ElseList)
71-
}
72-
case *parse.RangeNode:
73-
if node != nil {
74-
found = findJidOrJsOrHTMLNode(node.Pipe)
75-
found = found || findJidOrJsOrHTMLNode(node.List)
76-
found = found || findJidOrJsOrHTMLNode(node.ElseList)
77-
}
78-
case *parse.TemplateNode:
79-
if node != nil {
80-
found = findJidOrJsOrHTMLNode(node.Pipe)
81-
}
82-
case *parse.PipeNode:
83-
if node != nil {
84-
for _, n := range node.Cmds {
85-
found = found || findJidOrJsOrHTMLNode(n)
86-
}
87-
}
88-
case *parse.CommandNode:
89-
if node != nil {
90-
for _, n := range node.Args {
91-
found = found || findJidOrJsOrHTMLNode(n)
92-
}
93-
}
94-
case *parse.VariableNode:
95-
if node != nil {
96-
for _, s := range node.Ident {
97-
found = found || isJidOrJs(s)
98-
}
99-
}
100-
case *parse.FieldNode:
101-
if node != nil {
102-
for _, s := range node.Ident {
103-
found = found || isJidOrJs(s)
104-
}
105-
}
106-
case *parse.IdentifierNode:
107-
if node != nil {
108-
found = found || isJidOrJs(node.Ident)
109-
}
110-
case *parse.ChainNode:
111-
if node != nil {
112-
found = findJidOrJsOrHTMLNode(node.Node)
113-
for _, s := range node.Field {
114-
found = found || isJidOrJs(s)
115-
}
48+
return
49+
}
50+
51+
func (tmpl Template) auth(elem *jaws.Element) (auth jaws.Auth) {
52+
auth = jaws.DefaultAuth{}
53+
if f := elem.Request.Jaws.MakeAuth; f != nil {
54+
auth = f(elem.Request)
55+
}
56+
return
57+
}
58+
59+
func (tmpl Template) execute(elem *jaws.Element, w io.Writer, lookedUp *template.Template) (err error) {
60+
err = lookedUp.Execute(w, With{
61+
Element: elem,
62+
RequestWriter: RequestWriter{Request: elem.Request, Writer: w},
63+
Dot: tmpl.Dot,
64+
Auth: tmpl.auth(elem),
65+
})
66+
return
67+
}
68+
69+
func writeTemplateWrapperStart(elem *jaws.Element, w io.Writer, attrs []string) (err error) {
70+
b := elem.Jid().AppendStartTagAttr(nil, "div")
71+
b = append(b, " data-jawstemplate"...)
72+
for _, attr := range attrs {
73+
if attr != "" {
74+
b = append(b, ' ')
75+
b = append(b, attr...)
11676
}
11777
}
78+
b = append(b, '>')
79+
_, err = w.Write(b)
11880
return
11981
}
12082

121-
// JawsRender renders t through the request's configured template lookupers.
122-
func (tmpl Template) JawsRender(elem *jaws.Element, w io.Writer, params []any) (err error) {
83+
func (tmpl Template) render(elem *jaws.Element, w io.Writer, params []any, wrapped bool) (err error) {
12384
var expandedTags []any
12485
if expandedTags, err = tag.TagExpand(elem.Request, tmpl.Dot); err == nil {
12586
elem.Request.TagExpanded(elem, expandedTags)
12687
tags, handlers, attrs := jaws.ParseParams(params)
12788
elem.Tag(tags...)
12889
elem.AddHandlers(handlers...)
129-
attrstr := template.HTMLAttr(strings.Join(attrs, " ")) // #nosec G203
130-
var auth jaws.Auth
131-
auth = jaws.DefaultAuth{}
132-
if f := elem.Request.Jaws.MakeAuth; f != nil {
133-
auth = f(elem.Request)
134-
}
135-
err = errMissingTemplate(tmpl.Name)
136-
if lookedUp := elem.Request.Jaws.LookupTemplate(tmpl.Name); lookedUp != nil {
137-
err = lookedUp.Execute(w, With{
138-
Element: elem,
139-
RequestWriter: RequestWriter{Request: elem.Request, Writer: w},
140-
Dot: tmpl.Dot,
141-
Attrs: attrstr,
142-
Auth: auth,
143-
})
144-
if deadlock.Debug && elem.Jaws.Logger != nil {
145-
if !findJidOrJsOrHTMLNode(lookedUp.Tree.Root) {
146-
elem.Jaws.Logger.Warn("jaws: template has no Jid reference", "template", tmpl.Name)
90+
var lookedUp *template.Template
91+
if lookedUp, err = tmpl.lookup(elem); err == nil {
92+
if wrapped {
93+
err = writeTemplateWrapperStart(elem, w, attrs)
94+
}
95+
if err == nil {
96+
if err = tmpl.execute(elem, w, lookedUp); err == nil {
97+
if wrapped {
98+
_, err = io.WriteString(w, "</div>")
99+
}
147100
}
148101
}
149102
}
150103
}
151104
return
152105
}
153106

154-
// JawsUpdate delegates updates to t.Dot when it implements [jaws.Updater].
107+
// JawsRender renders t through the request's configured template lookupers.
108+
func (tmpl Template) JawsRender(elem *jaws.Element, w io.Writer, params []any) (err error) {
109+
err = tmpl.render(elem, w, params, true)
110+
return
111+
}
112+
113+
// JawsUpdate re-renders t into the template wrapper.
155114
func (tmpl Template) JawsUpdate(elem *jaws.Element) {
156-
if dot, ok := tmpl.Dot.(jaws.Updater); ok {
157-
dot.JawsUpdate(elem)
115+
lookedUp, err := tmpl.lookup(elem)
116+
if err == nil {
117+
var sb strings.Builder
118+
if err = tmpl.execute(elem, &sb, lookedUp); err == nil {
119+
elem.SetInner(template.HTML(sb.String())) // #nosec G203
120+
}
158121
}
122+
elem.Request.MustLog(err)
159123
}
160124

161125
// JawsClick delegates click events to t.Dot when it implements [jaws.ClickHandler].
@@ -197,7 +161,9 @@ func NewTemplate(name string, dot any) Template {
197161
// Template renders the given template using [With] as data.
198162
//
199163
// The Dot field in [With] is set to dot, and name is resolved to a
200-
// [template.Template] using [jaws.Jaws.LookupTemplate].
164+
// [template.Template] using [jaws.Jaws.LookupTemplate]. Template output is
165+
// wrapped in a generated div that owns the JaWS ID and any HTML attrs passed in
166+
// params. The template must be a partial, not a full HTML document.
201167
func (rw RequestWriter) Template(name string, dot any, params ...any) error {
202168
return rw.UI(NewTemplate(name, dot), params...)
203169
}

0 commit comments

Comments
 (0)