Skip to content
Closed
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
10 changes: 10 additions & 0 deletions .agents/skills/jaws/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ These are the two usual building blocks for widget handlers passed to `$.Button`
- `.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.
- Writable setters used with `ui.NewText`, `ui.NewPassword`, `ui.NewTextarea`,
`ui.NewCheckbox`, `ui.NewRadio`, `ui.NewNumber`, `ui.NewRange`, and `ui.NewDate`
must give `Element.ApplyGetter` a dirty target that successfully expands to at
least one stable usable key. Prefer `bind.New`; otherwise use a pointer-valued
custom setter or implement `JawsGetTag`. A `JawsGetTag` result takes precedence
over the setter's own identity.
- Input widgets retain only that setter-derived target for post-set dirtying. An
explicit render-param tag registers the Element but does not substitute for it;
without the setter-derived target, rejected or normalized browser values are not
automatically reconciled.

### When to use which

Expand Down
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,71 @@ Since all data access need to be protected with locks, you will usually use `bin
that combines a (RW)Locker and a pointer to a value of type `T`. It also allows you to add chained setters,
getters and on-success handlers.

Writable setters used with `ui.NewText`, `ui.NewPassword`, `ui.NewTextarea`,
`ui.NewCheckbox`, `ui.NewRadio`, `ui.NewNumber`, `ui.NewRange`, and `ui.NewDate`
must also provide a stable dirty target. During rendering, the widget retains the
target derived from the setter. After each set result that does not match
`jaws.ErrValueUnchanged`, it dirties that target so the authoritative server
value can reconcile rejected or normalized browser input. `bind.New(&mu, &value)`
is the usual choice because it exposes the backing pointer. A custom setter can
instead be passed as a stable pointer value or implement `JawsGetTag` and return a
target that successfully expands to at least one stable usable key. When a setter
implements `JawsGetTag`, that result takes precedence over the setter's own
identity.

For example, this validating setter contains a slice and is not comparable as a
value, so `JawsGetTag` exposes its backing value pointer:

```go
var errValueNotAllowed = errors.New("value not allowed")

type validatedText struct {
mu *sync.RWMutex
value *string
allowed []string // immutable
}

func (s validatedText) JawsGet(*jaws.Element) (value string) {
s.mu.RLock()
value = *s.value
s.mu.RUnlock()
return
}

func (s validatedText) JawsSet(_ *jaws.Element, value string) (err error) {
if !slices.Contains(s.allowed, value) {
err = errValueNotAllowed
return
}
s.mu.Lock()
if *s.value == value {
err = jaws.ErrValueUnchanged
} else {
*s.value = value
}
s.mu.Unlock()
return
}

func (s validatedText) JawsGetTag() any { return s.value }

func newStatusInput(mu *sync.RWMutex, value *string) *ui.Text {
return ui.NewText(validatedText{
mu: mu,
value: value,
allowed: []string{"draft", "published"},
})
}
```

