diff --git a/.agents/skills/jaws/SKILL.md b/.agents/skills/jaws/SKILL.md
index eda7d027..79c12bcd 100644
--- a/.agents/skills/jaws/SKILL.md
+++ b/.agents/skills/jaws/SKILL.md
@@ -46,8 +46,7 @@ JaWS is an immediate-mode, server-driven UI framework, not an MVC framework.
nil `UI` interface is a no-op. Surviving such a call is up to the concrete type, not
a requirement: a widget that dereferences its fields panics, and none of the
standard `lib/ui` widgets document nil-receiver tolerance. Do not pass a nil pointer
- of a type that does not; use its zero value where that type documents one (for example
- `ui.Template{}`).
+ of a type that does not; use a zero value only where that type documents one.
- Every JaWS `UI` value is request-scoped. Once used by one Request, never use
that value with another Request; construct fresh widgets per request. The
widgets may still refer to shared, synchronized application state, binders,
@@ -60,7 +59,10 @@ JaWS is an immediate-mode, server-driven UI framework, not an MVC framework.
- `jaws.Container.JawsContains` must return `UI` items that are comparable and equal to
themselves (see above); returning one that is not cancels the `Request`. The
returned slice must not be mutated after return. A UI value may occur more than
- once in one returned slice only when its type supports multiple live Elements.
+ once in one returned slice only when its type supports multiple live Elements. Each
+ child must render one addressable direct DOM node carrying its Element's JaWS ID so
+ removal and ordering can target it; `ui.NewTemplate` provides that node through its
+ generated wrapper.
- Treat the package documentation shown by
`go doc github.com/linkdata/jaws/lib/ui` as the canonical standard-widget
multiplicity summary, and consult each concrete type's docs for its conditions.
@@ -184,11 +186,18 @@ These are the two usual building blocks for widget handlers passed to `$.Button`
```gotemplate
{{$.Template "div" "partialName" .Dot "class=\"panel\""}}
{{$.Template "tr" "rowPartial" . "class=\"selected\""}}
-{{$.Template "" "barePartial" .Dot}}
```
The outer tag should match the DOM context where the generated JaWS wrapper will
-be inserted. An empty outer tag renders the template without a generated wrapper.
+be inserted. An empty outer tag selects the default `div` wrapper. Use a semantic
+wrapper such as `tr`, `td`, `li`, or `option` where the DOM context requires it.
+
+For a static structural fragment that needs no JaWS-managed wrapper, use Go's native
+template inclusion so the surrounding Template owns the DOM:
+
+```gotemplate
+{{template "barePartial" .Dot}}
+```
JaWS parses template params as:
- HTML attrs: `string`, `[]string`, `template.HTMLAttr`, `[]template.HTMLAttr`
@@ -198,11 +207,36 @@ JaWS parses template params as:
Implications:
- Non-comparable handlers are not auto-tagged unless they implement `tag.TagGetter`.
- Pass explicit tags when dirty targeting depends on them.
-- HTML attributes passed to `$.Template(...)` are applied to the generated template wrapper, if one exists.
+- HTML attributes passed to `$.Template(...)` are applied to the generated template wrapper.
- Template bodies used with `$.Template(...)` must be partials, not full documents.
-- Unwrapped templates have no wrapper-owned DOM element for direct template updates; use nested JaWS UI for dynamic regions.
- 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.
+## Registering template-authored elements
+
+- `$.Register(updater, params...)` binds a render-independent `jaws.Updater` to a DOM
+ element whose markup is written by the surrounding template. The returned Jid must be
+ used as that element's HTML `id`:
+
+ ```gotemplate
+
+ ```
+
+- Register never calls `JawsRender`; use it only for a custom updater designed to work
+ without render-time initialization. It uses the updater as a tag, attaches its event
+ handlers, applies tag and handler params, and invokes `JawsUpdate` once. HTML attribute
+ params are ignored; write attributes on the template-authored element.
+- The updater must be a non-nil interface whose dynamic value is comparable at runtime,
+ equal to itself, and usable as a tag. A typed nil is invoked normally and must tolerate
+ its nil receiver. Reuse one updater for live Elements only when it supports that use
+ without retaining Element-specific state on the shared value; it must be safe for
+ concurrent use when shared across Requests.
+- Prefer ordinary widget rendering. The container family supports update-only
+ registration with the limitations below; typed inputs omit render-derived metadata,
+ while `ui.JsVar` and Templates with a non-empty `OuterHTMLTag` require rendering.
+- Always emit the returned Jid as the element's `id`. A surrounding Template owns the
+ registered Element; otherwise it remains until explicit deletion, reported DOM
+ removal, or Request shutdown.
+
## Event handling model
On incoming events, JaWS dispatches in this order:
@@ -240,8 +274,8 @@ For clickable content rendering:
should register and use a usable tag exposed by its handler.
- Container ownership also lives in `containerState`, not in `elem.UI()`. Cleanup
detaches children under the state mutex and recurses after unlocking, so it also finds
- children when the Element's visible UI is a `Register` wrapper. Failed render and
- append paths unregister every child and nested owner they created.
+ children when the Element's visible UI is the private registration wrapper. Failed
+ render and append paths unregister every child and nested owner they created.
- Provider callbacks and child validation run without the state mutex. Reconciliation
holds it only while matching children and calling `Request.NewElement`; rendering,
removal, cancellation, recursive cleanup and logging happen after it is released.
@@ -268,11 +302,10 @@ For clickable content rendering:
Element;
- a composite UI must use Template values equal under `==` for rendering and updating
an Element; using unequal values is unsupported;
- - a **wrapped** Template updates only an Element rendered by an equal Template value, so
- it is not usable as a `$.Register` updater;
- - an **unwrapped** Template is usable there — its updates are a documented no-op — and
- `$.Register` automatically attaches its click/input/context-menu handlers; a bare
- `ui.Register`/`ui.NewRegister` value promotes no handler methods.
+ - a Template with a non-empty `OuterHTMLTag`, including any returned by
+ `ui.NewTemplate`, updates only an Element rendered by an equal Template value, so it
+ is not usable as a `$.Register` updater; `$.Register` never invokes its renderer and
+ therefore cannot establish the wrapper state an update needs.
- Call `$.RadioGroup` from the template that renders the group: its Elements belong to
the template whose body called it, not to the wrapper their markup lands in.
- 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`.
@@ -338,6 +371,8 @@ Guideline:
definition equality with pointer identity.
- Passing a runtime-incomparable application object directly to a container-family
constructor instead of retaining it behind a stable pointer.
+- Using `$.Register` for a widget that can render its own element, or failing to place
+ its returned Jid on the template-authored DOM node the updater controls.
- Returning a shared/group tag from an item's `JawsGetTag` (bundling it into the item's own dirty identity), which makes a single-item `Dirty` fan out to the whole group.
- Passing explicit template click handlers when dot-owned `JawsClick` already covers behavior.
- Adding custom browser JavaScript for state that can be expressed through JaWS events and server updates.
diff --git a/contracts.go b/contracts.go
index 7641ccd1..a14245cc 100644
--- a/contracts.go
+++ b/contracts.go
@@ -17,9 +17,10 @@ type Container interface {
// NaN) cancels the [Request] instead of being reconciled. A typed nil is usable.
// The slice contents must not be modified after returning it. Returning a usable
// child UI again from a later call lets the container reuse its existing live
- // [Element]. The same UI may occur more than once in one returned slice only when
- // its type documents support for backing multiple live Elements. A child UI must
- // not be shared with a different [Request].
+ // [Element]. Each child must render one direct DOM node carrying its Element's JaWS
+ // ID, because reconciliation removes and orders that node. The same UI may occur more
+ // than once in one returned slice only when its type documents support for backing
+ // multiple live Elements. A child UI must not be shared with a different [Request].
JawsContains(elem *Element) (contents []UI)
}
@@ -36,11 +37,9 @@ type Renderer interface {
// Do not call this yourself unless it is from within another JawsRender implementation.
// The engine does not invoke this once the [Element] is deleted (see [Element.Deleted]).
//
- // When delegating, note that a renderer may claim the Element's widget state slot
- // (see [SetElementState]) and that only one of them can: a delegate whose own
- // renderer claims the slot — [github.com/linkdata/jaws/lib/ui.Template] does —
- // fails with [ErrElementStateClaimed] if the delegating renderer, or an earlier
- // delegate, already claimed it.
+ // A delegating renderer and its delegates may claim the Element's widget state
+ // slot only once. A later claim fails with [ErrElementStateClaimed]; see
+ // [SetElementState].
JawsRender(elem *Element, w io.Writer, params []any) error
}
diff --git a/lib/ui/README.md b/lib/ui/README.md
index cce27689..4fc36c35 100644
--- a/lib/ui/README.md
+++ b/lib/ui/README.md
@@ -15,67 +15,60 @@ This package is the home of JaWS widget implementations.
`rw.Text(...)`, and `rw.Select(...)` for concise template use.
`rw.Template(tag, ...)` renders partial templates inside a generated JaWS
wrapper using the provided HTML tag, so template bodies should let that wrapper
-own JaWS identity and wrapper-level attributes. Passing an empty tag renders the
-template without a generated wrapper. Attribute params passed to
-`rw.Template(...)` are applied to the generated wrapper when one exists.
+own JaWS identity and wrapper-level attributes. Passing an empty tag selects the
+default `div` wrapper. Attribute params passed to `rw.Template(...)` are applied
+to that generated wrapper.
Template bodies used with `rw.Template(...)` must be partials; full page
templates should be rendered through `ui.Handler`.
-Template execution is best-effort rather than transactional. Nested UI helpers
-such as `{{$.Span ...}}` register elements as the template runs, and custom
-template actions may queue updates or mutate application state. If execution
-later returns an error, JaWS returns or logs that error and preserves whatever
-already happened; it does not roll back partial output, queued messages, or
-application side effects. The tracked elements the failed execution registered are
-unregistered, since nothing will update them.
-
-A template owns every element created through the `RequestWriter` it is given —
-`{{$.Span ...}}`, `{{$.Button ...}}`, `{{$.Register ...}}`, `{{$.RadioGroup ...}}`,
-a nested `{{$.Template ...}}`, and so on. A successful update unregisters the ones
-the previous render left behind, along with the DOM that `SetInner` replaces.
-Ownership is recorded when an element is created rather than after it renders, so
-an element that never reached the browser is reclaimed too. On updates that
-`SetInner` is queued only after a complete successful render, so a failed update
-leaves the browser DOM unchanged — and with it the previous render's elements —
-while earlier server-side side effects from that attempted render may remain. Treat
-template execution errors as application bugs: validate data before rendering and
-keep template actions infallible once they start emitting output or nested UI.
-
-`$.RadioGroup` has one attribution condition: its radio and label elements belong to
-the template whose body called it, not to the wrapper their markup lands in. Call it
-from the template that renders the group; see `RequestWriter.RadioGroup` for what
-happens when the two differ.
-
-The ownership set lives in the element's widget state slot (`jaws.SetElementState`),
-not on the `ui.Template` value, which is what keeps `NewTemplate` returning a plain
-value. That matters for containers: a `JawsContains` implementation may
-rebuild equal child values on every call and the container will still reuse their
-elements, because that equality *is* the reuse key. Always use `ui.Template` as a
-value, as `NewTemplate` returns it; taking its address is unsupported because it changes
-container reuse to pointer identity. Under the general `jaws.UI` contract, the resulting
-Template must be comparable at runtime and equal to itself — a slice, map or func `Dot`
-makes the whole widget unusable, and implementing `tag.TagGetter` does not change that,
-since it addresses tag resolution rather than widget comparability.
-
-Comparability alone is not enough. A nil-interface `Dot` is valid and contributes no tag.
-A typed nil is a non-nil interface and follows its dynamic type's comparability and
-expansion rules. Rendering expands a non-nil-interface `Dot` through `tag.TagExpand`,
-which rejects the exact dynamic types `string`, `bool`, `int`/`int8`/`int16`/`int32`/`int64`,
-`uint`/`uint8`/`uint16`/`uint32`/`uint64`, `float32`/`float64`, `template.HTML`,
-`template.HTMLAttr`, `jid.Jid` and `key.Key`. Aliases of a rejected type have that same
-dynamic type and are rejected. `uintptr` and the complex types are not on the rejection
-list. Other defined types are not rejected merely because their underlying predeclared
-type is on it; they must still be comparable and equal to themselves. Wrap a rejected
-scalar in `tag.Tag("...")` or a comparable struct when it should be a tag.
-
-A template claims that slot while rendering, so at most one template may render a given
-element. A composite UI must use template values equal under `==` for rendering and
-updating that element; using unequal values is unsupported. A wrapped template is
-therefore not usable as a `$.Register` updater — `$.Register` never invokes its updater's
-render method — while an unwrapped one is, since its updates are a documented no-op. On
-an element no template claimed, a wrapped template's update reports
-`ErrElementStateUnclaimed` through `jaws.Request.MustLog`, which **panics** when no
-`Jaws.Logger` is configured.
+`rw.Register(...)` is the escape hatch for attaching a render-independent updater
+to an element whose markup is written directly in the surrounding template. Its
+returned JaWS ID must become that element's `id`:
+
+```gotemplate
+
+ template-authored content
+
+```
+
+`Register` never calls `JawsRender`; use it for a custom updater designed to work
+without render-time initialization. It tags the element with the updater, attaches
+its event handlers, and calls `JawsUpdate` once for initial state. Write HTML
+attributes in the template because attribute params are ignored. Prefer a normal
+widget helper whenever the widget can render its own element; see
+`RequestWriter.Register` for the standard-widget limitations.
+
+Use Go's native template action when a static structural fragment must be included
+without another JaWS-managed wrapper:
+
+```gotemplate
+{{template "partial" .Dot}}
+```
+
+`rw.Template` supplies an addressable wrapper for updates and container
+reconciliation. Choose the semantic element required by the DOM context, such as
+`tr`, `td`, `li`, or `option`, instead of relying on the `div` default there.
+
+Template execution is not transactional. An error may leave partial output, queued
+messages, or application side effects in place. Elements created by the failed
+attempt are unregistered; a failed update retains the previous browser DOM and its
+Elements.
+
+A Template owns every Element created through its `RequestWriter`. A successful
+update unregisters the previous generation when it replaces the wrapper content.
+
+Call `$.RadioGroup` from the Template that renders the group; ownership follows the
+call site rather than the wrapper receiving its markup.
+
+Use the `ui.Template` value returned by `NewTemplate` directly; taking its address
+changes container identity to pointer identity. A container may retain Elements for
+equal values rebuilt by `JawsContains`. A Template must be comparable and equal to
+itself, and its `Dot` must be nil or usable as a tag under `tag.TagExpand`. Use
+`tag.Tag("...")` for string tags.
+
+A Template with a non-empty `OuterHTMLTag` can update only an Element rendered by
+an equal Template value. It is not usable as a `$.Register` updater because
+registration does not call its renderer.
## Container-family value widgets
@@ -100,6 +93,10 @@ values may back several live Elements within one request when their providers or
handlers are safe for all calls and each child UI value reused across those Elements
supports multiple live Elements.
+Each child must render one addressable direct DOM node carrying its Element's JaWS ID,
+because removal and ordering target that node. Construct Template children with
+`NewTemplate`, which always supplies a wrapper.
+
Reconciliation updates direct children only. Reordering retained equal children
preserves their Elements and nested subtrees; changed nested containers need their own
update. Child Element identity is parent-scoped, so moving a child definition between
@@ -110,11 +107,9 @@ provider, panic when rendering or updating calls the missing provider. A zero
Select behaves the same for render and update, while its `JawsInput` is a no-op.
A typed-nil provider is called normally and must tolerate its nil receiver itself.
-Container, Tbody, and Select support update-only use through `Register`. A Select
-registered this way retains no handler-derived tag for its own post-set dirtying.
-Any handler-initiated dirtying still occurs, and separately registered tags remain
-registered. Use ordinary Select rendering when Select should register and use a usable
-tag exposed by its handler.
+Container, Tbody, and Select support update-only use through
+`RequestWriter.Register`, although ordinary rendering provides their full
+initialization. A registered Select has no getter-derived tag for post-input dirtying.
You can also use explicit constructors through:
diff --git a/lib/ui/container.go b/lib/ui/container.go
index 25debe6b..28fd36e2 100644
--- a/lib/ui/container.go
+++ b/lib/ui/container.go
@@ -17,6 +17,8 @@ import (
// preserves their Elements and nested subtrees. Nested containers whose children
// change need their own update. Child Element identity is scoped to its parent;
// moving a child definition between parents does not preserve its Element.
+// Each child must render one direct DOM node carrying its Element's JaWS ID. Use
+// [NewTemplate] for Template children so removal and ordering can target a wrapper.
//
// Equal Container values may back multiple live Elements in one [jaws.Request]
// when the provider is safe for all calls and each child UI value reused across
@@ -47,8 +49,8 @@ func (u Container) JawsRender(elem *jaws.Element, w io.Writer, params []any) err
// JawsUpdate reconciles u's direct children.
//
-// JawsUpdate supports update-only use through [Register]. If elem's widget state
-// cannot be used, it reports [jaws.ErrElementStateClaimed] through
+// JawsUpdate supports update-only use through [RequestWriter.Register]. If elem's
+// widget state cannot be used, it reports [jaws.ErrElementStateClaimed] through
// [jaws.Request.MustLog] without calling the provider or queuing browser work.
func (u Container) JawsUpdate(elem *jaws.Element) {
u.update(elem)
diff --git a/lib/ui/container_reuse_benchmark_test.go b/lib/ui/container_reuse_benchmark_test.go
index 6e03de49..080dfc72 100644
--- a/lib/ui/container_reuse_benchmark_test.go
+++ b/lib/ui/container_reuse_benchmark_test.go
@@ -219,7 +219,7 @@ func BenchmarkContainerAppendRemoveUpdate(b *testing.B) {
}
// BenchmarkContainerRegisterFirstUpdate measures the lazy state-claim path used by an
-// update-only Register Element.
+// update-only registered Element.
func BenchmarkContainerRegisterFirstUpdate(b *testing.B) {
b.StopTimer()
tr := newReuseRequest(b)
@@ -232,7 +232,7 @@ func BenchmarkContainerRegisterFirstUpdate(b *testing.B) {
batchSize := min(benchmarkContainerBatchSize, b.N-completed)
elems := make([]*jaws.Element, batchSize)
for i := range elems {
- elems[i] = tr.NewElement(NewRegister(container))
+ elems[i] = tr.NewElement(registerUI{Updater: container})
}
b.StartTimer()
diff --git a/lib/ui/container_reuse_test.go b/lib/ui/container_reuse_test.go
index 096266a5..75669ae4 100644
--- a/lib/ui/container_reuse_test.go
+++ b/lib/ui/container_reuse_test.go
@@ -143,6 +143,72 @@ func TestContainer_RebuiltTemplateChildrenAreReused(t *testing.T) {
}
}
+// TestContainer_DefaultWrappedTemplateChildrenCanReorderAndRemove verifies that the
+// wrapper NewTemplate supplies for an empty tag gives reconciliation a direct DOM node
+// for each child. Reordering retained children must queue only Order, while removing one
+// must target its existing Jid rather than replace the remaining children.
+func TestContainer_DefaultWrappedTemplateChildrenCanReorderAndRemove(t *testing.T) {
+ tr := newReuseRequest(t)
+
+ first := &reuseRow{id: 1}
+ second := &reuseRow{id: 2}
+ third := &reuseRow{id: 3}
+ tc := &rebuildingContainer{
+ rows: []*reuseRow{first, second, third},
+ build: func(row *reuseRow) jaws.UI {
+ return NewTemplate("", "row", row)
+ },
+ }
+ elem := tr.NewElement(NewContainer("section", tc))
+ var output strings.Builder
+ if err := elem.JawsRender(&output, nil); err != nil {
+ t.Fatal(err)
+ }
+ if got := strings.Count(output.String(), `
{{end}}
-{{define "state-owned-container-child"}}{{$.RequestWriter.Template "" "owned-leaf" $.Dot}}{{$.RequestWriter.Container "div" $.Dot.Container}}{{end}}
+{{define "state-owned-container-child"}}{{$.RequestWriter.Template "div" "owned-leaf" $.Dot}}{{$.RequestWriter.Container "div" $.Dot.Container}}{{end}}
`
type registerContainerDot struct {
@@ -52,8 +52,8 @@ func containerStateOwnedTemplateChildren(t *testing.T, elem *jaws.Element) (chil
}
// TestTemplateRegisterContainerReclaimsChildren covers the ownership path whose
-// Element stores a Register UI wrapper rather than the Container updater. Recursive
-// cleanup must find the Container's children through the Element state slot.
+// Element stores a private registration wrapper rather than the Container updater.
+// Recursive cleanup must find the Container's children through the Element state slot.
func TestTemplateRegisterContainerReclaimsChildren(t *testing.T) {
jw, rq := newOwnedRequest(t)
addContainerStateOwnedTemplates(t, jw)
@@ -63,7 +63,7 @@ func TestTemplateRegisterContainerReclaimsChildren(t *testing.T) {
tmpl := NewTemplate("div", "state-register-container", dot)
wrapper := renderOwned(t, rq, tmpl)
- const wantRegistered = 3 // Template wrapper, Register Element, Container child.
+ const wantRegistered = 3 // Template wrapper, registered Element, Container child.
if got := countRegistered(t, rq); got != wantRegistered {
t.Fatalf("registered elements after render = %d, want %d", got, wantRegistered)
}
@@ -71,19 +71,19 @@ func TestTemplateRegisterContainerReclaimsChildren(t *testing.T) {
for round := 1; round <= 3; round++ {
generation := containerStateOwnedTemplateChildren(t, wrapper)
if len(generation) != 1 {
- t.Fatalf("round %d: Template owns %d Elements, want the Register Element", round, len(generation))
+ t.Fatalf("round %d: Template owns %d Elements, want the registered Element", round, len(generation))
}
registerElem := generation[0]
children := containerStateOwnedChildren(t, registerElem)
if len(children) != 1 {
- t.Fatalf("round %d: Register Container owns %d children, want 1", round, len(children))
+ t.Fatalf("round %d: registered Container owns %d children, want 1", round, len(children))
}
childElem := children[0]
tmpl.JawsUpdate(wrapper)
if !registerElem.Deleted() || rq.GetElementByJid(registerElem.Jid()) != nil {
- t.Fatalf("round %d: previous Register Element %v is still registered", round, registerElem.Jid())
+ t.Fatalf("round %d: previous registered Element %v is still registered", round, registerElem.Jid())
}
if !childElem.Deleted() || rq.GetElementByJid(childElem.Jid()) != nil {
t.Fatalf("round %d: previous Container child %v is still registered", round, childElem.Jid())
diff --git a/lib/ui/containerstate.go b/lib/ui/containerstate.go
index 32c730f1..8a8cb2ec 100644
--- a/lib/ui/containerstate.go
+++ b/lib/ui/containerstate.go
@@ -16,7 +16,7 @@ type containerState struct {
mu sync.Mutex
// rendering is true from the successful state claim until JawsRender finishes.
// It keeps an update from treating a published but incomplete state as the lazy
- // state of an update-only Register Element.
+ // state of an update-only registered Element.
rendering bool
dirtyTag any
contents []*jaws.Element
@@ -68,7 +68,7 @@ func claimContainerState(elem *jaws.Element) (st *containerState, err error) {
// stateForContainerUpdate returns usable container state for elem, claiming an empty
// state when the slot is unclaimed.
func stateForContainerUpdate(elem *jaws.Element) (st *containerState, err error) {
- // Update-only Register is the intended lazy-claim path, but the state slot does
+ // Update-only registration is the intended lazy-claim path, but the state slot does
// not encode how the Element was created. Treat an occupied foreign slot, typed
// nil, in-progress render, or lost concurrent claim as contention.
switch state := jaws.ElementState(elem).(type) {
diff --git a/lib/ui/doc.go b/lib/ui/doc.go
index adca2e66..933a5b04 100644
--- a/lib/ui/doc.go
+++ b/lib/ui/doc.go
@@ -11,10 +11,10 @@
// for each request. Widgets from different requests may refer to the same
// synchronized application state, binders, handlers, or tags.
//
-// [Container], [Tbody], [Select], [Option], [Register], and [Template]
-// constructors return values; other constructors generally return pointers. Use
-// Container, Tbody, Select, and Template as values because taking their addresses
-// changes definition equality to pointer identity.
+// [Container], [Tbody], [Select], [Option], and [Template] constructors return
+// values; other constructors generally return pointers. Use Container, Tbody,
+// Select, and Template as values because taking their addresses changes definition
+// equality to pointer identity.
//
// See [jaws.UI] and each concrete widget for comparability, typed-nil, and
// zero-value behavior. No pointer widget in this package documents nil-receiver
@@ -23,8 +23,15 @@
// Within one request, a widget normally backs one live [jaws.Element]. The
// HTML-inner widgets, [Img], [Option], [Template], [Container], [Tbody], and
// [Select] support multiple live Elements under their documented conditions.
-// [Register] does so only when its updater does; input widgets and [JsVar] require
-// distinct widget values.
+// Input widgets and [JsVar] require distinct widget values.
+//
+// [RequestWriter.Register] binds a render-independent updater to a DOM element
+// authored by the surrounding template. It is an escape hatch for custom markup,
+// not a replacement for rendering a widget normally.
+//
+// Container children must render one addressable direct DOM node carrying their
+// Element's JaWS ID so reconciliation can remove and order them. [NewTemplate]
+// supplies that node through its generated wrapper.
//
// HTML-inner widgets route content through [bind.MakeHTMLGetter]. Plain strings
// are treated as trusted HTML, while [bind.Getter][string], [bind.Binder][string]
diff --git a/lib/ui/errelementstateunclaimed.go b/lib/ui/errelementstateunclaimed.go
index 1e4595ce..3c1a8d2b 100644
--- a/lib/ui/errelementstateunclaimed.go
+++ b/lib/ui/errelementstateunclaimed.go
@@ -4,19 +4,11 @@ import (
"strconv"
)
-// ErrElementStateUnclaimed reports that a wrapped [Template] tried to update a
-// [jaws.Element] for which no Template claimed the state slot during rendering.
+// ErrElementStateUnclaimed reports an update of an Element that no [Template]
+// rendered.
//
-// With no claim there is no previous generation to reconcile against, so the update
-// executes nothing. The usual cause is using a wrapped Template as a
-// [RequestWriter.Register] updater; RequestWriter.Register does not call
-// [Template.JawsRender], so it cannot claim the Element while rendering.
-//
-// [Template.JawsUpdate] reports it through [jaws.Request.MustLog] rather than returning
-// it, so it reaches a caller as a returned error only where that panic is recovered: an
-// `html/template` action wraps it in an execution error, which [errors.Is] resolves
-// through. Template lookup happens first, so a missing template reports
-// [ErrMissingTemplate] and this error is reached only after a successful lookup.
+// [Template.JawsUpdate] reports this error through [jaws.Request.MustLog] instead
+// of returning it.
var ErrElementStateUnclaimed errElementStateUnclaimed
type errElementStateUnclaimed string
diff --git a/lib/ui/example_test.go b/lib/ui/example_test.go
index 7a57861e..bf5d7e35 100644
--- a/lib/ui/example_test.go
+++ b/lib/ui/example_test.go
@@ -89,6 +89,14 @@ func ExampleTemplate_failureBehavior() {
// true
}
+func ExampleNewTemplate_defaultWrapper() {
+ tmpl := ui.NewTemplate("", "partial", tag.Tag("dot"))
+ fmt.Println(tmpl.OuterHTMLTag)
+
+ // Output:
+ // div
+}
+
type exampleContainer []string
func (c exampleContainer) JawsContains(elem *jaws.Element) (contents []jaws.UI) {
diff --git a/lib/ui/handler.go b/lib/ui/handler.go
index a647b533..4561e7e6 100644
--- a/lib/ui/handler.go
+++ b/lib/ui/handler.go
@@ -89,7 +89,9 @@ func (h uiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Request.NewElement if a bare pageTemplate value (whose Dot is any) were used.
// The pointer identity is always comparable and fresh per request. Element tracking
// lives in the page Element's state slot claimed by pageTemplate.JawsRender.
- pt := &pageTemplate{Template: Template{Name: h.name, Dot: h.dot}}
+ // The private constructor bypasses NewTemplate's "div" default. pageTemplate
+ // executes the document directly and deliberately emits no generated wrapper.
+ pt := &pageTemplate{Template: newTemplate("", h.name, h.dot)}
if err := rw.NewUI(pt); err != nil {
_ = h.Log(err)
// A failure before any output (for example a missing template) can still
@@ -107,7 +109,9 @@ func (h uiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// The returned handler can be registered directly with a router. Each request
// results in the template being looked up through the configured template
// lookupers and rendered with a [With] value as the template data, exposing
-// dot through its Dot field.
+// dot through its Dot field. Handler renders without a generated wrapper and does
+// not use dot as a tag, so dot may be arbitrary template data. The handler reuses
+// dot across requests; dot and its callbacks must support concurrent execution.
func Handler(jw *jaws.Jaws, name string, dot any) http.Handler {
return uiHandler{Jaws: jw, name: name, dot: dot}
}
diff --git a/lib/ui/html_widgets_test.go b/lib/ui/html_widgets_test.go
index 9d617748..8726c9fa 100644
--- a/lib/ui/html_widgets_test.go
+++ b/lib/ui/html_widgets_test.go
@@ -117,15 +117,6 @@ func TestOption_RenderBoolNameTakesPrecedenceOverCallerValue(t *testing.T) {
}
}
-func TestRegister_Render(t *testing.T) {
- _, rq := newCoreRequest(t)
- ui := NewRegister(NewSpan(testHTMLGetter("x")))
- _, got := renderUI(t, rq, ui)
- if got != "" {
- t.Fatalf("expected empty output got %q", got)
- }
-}
-
func TestHTMLInner_RenderInitialHTMLAttrFromObject(t *testing.T) {
_, rq := newCoreRequest(t)
diff --git a/lib/ui/input_widgets.go b/lib/ui/input_widgets.go
index 1206ec77..cc406259 100644
--- a/lib/ui/input_widgets.go
+++ b/lib/ui/input_widgets.go
@@ -170,7 +170,7 @@ func (u *InputFloat) JawsUpdate(elem *jaws.Element) {
elem.Cancel(fmt.Errorf("%w: %g", jaws.ErrValueNotFinite, v))
return
}
- // An empty Last (no value stored yet, e.g. an update-only Register that never ran
+ // An empty Last (no value stored yet, e.g. update-only registration that never ran
// renderFloatInput) makes the float64 assertion fail with ok==false; send the
// initial value unconditionally in that case, matching how the other input
// widgets' nil != value comparison sends on their first update.
diff --git a/lib/ui/register.go b/lib/ui/register.go
index 2d51f708..cd080ad8 100644
--- a/lib/ui/register.go
+++ b/lib/ui/register.go
@@ -7,55 +7,42 @@ import (
"github.com/linkdata/jaws/lib/jid"
)
-// Register is an update-only widget that renders no HTML; it exists so its
-// embedded [jaws.Updater] receives dynamic updates.
-//
-// One Register value may back multiple live [jaws.Element] values only when its
-// Updater supports those calls without retaining Element-specific state on a
-// shared value.
-//
-// Register does not call the updater's [jaws.Renderer.JawsRender]. The updater
-// must support [jaws.Updater.JawsUpdate] without render-time initialization. A
-// wrapped [Template] does not; [Container], [Tbody], and [Select] do.
-//
-// Register does not forward event-handler methods. [RequestWriter.Register]
-// automatically attaches handler methods implemented by its updater.
-type Register struct{ jaws.Updater }
+// registerUI adapts a render-independent updater to [jaws.UI]. The surrounding
+// template renders the Element's DOM node and places its JaWS ID on that node.
+type registerUI struct{ jaws.Updater }
-// NewRegister returns an update-only widget that invokes updater during updates.
-func NewRegister(updater jaws.Updater) Register { return Register{Updater: updater} }
-
-// JawsRender renders no HTML for update-only registration.
-//
-// It ignores params; to attach extra tags or event handlers, use
-// [RequestWriter.Register], which applies them before the element is frozen.
-func (u Register) JawsRender(elem *jaws.Element, w io.Writer, params []any) error {
+func (registerUI) JawsRender(*jaws.Element, io.Writer, []any) error {
return nil
}
-// Register creates an update-only Element and returns its [jid.Jid].
+// Register binds updater to a template-authored HTML element.
+//
+// The returned [jid.Jid] must be the element's id. Register never calls
+// [jaws.Renderer.JawsRender], so updater must work without render-time
+// initialization. Prefer [RequestWriter.NewUI] or a widget helper when the widget
+// can render its own element.
//
-// The updater is also a tag for dynamic updates. Additional tags may be provided
-// in params.
-// If updater also implements an event handler interface, it receives matching
-// events after handlers provided in params have had a chance to handle them.
-// The updater's [jaws.Updater.JawsUpdate] method will be called immediately to
-// ensure the initial rendering is correct.
+// Register tags the Element with updater, applies tag and event-handler params,
+// attaches event-handler methods implemented by updater, and invokes
+// [jaws.Updater.JawsUpdate] once for the initial browser state. Updater handlers
+// are tried only after applicable param handlers return [jaws.ErrEventUnhandled].
+// HTML attribute params have no effect; write attributes in the template.
//
-// A surrounding [Template] owns the Element and unregisters it when its content
-// is replaced. Otherwise ordinary DOM-removal handling unregisters it; an Element
-// whose Jid never reaches the DOM remains until explicitly removed or its
-// [jaws.Request] ends.
+// The updater must be non-nil, comparable at runtime, equal to itself, and usable
+// as a tag. A typed nil is invoked normally and must tolerate its nil receiver.
+// The same updater may back multiple live Elements only when it supports that use
+// without retaining Element-specific state on the shared value. If shared across
+// requests, it must be safe for concurrent use.
//
-// Register does not call the updater's [jaws.Renderer.JawsRender]; see [Register]
-// for updater constraints. It automatically attaches event-handler methods
-// implemented by updater.
+// A surrounding [Template] owns and cleans up the registered Element. Outside a
+// Template, it remains registered until explicitly deleted, DOM removal is
+// reported, or its [jaws.Request] ends; always emit the returned Jid.
//
-// A Select registered this way retains no handler-derived tag for its own post-set
-// dirtying. Any handler-initiated dirtying still occurs, and separately registered
-// tags remain registered. Use [RequestWriter.Select] when Select should register and
-// use a usable tag exposed by its handler. Typed input widgets require their ordinary
-// NewUI or RequestWriter rendering path.
+// [Container], [Tbody], and [Select] support registration, though ordinary
+// rendering is preferable. A registered Select omits its handler-derived tag for
+// post-input dirtying. Typed input widgets omit getter-derived attributes,
+// handlers, and their getter-derived dirty tag. [JsVar] and a [Template] with a
+// non-empty OuterHTMLTag require ordinary rendering.
//
// The returned Jid is suitable for including as an HTML id attribute:
//
@@ -66,10 +53,10 @@ func (rw RequestWriter) Register(updater jaws.Updater, params ...any) jid.Jid {
// JawsRender, which appends a debug comment when Jaws.Debug is set, and the
// documented usage puts the returned Jid inside an attribute
// (), where that comment would corrupt the markup.
- elem := rw.NewElement(Register{Updater: updater})
+ elem := rw.NewElement(registerUI{Updater: updater})
rw.trackElement(elem)
elem.Tag(updater)
- // The wrapping Register element's UI is not the updater, so events reach the
+ // The registerUI Element's UI is not the updater, so events reach the
// updater only through the element's handler list, not the elem.UI() fallback.
switch updater.(type) {
case jaws.InputHandler, jaws.ClickHandler, jaws.ContextMenuHandler:
diff --git a/lib/ui/requestwriter_test.go b/lib/ui/requestwriter_test.go
index f9147ea2..be75f28c 100644
--- a/lib/ui/requestwriter_test.go
+++ b/lib/ui/requestwriter_test.go
@@ -203,6 +203,15 @@ func TestRequestWriter_RegisterFreezesElement(t *testing.T) {
elem.AddHandlers(struct{}{}) // production logs and drops, must not panic
}
+func TestRegisterUI_RenderDoesNothing(t *testing.T) {
+ _, rq := newCoreRequest(t)
+ registered := registerUI{Updater: NewSpan(testHTMLGetter("x"))}
+ _, got := renderUI(t, rq, registered)
+ if got != "" {
+ t.Fatalf("registerUI render output = %q, want empty", got)
+ }
+}
+
func TestRequestWriter_RegisterUsesUpdaterEventHandler(t *testing.T) {
_, rq := newCoreRequest(t)
var buf bytes.Buffer
diff --git a/lib/ui/template.go b/lib/ui/template.go
index a17335f2..2c6f071b 100644
--- a/lib/ui/template.go
+++ b/lib/ui/template.go
@@ -11,80 +11,33 @@ import (
"github.com/linkdata/jaws/lib/tag"
)
-// Template references a Go [html/template] template to be rendered through JaWS.
+// Template renders a named Go [html/template] partial through JaWS.
//
-// A Template retains no Element-specific state and may back multiple live
-// [jaws.Element] values: the Elements created while it executes are tracked in the
-// rendering Element's widget state slot (see [jaws.SetElementState]), not on the
-// Template. Its Dot and any callbacks reached during execution are shared by those
-// Elements and must be safe for their render, update and event calls.
+// Use Templates as values; [NewTemplate] is the usual constructor for wrapped
+// Templates. A Template may back multiple live [jaws.Element] values in one
+// [jaws.Request]; its Dot and callbacks must support all of their render, update,
+// and event calls. Taking a Template's address is unsupported because it changes
+// container reuse from value identity to pointer identity.
//
-// Template is a value widget and must be passed to JaWS as a value, normally the value
-// returned by [NewTemplate]. Do not take its address: although Go gives *Template the
-// value receiver's method set, a container would then key reuse on pointer identity
-// instead of Template value equality.
+// Dot may be a nil interface. Otherwise the Template must be comparable at
+// runtime and equal to itself, and Dot must be usable as a tag under
+// [tag.TagExpand].
//
-// Like every [jaws.UI] value passed to [jaws.Request.NewElement], a Template must be
-// comparable at runtime and equal to itself because the container widgets use UI values
-// as map keys. A Dot holding a slice, map, func or NaN makes the Template unusable.
-// Comparability is necessary but not sufficient. A nil-interface Dot is valid and
-// contributes no tag. A typed nil is a non-nil interface and follows its dynamic type's
-// comparability and expansion rules. Rendering expands a non-nil-interface Dot through
-// [github.com/linkdata/jaws/lib/tag.TagExpand], which rejects the exact dynamic types
-// string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64,
-// float32, float64, [html/template.HTML], [html/template.HTMLAttr],
-// [github.com/linkdata/jaws/lib/jid.Jid] and [github.com/linkdata/jaws/lib/key.Key].
-// Aliases of a rejected type have that same dynamic type and are rejected. uintptr and
-// the complex types are not on the rejection list. Other defined types are not rejected
-// merely because their underlying predeclared type is on it; they must still be comparable
-// and equal to themselves. [Handler] is the arbitrary-Dot exception; its private
-// whole-page renderer is distinct from a Template widget and does not use the page dot as
-// a tag.
+// OuterHTMLTag names the wrapper that receives the JaWS ID and render-time HTML
+// attributes. An empty field renders without a wrapper, making [Template.JawsUpdate]
+// a no-op. [NewTemplate] defaults an empty wrapper argument to "div". The named
+// template must be a partial; use [Handler] for a complete document.
//
-// The state slot is claimed while rendering, so at most one Template may render a given
-// Element. On an Element no Template claimed, an unwrapped [Template.JawsUpdate] is a
-// no-op while a wrapped one executes nothing and reports [ErrElementStateUnclaimed]
-// through [jaws.Request.MustLog], which panics when no [jaws.Jaws.Logger] is configured.
-// A composite UI that delegates rendering and updating to a Template must use Template
-// values equal under == for both calls; using unequal values is unsupported. A claim
-// survives a render error the delegating renderer handles, so a later update through an
-// equal Template value can still run when the delegator preserves the wrapped Template's
-// DOM target.
+// A Template owns the Elements created through its [RequestWriter]. A successful
+// update unregisters Elements from the previous execution. Elements created by a
+// failed execution are also unregistered.
//
-// The OuterHTMLTag field identifies the generated wrapper element used for
-// partial templates. If OuterHTMLTag is empty, the template is rendered without
-// a generated wrapper. Name identifies the template to execute and Dot contains
-// the data exposed to the template through the [With] structure constructed
-// during rendering. Wrapped templates receive the JaWS ID and any HTML
-// attributes supplied at render time through the [RequestWriter.Template]
-// helper. The referenced template must be a partial template, not a full HTML
-// document.
-//
-// Every Element a template creates through the [RequestWriter] it is given belongs to
-// the Template that rendered it — the widget helpers, [RequestWriter.Register],
-// [RequestWriter.RadioGroup] and a nested [RequestWriter.Template] alike. When
-// [Template.JawsUpdate] replaces the wrapper's content, those Elements are
-// unregistered along with the DOM that held them, and a nested widget's own Elements
-// go with it.
-//
-// Ownership is recorded when an Element is created rather than after it renders, so
-// one that never reaches the browser is reclaimed too: an Element whose render failed,
-// or a radio Element left unrendered by a [RadioElement.Label] without its
-// [RadioElement.Radio]. See [RequestWriter.RadioGroup] for the one attribution
-// condition, which applies when a group's markup ends up in a different wrapper than
-// the RadioGroup call.
-//
-// Template execution is best-effort rather than transactional. Template actions
-// and nested JaWS helpers run as the template executes, so an execution error
-// after partial output can leave already-written HTML, queued messages, domain
-// mutations or other side effects in place. The tracked Elements created by the
-// failed execution are unregistered. Treat such errors as application bugs:
-// validate data before rendering and keep template actions infallible once they
-// start emitting output or nested UI.
+// Execution is not transactional. An error may leave partial output, queued
+// messages, or application side effects in place.
type Template struct {
- OuterHTMLTag string // Optional wrapper tag for partial templates, for example "div" or "tr"; empty renders unwrapped.
+ OuterHTMLTag string // Wrapper element; empty renders unwrapped and disables JawsUpdate.
Name string // Template name to be looked up using Jaws.LookupTemplate.
- Dot any // Dot value to place in With.
+ Dot any // Template data exposed as With.Dot and expanded for tag registration.
}
var (
@@ -241,13 +194,11 @@ func (tmpl Template) render(elem *jaws.Element, w io.Writer, params []any) (err
return
}
-// JawsRender renders t through the request's configured template lookupers,
-// streaming output directly to w. Template execution has the best-effort error
-// behavior described on [Template].
+// JawsRender renders t through the request's configured template lookupers.
//
-// It claims the [jaws.Element]'s widget state slot before doing anything else, so
-// rendering a second Template into one Element fails with
-// [jaws.ErrElementStateClaimed] having changed nothing.
+// If elem's widget state is occupied, JawsRender returns
+// [jaws.ErrElementStateClaimed] without output. Other errors may leave partial
+// output or side effects as described on [Template].
func (tmpl Template) JawsRender(elem *jaws.Element, w io.Writer, params []any) (err error) {
err = tmpl.render(elem, w, params)
return
@@ -255,27 +206,16 @@ func (tmpl Template) JawsRender(elem *jaws.Element, w io.Writer, params []any) (
// JawsUpdate re-renders the Template into its wrapper.
//
-// Unwrapped templates have no generated DOM element to update, so updates are
-// ignored; nested JaWS UI rendered by the template can still update through its own
-// elements. The wrapper's SetInner is queued only after execution succeeds (see the
-// best-effort error behavior on [Template]).
-//
-// A successful update unregisters the tracked Elements from the previous execution,
-// along with any they own: SetInner replaces the DOM that held them. If execution
-// fails nothing is queued, so the tracked Elements it created are unregistered
-// instead and the previous ones stay live to match the unchanged DOM. See [Template]
-// for which Elements are tracked.
+// An empty OuterHTMLTag has no DOM target, so the update is a no-op. Otherwise
+// elem must have been rendered by an equal Template value; using an unequal value
+// is unsupported. After a successful lookup, missing Template state reports
+// [ErrElementStateUnclaimed].
//
-// A wrapped Template updates only an Element rendered by a Template value equal under ==
-// (see [jaws.SetElementState]); using an unequal value for the update is unsupported. With
-// no claim there is nothing to reconcile against, so it executes nothing and reports
-// [ErrElementStateUnclaimed]. That makes a wrapped Template unusable as a
-// [RequestWriter.Register] updater, since RequestWriter.Register never invokes the
-// updater's [Template.JawsRender]. Lookup happens first, so a missing template reports
-// [ErrMissingTemplate] and the missing-claim diagnostic is reached only after a
-// successful lookup.
+// On success, JawsUpdate replaces the wrapper content and unregisters Elements
+// from the previous execution. On execution failure, it keeps the previous DOM and
+// Elements and unregisters Elements created by the failed attempt.
//
-// Lookup, missing-state or execution errors are reported through
+// Lookup, state, and execution errors are reported through
// [jaws.Request.MustLog], which may panic when no [jaws.Jaws.Logger] is configured.
func (tmpl Template) JawsUpdate(elem *jaws.Element) {
if tmpl.OuterHTMLTag != "" {
@@ -332,26 +272,29 @@ func (tmpl Template) JawsInput(elem *jaws.Element, value string) (err error) {
// exposed as [With.Dot].
//
// outerHTMLTag names the generated wrapper element that owns the JaWS ID and
-// render-time HTML attributes, or renders unwrapped (and [Template.JawsUpdate] has no
-// wrapper to update) if empty. The name is resolved at render or update time via
-// [jaws.Jaws.LookupTemplate]. See [Template] for the field semantics, event
-// delegation and best-effort error behavior.
+// render-time HTML attributes. If outerHTMLTag is empty, "div" is used. Choose a tag
+// suitable for the DOM context. For an unwrapped fragment, use html/template's native
+// {{template "name" pipeline}} action. The name is resolved at render and update time.
//
-// The returned Template holds no per-Element state, so equal Templates are
-// interchangeable and a container that rebuilds its children on every
-// [jaws.Container.JawsContains] call still reuses their Elements. dot may be a nil
-// interface, which contributes no tag. Otherwise it must be comparable at runtime, equal
-// to itself and usable as a tag because rendering expands it through
-// [github.com/linkdata/jaws/lib/tag.TagExpand]. See [Template] for the exact rejected
-// dynamic types; the rules distinguish aliases from new defined types. Use the returned
-// Template as a value; taking its address is unsupported.
+// dot may be a nil interface. Otherwise it must make the returned Template
+// comparable and equal to itself, and it must be usable as a tag under
+// [tag.TagExpand]. Use the returned Template as a value; taking its address is
+// unsupported.
func NewTemplate(outerHTMLTag, name string, dot any) Template {
+ if outerHTMLTag == "" {
+ outerHTMLTag = "div"
+ }
+ return newTemplate(outerHTMLTag, name, dot)
+}
+
+func newTemplate(outerHTMLTag, name string, dot any) Template {
return Template{OuterHTMLTag: outerHTMLTag, Name: name, Dot: dot}
}
-// Template renders the named partial template with dot exposed as [With.Dot],
-// wrapping the output in a generated outerHTMLTag element (unwrapped if empty) that
-// owns the JaWS ID and any HTML attrs in params. See [NewTemplate] and [Template].
+// Template renders the named partial template with dot exposed as [With.Dot].
+//
+// The generated outerHTMLTag wrapper owns the JaWS ID and HTML attributes in
+// params. An empty outerHTMLTag defaults to "div". See [NewTemplate].
func (rw RequestWriter) Template(outerHTMLTag, name string, dot any, params ...any) error {
return rw.NewUI(NewTemplate(outerHTMLTag, name, dot), params...)
}
diff --git a/lib/ui/template_handler_test.go b/lib/ui/template_handler_test.go
index 809285c4..cccce317 100644
--- a/lib/ui/template_handler_test.go
+++ b/lib/ui/template_handler_test.go
@@ -178,8 +178,10 @@ func TestTemplate_RenderUpdateEventAndHelpers(t *testing.T) {
func TestTemplate_RenderWithTableRowWrapper(t *testing.T) {
jw, rq := newCoreRequest(t)
+ // The native template action includes the structural td fragment without another
+ // JaWS wrapper; the managed row Template supplies the one addressable tr.
_ = jw.AddTemplateLookuper(template.Must(template.New("row").Parse(
- `
{{.Dot}} | `,
+ `{{template "cell" .}}{{define "cell"}}
{{.Dot}} | {{end}}`,
)))
var sb bytes.Buffer
@@ -195,23 +197,45 @@ func TestTemplate_RenderWithTableRowWrapper(t *testing.T) {
}
}
-func TestTemplate_RenderWithoutWrapper(t *testing.T) {
+func TestTemplate_RenderWithDefaultWrapper(t *testing.T) {
jw, rq := newCoreRequest(t)
_ = jw.AddTemplateLookuper(template.Must(template.New("bare").Parse(
- `
{{.Dot}} | `,
+ `
{{.Dot}}`,
)))
var sb bytes.Buffer
rw := RequestWriter{Request: rq, Writer: &sb}
- if err := rw.Template("", "bare", tag.Tag("cell"), `class="ignored"`); err != nil {
+ dot := tag.Tag("cell")
+ if err := rw.Template("", "bare", dot, `class="defaulted"`); err != nil {
t.Fatal(err)
}
- got := sb.String()
- if got != `
cell | ` {
- t.Fatalf("unexpected unwrapped template output: %q", got)
+ elems := rq.GetElements(dot)
+ if len(elems) != 1 {
+ t.Fatalf("elements tagged with %q = %d, want 1", dot, len(elems))
}
- if strings.Contains(got, "Jid.") {
- t.Fatalf("unwrapped template should not contain generated wrapper markers: %q", got)
+ want := `
cell
`
+ if got := sb.String(); got != want {
+ t.Fatalf("default-wrapped template output = %q, want %q", got, want)
+ }
+}
+
+// TestTemplate_DirectEmptyWrapperRendersUnwrapped preserves the raw path used by
+// private construction and the zero value. Public callers using NewTemplate or
+// RequestWriter.Template get the default wrapper covered above.
+func TestTemplate_DirectEmptyWrapperRendersUnwrapped(t *testing.T) {
+ jw, rq := newCoreRequest(t)
+ _ = jw.AddTemplateLookuper(template.Must(template.New("bare").Parse(
+ `
{{.Dot}}`,
+ )))
+
+ var sb bytes.Buffer
+ rw := RequestWriter{Request: rq, Writer: &sb}
+ tmpl := newTemplate("", "bare", tag.Tag("cell"))
+ if err := rw.NewUI(tmpl, `class="ignored"`); err != nil {
+ t.Fatal(err)
+ }
+ if got, want := sb.String(), `
cell`; got != want {
+ t.Fatalf("direct empty-wrapper output = %q, want %q", got, want)
}
}
@@ -388,17 +412,10 @@ func TestTemplate_UpdateLogsMissingTemplate(t *testing.T) {
}
}
-func TestTemplate_UpdateWithoutWrapperNoop(t *testing.T) {
- jw, rq := newCoreRequest(t)
- logger := new(templateLogger)
- jw.Logger = logger
-
- tpl := NewTemplate("", "missingtemplate", tag.Tag("dot"))
- elem := rq.NewElement(tpl)
- tpl.JawsUpdate(elem)
-
- if len(logger.errors) != 0 {
- t.Fatalf("logged errors = %d, want 0", len(logger.errors))
+func TestNewTemplate_EmptyWrapperDefaultsToDiv(t *testing.T) {
+ tpl := NewTemplate("", "partial", tag.Tag("dot"))
+ if tpl.OuterHTMLTag != "div" {
+ t.Fatalf("OuterHTMLTag = %q, want %q", tpl.OuterHTMLTag, "div")
}
}
diff --git a/lib/ui/template_owned_benchmark_test.go b/lib/ui/template_owned_benchmark_test.go
index 6b51a847..9d7dc1a8 100644
--- a/lib/ui/template_owned_benchmark_test.go
+++ b/lib/ui/template_owned_benchmark_test.go
@@ -12,11 +12,11 @@ import (
"github.com/linkdata/jaws/lib/tag"
)
-// benchOwnedTemplates renders a wrapped template containing many unwrapped nested
-// templates, the shape that makes a template update replace a whole generation of
-// Elements at once.
+// benchOwnedTemplates renders a wrapped template containing many nested templates,
+// the shape that makes a template update replace a whole generation of Elements at
+// once.
const benchOwnedTemplates = `
-{{define "bench-parent"}}{{range $.Dot.Names}}{{$.RequestWriter.Template "" "bench-leaf" $.Dot}}{{end}}{{end}}
+{{define "bench-parent"}}{{range $.Dot.Names}}{{$.RequestWriter.Template "div" "bench-leaf" $.Dot}}{{end}}{{end}}
{{define "bench-leaf"}}leaf{{end}}
`
diff --git a/lib/ui/template_owned_test.go b/lib/ui/template_owned_test.go
index 3ac81365..222b63c5 100644
--- a/lib/ui/template_owned_test.go
+++ b/lib/ui/template_owned_test.go
@@ -21,11 +21,11 @@ import (
// The templates used by the ownership tests. Every nested helper passes $.Dot along,
// so one dot tags the whole subtree and GetElements(dot) counts it.
const ownedTestTemplates = `
-{{define "owned-parent"}}{{$.RequestWriter.Template "" "owned-leaf" $.Dot}}{{end}}
+{{define "owned-parent"}}{{$.RequestWriter.Template "div" "owned-leaf" $.Dot}}{{end}}
{{define "owned-leaf"}}leaf{{end}}
-{{define "owned-deep"}}{{$.RequestWriter.Template "" "owned-parent" $.Dot}}{{end}}
-{{define "owned-failafter"}}{{$.RequestWriter.Template "" "owned-leaf" $.Dot}}{{$.Dot.Check}}{{end}}
-{{define "owned-many"}}{{range $.Dot.Names}}{{$.RequestWriter.Template "" "owned-leaf" $.Dot}}{{end}}{{end}}
+{{define "owned-deep"}}{{$.RequestWriter.Template "div" "owned-parent" $.Dot}}{{end}}
+{{define "owned-failafter"}}{{$.RequestWriter.Template "div" "owned-leaf" $.Dot}}{{$.Dot.Check}}{{end}}
+{{define "owned-many"}}{{range $.Dot.Names}}{{$.RequestWriter.Template "div" "owned-leaf" $.Dot}}{{end}}{{end}}
{{define "owned-container"}}{{$.RequestWriter.Container "div" $.Dot.Container}}{{end}}
{{define "owned-register"}}
{{end}}
{{define "owned-radiogroup"}}{{range $.RequestWriter.RadioGroup $.Dot.Radios}}{{.Radio}}{{.Label}}{{end}}{{end}}
@@ -120,8 +120,8 @@ func newOwnedRequest(t *testing.T) (*jaws.Jaws, *jaws.Request) {
const maxProbedJid = jaws.Jid(500)
// countRegistered returns how many Elements are still registered in rq, probing the
-// Jid space directly so Elements carrying no tag of their own (unwrapped nested
-// templates, container children) are counted too.
+// Jid space directly so Elements carrying no unique tag of their own (for example,
+// container children) are counted too.
func countRegistered(t *testing.T, rq *jaws.Request) (count int) {
t.Helper()
for jid := jaws.Jid(1); jid <= maxProbedJid; jid++ {
@@ -143,10 +143,9 @@ func renderOwned(t *testing.T, rq *jaws.Request, ui jaws.UI) *jaws.Element {
return elem
}
-// TestTemplate_NestedUnwrappedDoesNotLeakOnUpdate is the reported issue (#216): a
-// wrapped template invoking an unwrapped nested one must not register an extra
-// Element on every update.
-func TestTemplate_NestedUnwrappedDoesNotLeakOnUpdate(t *testing.T) {
+// TestTemplate_NestedTemplateDoesNotLeakOnUpdate verifies that a template invoking a
+// nested one does not register an extra Element on every update.
+func TestTemplate_NestedTemplateDoesNotLeakOnUpdate(t *testing.T) {
jw, err := jaws.New()
if err != nil {
t.Fatal(err)
@@ -392,7 +391,7 @@ func TestRequestWriter_NewUIReportsElementBeforeRendering(t *testing.T) {
}
// TestTemplate_UpdateReclaimsWholeSubtree covers the recursive walk: the wrapper
-// owns a nested unwrapped template that owns another one.
+// owns a nested template that owns another one.
func TestTemplate_UpdateReclaimsWholeSubtree(t *testing.T) {
_, rq := newOwnedRequest(t)
diff --git a/lib/ui/template_register_test.go b/lib/ui/template_register_test.go
index 9f07797a..371a12cf 100644
--- a/lib/ui/template_register_test.go
+++ b/lib/ui/template_register_test.go
@@ -4,7 +4,6 @@ import (
"errors"
"html/template"
"strings"
- "sync/atomic"
"testing"
"time"
@@ -36,11 +35,11 @@ func newRegisterRequest(t *testing.T, logger *templateLogger) (*jaws.Jaws, *jaws
return jw, rq
}
-// TestRegister_WrappedTemplateUpdaterReportsUnclaimed checks every reporting path for a
-// wrapped Template updater. RequestWriter.Register never calls the Template's renderer,
+// TestRegister_TemplateUpdaterReportsUnclaimed checks every reporting path for a
+// Template updater. RequestWriter.Register never calls the Template's renderer,
// so the Template has no state claim to update against.
-func TestRegister_WrappedTemplateUpdaterReportsUnclaimed(t *testing.T) {
- wrapped := NewTemplate("div", "reg-plain", tag.Tag("dot"))
+func TestRegister_TemplateUpdaterReportsUnclaimed(t *testing.T) {
+ tmpl := NewTemplate("div", "reg-plain", tag.Tag("dot"))
t.Run("direct call logs", func(t *testing.T) {
logger := new(templateLogger)
@@ -48,7 +47,7 @@ func TestRegister_WrappedTemplateUpdaterReportsUnclaimed(t *testing.T) {
var sb strings.Builder
rw := RequestWriter{Request: rq, Writer: &sb}
- if jid := rw.Register(wrapped); !jid.IsValid() {
+ if jid := rw.Register(tmpl); !jid.IsValid() {
t.Fatal("expected a valid Jid even though the update failed")
}
if len(logger.errors) != 1 || !errors.Is(logger.errors[0], ErrElementStateUnclaimed) {
@@ -67,7 +66,7 @@ func TestRegister_WrappedTemplateUpdaterReportsUnclaimed(t *testing.T) {
// Register runs the update immediately, so MustLog's panic escapes to the caller.
recovered := func() (recovered any) {
defer func() { recovered = recover() }()
- rw.Register(wrapped)
+ rw.Register(tmpl)
return
}()
if err, ok := recovered.(error); !ok || !errors.Is(err, ErrElementStateUnclaimed) {
@@ -86,7 +85,7 @@ func TestRegister_WrappedTemplateUpdaterReportsUnclaimed(t *testing.T) {
// html/template recovers a panic raised by a called method and returns it as an
// execution error, so this surfaces as a render error rather than escaping. The
// wrapping preserves the sentinel, so match on that rather than on the text.
- err := rw.Template("div", "reg-page", ®isterDot{updater: wrapped})
+ err := rw.Template("div", "reg-page", ®isterDot{updater: tmpl})
if err == nil {
t.Fatal("expected a render error from the recovered panic")
}
@@ -101,7 +100,7 @@ func TestRegister_WrappedTemplateUpdaterReportsUnclaimed(t *testing.T) {
var sb strings.Builder
rw := RequestWriter{Request: rq, Writer: &sb}
- if err := rw.Template("div", "reg-page", ®isterDot{updater: wrapped}); err != nil {
+ if err := rw.Template("div", "reg-page", ®isterDot{updater: tmpl}); err != nil {
t.Fatalf("render = %v, want nil: the diagnostic is logged, not fatal", err)
}
if !strings.Contains(sb.String(), "
") {
@@ -113,10 +112,10 @@ func TestRegister_WrappedTemplateUpdaterReportsUnclaimed(t *testing.T) {
})
}
-// TestRegister_WrappedTemplateUpdaterOnTheRequestLoop reaches the diagnostic from the
+// TestRegister_TemplateUpdaterOnTheRequestLoop reaches the diagnostic from the
// request loop, which RequestWriter.Register cannot do because it updates immediately: a
-// NewRegister child is rendered with a tag, then that tag is dirtied.
-func TestRegister_WrappedTemplateUpdaterOnTheRequestLoop(t *testing.T) {
+// registerUI child is rendered with a tag, then that tag is dirtied.
+func TestRegister_TemplateUpdaterOnTheRequestLoop(t *testing.T) {
for _, tt := range []struct {
name string
withLog bool
@@ -150,12 +149,12 @@ func TestRegister_WrappedTemplateUpdaterOnTheRequestLoop(t *testing.T) {
<-tr.ReadyCh
dirty := tag.Tag("registered")
- wrapped := NewTemplate("div", "reg-plain", tag.Tag("dot"))
- // Build the Register Element by hand: RequestWriter.Register would run the
- // failing update immediately, and Register.JawsRender documents that it ignores
- // params, so NewUI would not apply the tag. This leaves the first failing update
- // to the request loop.
- regElem := tr.NewElement(NewRegister(wrapped))
+ tmpl := NewTemplate("div", "reg-plain", tag.Tag("dot"))
+ // Build the registered Element by hand: RequestWriter.Register would run the
+ // failing update immediately, and registerUI.JawsRender ignores params, so
+ // NewUI would not apply the tag. This leaves the first failing update to the
+ // request loop.
+ regElem := tr.NewElement(registerUI{Updater: tmpl})
regElem.Tag(dirty)
regElem.Freeze()
@@ -196,63 +195,3 @@ func TestRegister_WrappedTemplateUpdaterOnTheRequestLoop(t *testing.T) {
})
}
}
-
-// TestRegister_UnwrappedTemplateUpdaterStaysUsable covers the other half of the narrowing:
-// an unwrapped Template remains a valid Register updater because its updates are a
-// documented no-op — but only RequestWriter.Register also delivers its event handlers,
-// since Register embeds jaws.Updater and therefore promotes no handler methods.
-func TestRegister_UnwrappedTemplateUpdaterStaysUsable(t *testing.T) {
- dot := &clickCountingDot{}
- unwrapped := NewTemplate("", "reg-plain", dot)
-
- // The UI value itself delegates nothing: Register embeds jaws.Updater, whose method set
- // is JawsUpdate alone, so a Template's handler methods are never promoted onto it.
- if _, ok := any(NewRegister(unwrapped)).(jaws.ClickHandler); ok {
- t.Fatal("Register promoted a ClickHandler; it embeds only jaws.Updater")
- }
-
- jw, err := jaws.New()
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(jw.Close)
- logger := new(templateLogger)
- jw.Logger = logger
- if err = jw.AddTemplateLookuper(template.Must(template.New("reg-plain").Parse(`plain`))); err != nil {
- t.Fatal(err)
- }
- go jw.Serve()
- tr := jawstest.NewTestRequest(jw, nil)
- t.Cleanup(func() {
- tr.Close()
- <-tr.DoneCh
- })
- <-tr.ReadyCh
-
- var sb strings.Builder
- rw := RequestWriter{Request: tr.Request, Writer: &sb}
- jid := rw.Register(unwrapped)
- if len(logger.errors) != 0 {
- t.Fatalf("logged errors = %v, want none for an unwrapped Template", logger.errors)
- }
-
- // RequestWriter.Register added the concrete updater to the Element's handler list, so a
- // browser event reaches the Template and through it the Dot.
- // Click data is "X Y kstate name"; a bare name does not parse.
- tr.InCh <- wire.WsMsg{Jid: jid, What: what.Click, Data: "1 2 0 btn"}
- deadline := time.Now().Add(2 * time.Second)
- for dot.clicks.Load() == 0 && time.Now().Before(deadline) {
- time.Sleep(time.Millisecond)
- }
- if got := dot.clicks.Load(); got != 1 {
- t.Fatalf("clicks delivered through the handler list = %d, want 1", got)
- }
-}
-
-// clickCountingDot counts clicks delivered through a Template's event delegation.
-type clickCountingDot struct{ clicks atomic.Int32 }
-
-func (d *clickCountingDot) JawsClick(*jaws.Element, jaws.Click) error {
- d.clicks.Add(1)
- return nil
-}
diff --git a/lib/ui/template_state_test.go b/lib/ui/template_state_test.go
index 872e88db..6ef30a4c 100644
--- a/lib/ui/template_state_test.go
+++ b/lib/ui/template_state_test.go
@@ -5,6 +5,7 @@ import (
"html/template"
"io"
"strings"
+ "sync/atomic"
"testing"
"time"
@@ -17,14 +18,22 @@ import (
)
const templateStateTemplates = `
-{{define "state-parent"}}{{$.RequestWriter.Template "" "state-leaf" $.Dot}}{{end}}
+{{define "state-parent"}}{{$.RequestWriter.Template "div" "state-leaf" $.Dot}}{{end}}
{{define "state-leaf"}}leaf{{end}}
-{{define "state-failafter"}}{{$.RequestWriter.Template "" "state-leaf" $.Dot}}{{$.Dot.Check}}{{end}}
+{{define "state-failafter"}}{{$.RequestWriter.Template "div" "state-leaf" $.Dot}}{{$.Dot.Check}}{{end}}
{{define "state-plain"}}plain{{end}}
{{define "state-b"}}b{{end}}
{{define "state-span"}}{{$.RequestWriter.Span "x"}}{{end}}
`
+// clickCountingDot counts clicks delivered through a Template's event delegation.
+type clickCountingDot struct{ clicks atomic.Int32 }
+
+func (d *clickCountingDot) JawsClick(*jaws.Element, jaws.Click) error {
+ d.clicks.Add(1)
+ return nil
+}
+
func newStateRequest(t *testing.T) (*jaws.Jaws, *jaws.Request) {
t.Helper()
return newConfiguredStateRequest(t, nil)
@@ -397,12 +406,12 @@ func TestTemplate_UpdateToleratesTypedNilContainerState(t *testing.T) {
}
}
-// TestTemplate_UnwrappedUpdateStaysSilent pins the path the missing-claim diagnostic does
-// not apply to: an unwrapped Template returns before the state slot is consulted.
-func TestTemplate_UnwrappedUpdateStaysSilent(t *testing.T) {
+// TestTemplate_ZeroValueUpdateStaysSilent preserves the zero value's update behavior: it
+// has no wrapper target, so it returns before consulting the state slot.
+func TestTemplate_ZeroValueUpdateStaysSilent(t *testing.T) {
_, rq := newStateRequest(t)
// No logger is configured, so MustLog would panic if this reported anything.
- tmpl := NewTemplate("", "state-plain", tag.Tag("dot"))
+ var tmpl Template
tmpl.JawsUpdate(rq.NewElement(tmpl))
}