135 lines
6.6 KiB
Markdown
135 lines
6.6 KiB
Markdown
# Vaadin-oriented Kotlin API
|
|
|
|
Keluna's public API should move closer to Vaadin Flow at each development step.
|
|
The workshop example introduces typed component objects rather than application
|
|
code built out of generic DOM elements. There is no JVM or Vaadin dependency:
|
|
these are Kotlin/Native implementations inspired by familiar Flow concepts.
|
|
|
|
## Components and composition
|
|
|
|
Create components before mounting them. `VerticalLayout`, `HorizontalLayout`
|
|
and `FormLayout` own their children through `add(...)`. A component can have only
|
|
one parent and can be mounted once. Use `isVisible`, `isEnabled`, `id`,
|
|
`addClassName(...)` and `removeClassName(...)` without touching the DOM.
|
|
Disabling a layout disables its descendants. Invisible, disabled and read-only
|
|
fields reject client value changes in Kotlin as well as presenting native control
|
|
states. Read-only controls can still be changed programmatically.
|
|
|
|
```kotlin
|
|
import dev.keluna.core.UiTree
|
|
import dev.keluna.ui.*
|
|
|
|
data class Person(var name: String = "", var accepted: Boolean = false)
|
|
|
|
val name = TextField("Name").apply { maxLength = 100 }
|
|
val accepted = Checkbox("Teilnahmebedingungen akzeptieren")
|
|
val binder = Binder<Person>()
|
|
binder.forField(name).asRequired("Name fehlt").bind(Person::name)
|
|
binder.forField(accepted).asRequired("Zustimmung fehlt").bind(Person::accepted)
|
|
|
|
val draft = Person()
|
|
binder.readBean(draft)
|
|
val view = VerticalLayout(
|
|
FormLayout(name, accepted),
|
|
Button("Prüfen") {
|
|
if (binder.writeBeanIfValid(draft)) println(draft.name)
|
|
},
|
|
)
|
|
val tree = UiTree().apply { mount(view) }
|
|
```
|
|
|
|
`Container.textField(...)` and `Container.button(...)` also provide typed Kotlin
|
|
DSL helpers. Existing `Element.column`, `Element.button` etc. remain available
|
|
for the original Counter and Snake demos. Prefer the typed component layer for
|
|
new application code. The `element` escape hatch and a field's `inputElement`
|
|
become available after mounting.
|
|
|
|
## Typed field values
|
|
|
|
| Component | `value` type | Empty value |
|
|
|---|---|---|
|
|
| `TextField`, `EmailField`, `TelephoneField`, `TextArea` | `String` | `""` |
|
|
| `IntegerField` | `Int?` | `null` |
|
|
| `DatePicker` | `kotlinx.datetime.LocalDate?` | `null` |
|
|
| `TimePicker` | `kotlinx.datetime.LocalTime?` | `null` |
|
|
| `Checkbox` | `Boolean` | `false` |
|
|
| `Slider` | `Int` | configured minimum |
|
|
| `Select<T>`, `RadioButtonGroup<T>` | `T?` | `null` |
|
|
|
|
Fields implement `HasValue<T>` and `HasValidation`. `ValueField<T>` is the base
|
|
for custom fields. `clear()` restores `emptyValue`. Numeric/date/time conversion
|
|
errors are kept separately from empty values, so invalid input never silently
|
|
reuses an older valid value. Email validation is a basic address-shape check;
|
|
it does not verify delivery or implement every RFC address form.
|
|
|
|
```kotlin
|
|
val listener = name.addValueChangeListener { event ->
|
|
println("${event.oldValue} -> ${event.value}, client=${event.isFromClient}")
|
|
}
|
|
name.value = "Ada" // event with isFromClient == false
|
|
listener.remove()
|
|
```
|
|
|
|
Browser edits set `isFromClient` to true. Identical typed values do not generate
|
|
a value-change event, but parsing/validation state is still updated. Listener
|
|
registrations can be removed independently and idempotently. Buttons similarly
|
|
expose `addClickListener` with a typed `ClickEvent`.
|
|
|
|
## Buffered Binder
|
|
|
|
`Binder<B>` binds explicit getter/setter lambdas or Kotlin mutable property
|
|
references. It does not use reflection to discover properties by name.
|
|
|
|
- `readBean(bean)` loads field values and clears displayed binding errors.
|
|
- Editing fields does not mutate the bean; each binding validates on changes.
|
|
- `asRequired(message)` checks empty/blank values and marks the field as required.
|
|
- `withValidator(predicate, message)` adds model-value validation.
|
|
- `withConverter(toModel, toPresentation, errorMessage)` converts both directions;
|
|
conversion failure produces a field error instead of a model write.
|
|
- `validate()` validates every binding and returns a Boolean.
|
|
- `writeBeanIfValid(bean)` checks every binding before invoking any setter.
|
|
A field/conversion validation failure writes nothing and returns false.
|
|
|
|
Setters should be ordinary in-memory property updates. Rollback of throwing
|
|
setters, bean-level transactional validators and external side effects are not
|
|
implemented. Hidden or disabled fields are not silently excluded from validation;
|
|
use an explicit conditional validator. The workshop catering binding demonstrates
|
|
this and clears catering in the submitted model for online attendance.
|
|
|
|
`isRequiredIndicatorVisible` is a presentation/native-control hint; use Binder's
|
|
`asRequired` for a Kotlin-side required-value rule.
|
|
|
|
## Input synchronization
|
|
|
|
Values and checked state use DOM **properties**, rather than HTML attributes.
|
|
An `input` or `change` event forwards the node, a monotonically increasing edit
|
|
revision, the browser's `badInput` flag, and the raw string to Kotlin. The wire
|
|
format is `value:<node>:<revision>:<badInput 0|1>:<raw text>`; only the first four
|
|
colons are delimiters, so Unicode, colons and newlines survive unchanged. Unknown
|
|
nodes, malformed envelopes and stale/replayed revisions are rejected.
|
|
|
|
Kotlin takes client values into its state without echoing them to the originating
|
|
control. Programmatic property updates carry the latest processed revision; the
|
|
browser rejects older updates to an already edited control. Radio-group revisions
|
|
cover all peers. Equal values are not assigned again, preserving caret and
|
|
selection. Unfinished invalid number/date text is explicitly cleared on reset,
|
|
even if the DOM reports an empty value. IME composition is neither forwarded nor
|
|
overwritten until it commits. Field labels, helper/error text, `aria-invalid` and
|
|
`aria-describedby` are generated by the component implementation.
|
|
|
|
## Current limits compared with Vaadin Flow
|
|
|
|
This is an API direction, not source/binary compatibility. Current differences:
|
|
|
|
- The host is Kotlin/Native with GTK/WebKitGTK, not a Java server.
|
|
- Date/time values use kotlinx-datetime instead of java.time.
|
|
- Binder is a buffered subset: no `setBean`, bean-level validators or reflection binding.
|
|
- `forField` currently accepts Keluna `ValueField<T>` subclasses.
|
|
- Select/radio items are fixed at construction; no lazy data providers or dynamic item replacement yet.
|
|
- Components support adding children, not removal, reparenting or attach/detach lifecycle events yet.
|
|
- Typed components expose Element after mounting, not before.
|
|
- `Slider` is a small Keluna HTML-range component; not a claim of a matching Vaadin core class.
|
|
|
|
References: [Vaadin component model](https://vaadin.com/docs/latest/flow/component-internals/basic),
|
|
[Vaadin Binder](https://vaadin.com/docs/latest/flow/binding-data/components-binder).
|