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
8 changes: 8 additions & 0 deletions .agents/skills/jaws/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ 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 by `ui.Input`-based widgets require a stable target
accepted by `Element.ApplyGetter` for post-set reconciliation. `bind.New`
supplies its backing pointer; otherwise use a pointer-valued setter or
`JawsGetTag` returning at least one stable, usable key. A `JawsGetTag` result
takes precedence over the setter's identity.
- Render-param tags register the Element but do not replace that setter-derived
target. Without a valid target, rejected or normalized browser values are not
automatically reconciled.

### When to use which

Expand Down
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,55 @@ 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`
need a stable dirty target derived from the setter. After each `JawsSet` result
that does not match `jaws.ErrValueUnchanged`, the widget dirties that target so
the server value can reconcile rejected or normalized browser input.
`bind.New(&mu, &value)` exposes the backing pointer. A custom setter can instead
be pointer-valued or implement `JawsGetTag` and return a target that expands to
at least one stable, usable key; `JawsGetTag` takes precedence over the setter's
identity. Tags passed as render parameters register the Element but do not
replace its setter-derived dirty target.

This validator rejects forbidden username characters while accepting ordinary
incremental edits. Its slice makes the setter value non-comparable, so
`JawsGetTag` exposes the backing pointer:

```go
type validatedText struct {
bind.Setter[string]
dirtyTag *string
forbidden []rune // immutable
}

func (s validatedText) JawsSet(elem *jaws.Element, value string) (err error) {
for _, r := range value {
if slices.Contains(s.forbidden, r) {
err = errors.New("value contains a forbidden character")
return
}
}
err = s.Setter.JawsSet(elem, value)
return
}

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

func newUsernameInput(mu *sync.RWMutex, value *string) *ui.Text {
return ui.NewText(validatedText{
Setter: bind.New(mu, value),
dirtyTag: value,
forbidden: []rune{' ', '/', '\\'},
})
}
```

Without a valid setter-derived target, rejected or normalized browser values are
not automatically reconciled. 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 @@ -247,7 +247,9 @@ Each base handles:

- tracking last rendered value
- receiving `what.Input`
- applying dirty tags on successful set
- retaining and dirtying the setter-derived tag after any set result other than
`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.
//
// For writable use, g must provide the setter-derived dirty target described 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.
//
// For writable use, g must provide the setter-derived dirty target described 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
12 changes: 11 additions & 1 deletion lib/ui/input_widgets.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,20 @@ import (

// Input stores common state for interactive input widgets.
//
// An Input value retains the last browser value and dirty tag for one live
// An Input value retains the last browser value and dirty target for one live
// [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.
//
// For post-set reconciliation, a writable setter must expose at least one stable,
// usable tag through [jaws.Element.ApplyGetter]. [bind.New] exposes its backing
// pointer.
//
// After [bind.Setter.JawsSet] returns a result that does not match
// [jaws.ErrValueUnchanged], Input dirties the setter-derived tag so the server
// value can reconcile rejected or normalized browser input. Tags supplied as
// render parameters register the Element but do not replace that dirty target.
// Without a valid setter-derived target, automatic reconciliation does not occur.
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.
//
// For writable use, g must provide the setter-derived dirty target described 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.
//
// For writable use, g must provide the setter-derived dirty target described 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.
//
// For writable use, g must provide the setter-derived dirty target described 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.
//
// For writable use, g must provide the setter-derived dirty target described 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.
//
// For writable use, g must provide the setter-derived dirty target described 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.
//
// For writable use, g must provide the setter-derived dirty target described by [Input].
func NewTextarea(g bind.Setter[string]) *Textarea { return &Textarea{InputText{Setter: g}} }

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