Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 49 additions & 14 deletions .agents/skills/jaws/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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`
Expand All @@ -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
<section id="{{$.Register .Dot.Panel}}">...</section>
```

- 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:
Expand Down Expand Up @@ -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.
Expand All @@ -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`.
Expand Down Expand Up @@ -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.
15 changes: 7 additions & 8 deletions contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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
}

Expand Down
121 changes: 58 additions & 63 deletions lib/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<section id="{{$.Register .Dot.Panel}}" class="panel">
template-authored content
</section>
```

`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

Expand All @@ -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
Expand All @@ -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:

Expand Down
6 changes: 4 additions & 2 deletions lib/ui/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading