| name | jaws |
| description | Use this skill when implementing or refactoring server-driven UI with github.com/linkdata/jaws, including templates, handlers, dirtying, tags, sessions, and render/update behavior. |
| metadata | {"short-description":"Build apps with JaWS"} |
When to apply this skill
Apply these rules whenever work involves any of the following:
- Go code that creates JaWS
UI values, binders, handlers, requests, or sessions
- Go templates rendered through
ui.Template / $.Template
- Event handling, dirtying, tag identity, or dynamic container updates
Primary objective
Keep browser behavior thin and deterministic while preserving server-side truth, stable identity, and predictable rerenders.
Framework mindset
JaWS is an immediate-mode, server-driven UI framework, not an MVC framework.
- Treat render output as a direct projection of current server state.
- Keep authoritative state in domain data, not duplicated in UI-specific state layers.
- Use tags to express data dependencies so rerenders are targeted and deterministic.
Practical data/tag alignment
- Model interactive units as first-class objects and keep related behavior on those objects where practical.
- Prefer direct pointer tags to underlying data (for example
*Item, *Node, &state.field) when identity is stable. bind.New(&mu, &field) follows this pattern automatically: its tag is always &field, unchanged by any chained hooks.
- Use getter-based values (
bind.StringGetterFunc / bind.HTMLGetterFunc) for UI text/HTML that must reflect changing server state.
- Dirty only affected dependency tags after mutations, and include any derived-field dependencies that changed.
- Avoid synthetic tags (coordinates, ad-hoc strings, wrappers) when a stable underlying data pointer exists.
- For collection elements, register each element with both its item-level tag (the item's pointer) and a shared group tag (for example
&g.items). Mutations can then dirty a single item, several items, or the whole group from the same tag namespace, without changing what each element listens to.
- Keep an item's own
JawsGetTag scoped to the item, never returning the shared group tag. Element registration (what an element listens to) and dirty-target expansion (what Request.Dirty(x) resolves to) are different things. If an item type implements tag.TagGetter and its JawsGetTag returns []any{item, &group}, then Request.Dirty(item) tag-expands to include &group and re-renders every element registered under it — silently defeating per-item dirtying even though you passed a single item. Return only the item's own identity from JawsGetTag, and attach the shared group tag separately at construction (for example pass &group as an extra tag param to the item's widget). The element then listens to both, but Dirty(item) stays scoped to that one item while Dirty(&group) still refreshes the whole group.
Hard framework constraints
- Every JaWS
UI value must be comparable.
- 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,
handlers, and tags.
Container.JawsContains must return hashable UI items, and the returned slice must not be mutated after return.
- Treat
*ui.Container / *ui.Tbody / ContainerHelper widgets as request-scoped; construct fresh per request, do not cache across requests.
Constructing UI values: ui.New and bind.New
These are the two usual building blocks for widget handlers passed to $.Button, $.Span, $.A, $.Div, $.Label, $.Text, $.Range, etc. Pick based on what the widget represents: presentational content (ui.New) versus editable state backed by a variable (bind.New).
ui.New(innerHTML any) Object
innerHTML is routed through bind.MakeHTMLGetter, so the same type rules apply: string is raw HTML, template.HTML is trusted as-is, Getter[string] / Binder[string] / fmt.Stringer are escaped.
- Returns a chainable
Object. Each builder — .Clicked(fn), .ContextMenu(fn), .InitialHTMLAttr(fn) — wraps the previous object so the chain is a linked list, newest first.
- Event dispatch walks the chain newest-first and stops at the first link that does not return
jaws.ErrEventUnhandled. Return ErrEventUnhandled from a link to fall through to the next.
JawsInitialHTMLAttr concatenates attribute strings from every link that defines one.
JawsGetTag collects non-nil tags from every link; this is the tag set used by tag.TagExpand for dirty targeting.
- Use
ui.New for buttons, spans, labels, and similar content-plus-behavior widgets where the identity of the underlying state is not the point.
bind.New[T comparable](l sync.Locker, p *T) Binder[T]
- Signature:
func New[T comparable](l sync.Locker, p *T) Binder[T]. If l also satisfies RWLocker (has RLock/RUnlock), the binder takes the read lock for reads; otherwise it upgrades to the write lock.
- The binder's tag is always
p (the pointer itself). Chaining never changes tag identity — bind.New(&mu, &field).Clicked(...).Success(...) still reports &field as its tag, so dirty targeting via &field keeps working through refactors.
- Default
JawsSetLocked assigns *p = v only when the value changed and returns jaws.ErrValueUnchanged when it did not. This is what lets the input-widget family skip redundant updates.
- Chain builders return a new
Binder[T]:
.SetLocked(fn) / .GetLocked(fn) — override read/write semantics.
.GetHTML(fn) — supply a custom JawsGetHTML for HTML-rendering widgets (Span, Div, A, Label).
.Success(fn) — run after a successful set. Accepted signatures: func(), func() error, func(*Element), func(*Element) error. Non-nil errors propagate; a handler can still return ErrValueUnchanged to signal no change.
.Clicked(fn) / .ContextMenu(fn) — attach click/context handlers to the same bound variable.
.InitialHTMLAttr(fn) — attach attribute hooks.
- Use
bind.New for input widgets and for content whose natural key is the backing variable. Multiple widgets bound to the same pointer share a tag automatically, so Request.Dirty(&field) refreshes all of them.
When to use which
ui.New when the widget is presentational or event-driven and identity flows from the containing object (for example a cell that already has a *Cell tag registered as a dep).
bind.New when the widget reads or writes a specific variable and you want the variable's address to be the stable dirty target.
- Either form accepts additional tag arguments where the API allows it (
bind.HTMLGetterFunc(fn, deps...), bind.StringGetterFunc(fn, deps...)) so a single widget can depend on several pieces of state without wrapping types.
HTML-inner constructors
ui.NewDiv, ui.NewSpan, ui.NewButton, ui.NewA, ui.NewLabel, ui.NewTd, ui.NewTr, and ui.NewLi accept innerHTML any and call bind.MakeHTMLGetter internally.
- The matching
RequestWriter helpers ($.Div, $.Span, etc.) have the same content conversion rules because they call the constructors.
- Plain strings are trusted raw HTML at both levels. Use
bind.Getter[string], bind.StringGetterFunc, fmt.Stringer, or explicit escaping for untrusted text.
Template-dot and tag rules
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.
ui.Template is for partial templates only; full document/page templates should be rendered through ui.Handler.
- Prefer comparable root dots (pointers or small comparable structs).
- If root dot is non-comparable, implement
JawsGetTag(tag.Context) any and return a comparable tag.
- Do not use plain
string, numeric, bool, template.HTML, or template.HTMLAttr as tags; tag.TagExpand rejects them.
- If you need string-like semantic tags, use
tag.Tag("...") or a comparable typed struct/pointer.
$.Template(...) signature and parameter semantics
$.Template(...) takes the wrapper tag first:
{{$.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.
JaWS parses template params as:
- HTML attrs:
string, []string, template.HTMLAttr, []template.HTMLAttr
- Handlers:
InputFn (the func alias func(e *Element, val string) error), plus anything satisfying InputHandler, ClickHandler, or ContextMenuHandler
- Tags: everything else (plus comparable handlers are auto-tagged)
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.
- 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.
Event handling model
On incoming events, JaWS dispatches in this order:
- Handlers attached to the element are tried in reverse registration order (most recently added first).
- If every attached handler returned
ErrEventUnhandled (or none matched the event kind), the UI object itself (elem.UI()) is called last as the fallback.
The handler candidate is asked via JawsClick / JawsContextMenu / JawsInput, matched to the event kind; there is no generic JawsEvent method. Return jaws.ErrEventUnhandled to fall through to the next candidate.
Clickable template pattern
For clickable content rendering:
- Prefer a template dot with
JawsClick over passing redundant explicit click handlers.
- Use explicit click handler params only when dot-owned handling is not viable.
ui.Template creates the outer JaWS wrapper; template roots should not declare the JaWS ID or carry forwarded wrapper attributes.
- Add interaction semantics where needed through Template params, for example
role="button" and tabindex="0".
- Keep body partials presentational; attach behavior at wrapper/dot level.
Rendering and update rules
- Keep HTML structure in templates; avoid manual HTML string assembly in Go.
ui.Template.JawsUpdate re-renders the template data into the generated wrapper.
- 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.
- 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).
Element.SetAttr/RemoveAttr/SetClass/RemoveClass/SetInner/SetValue/Append/Order/Remove/Replace are update-time operations; call them only from render/update processing.
- Remember that widgets such as
ui.Button update inner HTML from the original getter object; if that getter captured a stale static value, dirtying will not refresh the UI.
Dirtying rules
- Prefer
Request.Dirty(...) when in request context.
- Avoid
Jaws.Dirty(...) unless necessary; its tag expansion runs with nil request context.
- Dirty only precise tags whose output depends on the changed state.
- Avoid broad model-level dirty tags when finer-grained element-level tags are practical.
- For broad refreshes, attach a shared dependency tag to all relevant elements and dirty that shared tag instead of enumerating many element tags.
Request.Dirty runs the tag list through tag.TagExpand, which has a hard cap of 100 expanded entries and returns tag.ErrTooManyTags above that. When a mutation might touch more items than that, prefer the shared group tag over enumerating individual item tags.
- Redundant-update filtering is asymmetric: input widgets (
InputText, InputBool, InputFloat, InputDate) compare the new getter output against a stored Last value and skip SetValue when unchanged, but HTMLInner-backed widgets (spans, divs, buttons) do not — JawsUpdate unconditionally calls SetInner. For HTML-inner widgets, ensure dirty scope matches fields that actually changed, otherwise unrelated status/label spans will re-render (and lose selection, transitions, etc.) on every event. Usually the mutation code already knows what it changed and can dirty accordingly; fall back to snapshot-and-diff only when outcomes are hard to predict up front (e.g. flood-fill or win-condition checks) and the snapshot is cheap.
HTML safety rules
bind.MakeHTMLGetter behavior is type-dependent:
string is used as raw HTML and is not escaped.
template.HTML is trusted as-is.
Getter[string], Binder[string], fmt.Stringer, and formatter-based paths are escaped.
Guideline:
- Never pass untrusted input as plain
string to HTML-producing helpers.
Request/session integration rules
- Ensure pages provide the configured JaWS resources and Request key metadata;
HeadHTML is the usual way to emit them.
TailHTML is optional; it applies queued attr/class updates before the
WebSocket connects and can reduce initial flicker.
- Register the JaWS
/jaws/ route prefix correctly and pair request creation with UseRequest handling.
- Session storage is server-side and IP-bound; use
Jaws.SessionMiddleware(...) when page state should be per-user.
- For per-session app state, load from
Request.Get(key) and initialize with Request.Set(key, value) during the page request.
Runtime/lifecycle cautions
- Start JaWS processing loop (
Serve/ServeWithTimeout) before relying on broadcast-driven APIs.
Broadcast-driven helpers (including session reload/close flows) may block before the serve loop is running.
Testing checklist
- Use real JaWS requests/elements for render/click/update tests.
- Add regression tests for click dispatch when moving handlers between params and dot
JawsClick.
- For container regressions, verify identity reuse, append/remove/order behavior, and stale-element cleanup.
- Add pure domain tests for state transitions (win/loss, reset, bounds checks) independent of JaWS transport.
- If rerendering fails, inspect tag comparability and dirty-target coverage before broadening dirty scope.
Anti-patterns
- Repo-specific abstractions that hide JaWS contracts instead of modeling them.
- Fake binders or fake tags created only to satisfy an API shape.
- Hidden mutations in getter paths.
- Broad
Dirty(...) calls used to mask incorrect dependency targeting.
- 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.