Changes & Parts
Components write user input into the RxVars you pass them: open, selected, a field's text, a tree's open rows, a table's sort, a reorderable list's order, a splitter's size, a dock's layout. onChange sees each such change before it lands. The handler gets a ChangeDetails with the key that changes, the new value and the previous one, a reason, and a source. Calling cancel() keeps the old value:
scala
import wvlet.uni.dom.all.*
import wvlet.uni.dom.all.given
import wvlet.uni.rx.Rx
import wvlet.uni.ui.*
import scala.language.implicitConversions
val region = Rx.variable("tokyo")
val unsaved = Rx.variable(true)
val settings = Rx.variable("general")
div(
Select(region, Seq("tokyo", "oregon", "frankfurt"))(identity).onChange { d =>
if d.source == ChangeSource.Agent && d.value == "frankfurt" then d.cancel()
},
TabList(settings, "Settings")(Tab("general", "General"), Tab("billing", "Billing"))
.onChange(d => if unsaved.get then d.cancel())
)- Reasons.
ChangeReasonnames the interaction:Press,ItemPress,ActionPress,EscapeKey,OutsidePress,NavigationKey,Typeahead,Input,Drag,Reset,DeleteKey,FocusOut, andImperativefor calls such asdock.open(id). Make your own withChangeReason("voice-command"). - Sources.
ChangeSourceisPointer(mouse, touch, or pen),Keyboard,Programmatic, orAgent. A click that Enter or Space made on a button counts asKeyboard. Code that wraps a block inChangeSource.withSource(ChangeSource.Agent) { ... }marks every change inside it, which is how an AI agent driving the UI gets told apart from the user. - What is reported. Only changes the component makes. Writing
open := falseyourself reports nothing, and neither does a write that leaves the value as it was. - Text fields. A refused edit puts the field back. During an IME composition, the text stays in the field and goes through
onChangeonce, when the composition ends. - Callbacks.
onClick,onSelect,onRun,onConfirm, andonCancelkeep their signatures. Inside one,ChangeCause.currenttells what triggered it:
scala
Button("Delete").onClick { () =>
if ChangeCause.current.source == ChangeSource.Keyboard then println("pressed with a key")
}Parts
Every element a component renders carries data-slot, a stable name for CSS, tests, and tools. The root is the component's name, such as button, menu, or data-table, and its parts add a suffix: menu-item, dialog-title, dock-tab, select-option. Classes and data-state stay as they are, so existing styles keep working.
css
[data-slot='menu-item'][aria-disabled='true'] { opacity: 0.4; }