Without a setter-derived dirty target, input events still reach `JawsSet`, but a
rejected value can remain visible in the browser and a normalized accepted value
is not reflected automatically. An explicit tag passed as a widget rendering
parameter does not substitute for the setter-derived target: it registers the
Element for external dirtying, but the input does not retain it for post-set
dirtying. See [`ui.Input`](https://pkg.go.dev/github.com/linkdata/jaws/lib/ui#Input)
for the complete contract.

### Session handling

JaWS has non-persistent session handling integrated. Sessions won't
Expand Down
4 changes: 3 additions & 1 deletion lib/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@ Each base handles:

- tracking last rendered value
- receiving `what.Input`
- applying dirty tags on successful set
- retaining the setter-derived tag for reconciliation after every set result
that does not match `jaws.ErrValueUnchanged`; see
[`Input`](https://pkg.go.dev/github.com/linkdata/jaws/lib/ui#Input)
- update-driven `SetValue` pushes

## Adding a container widget
Expand Down
2 changes: 2 additions & 0 deletions lib/ui/checkbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
type Checkbox struct{ InputBool }

// NewCheckbox returns a checkbox input widget bound to g.
//
// g must meet the post-set dirty-tag requirement documented by [Input].
func NewCheckbox(g bind.Setter[bool]) *Checkbox { return &Checkbox{InputBool{Setter: g}} }

// JawsRender renders ui as an HTML checkbox input.
Expand Down
2 changes: 2 additions & 0 deletions lib/ui/date.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type Date struct{ InputDate }

// NewDate returns a date input widget bound to g.
//
// g must meet the post-set dirty-tag requirement documented by [Input].
//
// The widget is date-only; see [InputDate.JawsInput] for how a browser edit
// normalizes the bound [time.Time] to midnight UTC and which years round-trip.
func NewDate(g bind.Setter[time.Time]) *Date { return &Date{InputDate{Setter: g}} }
Expand Down
18 changes: 18 additions & 0 deletions lib/ui/input_widgets.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ import (
// [jaws.Element]. A widget embedding Input must therefore back at most one live
// Element. To render the same bound state more than once, construct distinct
// widgets that share the setter.
//
// During rendering, Input retains the dirty target [jaws.Element.ApplyGetter]
// derives from the bound setter. After [bind.Setter.JawsSet] returns a result that
// does not match [jaws.ErrValueUnchanged], Input dirties that target so an update
// can read the server value back and reconcile the browser. A writable setter must
// therefore produce a target that expands to at least one stable usable key. If
// the setter implements [github.com/linkdata/jaws/lib/tag.TagGetter], its
// JawsGetTag result is used; otherwise the setter's dynamic value must be
// comparable at runtime and equal to itself, typically a pointer. The selected
// target must expand successfully through
// [github.com/linkdata/jaws/lib/tag.TagExpand]. [bind.New] provides the backing
// pointer as its target.
//
// If rendering registers no setter-derived keys, browser input events still reach
// JawsSet, but a rejected or normalized value is not automatically reconciled and
// can remain visible while server state differs. A tag supplied separately in
// render params registers the Element for that tag, but Input does not retain it
// as its post-set dirty target.
type Input struct {
// tag is the dirty tag, written once during render and read on the event
// goroutine (JawsInput). The render-completes-before-events lifecycle makes
Expand Down
2 changes: 2 additions & 0 deletions lib/ui/number.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type Number struct{ InputFloat }

// NewNumber returns a number input widget bound to g.
//
// g must meet the post-set dirty-tag requirement documented by [Input].
//
// The bound value must be finite. A non-finite value (NaN or ±Inf) has no valid
// rendering or wire representation, so rendering, updating, or receiving one from
// the browser cancels the [jaws.Request] with a cause wrapping
Expand Down
2 changes: 2 additions & 0 deletions lib/ui/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
type Password struct{ InputText }

// NewPassword returns a password input widget bound to g.
//
// g must meet the post-set dirty-tag requirement documented by [Input].
func NewPassword(g bind.Setter[string]) *Password { return &Password{InputText{Setter: g}} }

// JawsRender renders ui as an HTML password input.
Expand Down
2 changes: 2 additions & 0 deletions lib/ui/radio.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
type Radio struct{ InputBool }

// NewRadio returns a radio input widget bound to g.
//
// g must meet the post-set dirty-tag requirement documented by [Input].
func NewRadio(g bind.Setter[bool]) *Radio { return &Radio{InputBool{Setter: g}} }

// JawsRender renders ui as an HTML radio input.
Expand Down
2 changes: 2 additions & 0 deletions lib/ui/range.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type Range struct{ InputFloat }

// NewRange returns a range input widget bound to g.
//
// g must meet the post-set dirty-tag requirement documented by [Input].
//
// The bound value must be finite. A non-finite value (NaN or ±Inf) has no valid
// rendering or wire representation, so rendering, updating, or receiving one from
// the browser cancels the [jaws.Request] with a cause wrapping
Expand Down
2 changes: 2 additions & 0 deletions lib/ui/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
type Text struct{ InputText }

// NewText returns a text input widget bound to g.
//
// g must meet the post-set dirty-tag requirement documented by [Input].
func NewText(g bind.Setter[string]) *Text { return &Text{InputText{Setter: g}} }

// JawsRender renders ui as an HTML text input.
Expand Down
2 changes: 2 additions & 0 deletions lib/ui/textarea.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
type Textarea struct{ InputText }

// NewTextarea returns a textarea widget bound to g.
//
// g must meet the post-set dirty-tag requirement documented by [Input].
func NewTextarea(g bind.Setter[string]) *Textarea { return &Textarea{InputText{Setter: g}} }

// JawsRender renders ui as an HTML textarea.
Expand Down