Files
keluna/docs/component-api.md
mhoennig ba0a1cf419
werkator build succeeded after 02:49
form example
2026-09-16 12:08:41 +02:00

6.6 KiB

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.

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.

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, Vaadin Binder.