Skip to main content

Using the DSL

Open in ChatGPT

The Kotlin DSL provides builder functions for webforJ components. Each function creates a component, adds it to a parent container, and runs a configuration block. This page covers the patterns and conventions you'll use when building UIs with the DSL.

Naming conventions

DSL functions are provided for all standard webforJ components, including buttons, fields, layouts, dialogs, drawers, lists, and HTML elements. Each function uses the component class name in camelCase. Button becomes button(), TextField becomes textField(), and FlexLayout becomes flexLayout().

div {
button("Click me")
textField("Username")
flexLayout {
// nested content
}
}
Header and Footer methods

The header and footer DSL methods were renamed to nativeHeader and nativeFooter to avoid conflicts with header and footer slots of other components.

Using the Break component

One exception: Break uses backticks because break is a Kotlin keyword:

div {
span("Line one")
`break`()
span("Line two")
}

Creating components

Create a component by adding its DSL function to a parent block, along with the optional arguments and configuration block, as shown below:

div {
// Creates a Button, adds it to this div, then runs the block
button("Submit") {
theme = ButtonTheme.PRIMARY
onClick { handleSubmit() }
}
}

When you use a component's DSL function, it creates the component, adds it to the parent, and then runs the configuration block. The configuration block receives the component as its receiver (this), so you can access properties and methods directly:

textField("Email") {
placeholder = "you@example.com" // this.placeholder
required = true // this.required
onModify { validate() } // this.onModify(...)
}

Nesting components

Components that can contain children accept nested DSL calls inside their block:

flexLayout {
direction = FlexDirection.COLUMN

h1("Dashboard")

div {
paragraph("Welcome back!")
button("View Reports")
}

flexLayout {
direction = FlexDirection.ROW
button("Settings")
button("Logout")
}
}

Scope safety

The DSL enforces proper scoping. You can only add children to components that support them, and the compiler prevents accidental references to outer scopes:

div {
button("Submit") {
// This looks like it adds a paragraph inside the button,
// but it would actually add it to the outer div.
// The DSL catches this mistake at compile time.
paragraph("Submitting...") // Won't compile
}
}

If you need to add to an outer scope, use labeled this to make the intent explicit:

div {
button("Submit") {
this@div.add(Paragraph("Submitting...")) // Explicit is allowed
}
}

This keeps UI code predictable by making scope jumps visible.

Styling components

The Kotlin DSL provides a styles extension property that gives map-like bracket access to CSS properties, equivalent to setStyle() and getStyle() in Java:

button("Styled") {
styles["background-color"] = "#007bff"
styles["color"] = "white"
styles["padding"] = "12px 24px"
styles["border-radius"] = "4px"
}
CSS classes

For reusable styles, add CSS classes instead of inline styles. The HasClassName extension allows adding class names with +=:

button("Primary Action") {
classNames += "btn-primary"
}

Event handling

Components almost always need to respond to user interaction. The DSL provides concise event listener syntax using on prefix methods that accept lambdas:

button("Save") {
onClick {
saveData()
showNotification("Saved!")
}
}

textField("Search") {
onModify { event ->
performSearch(event.text)
}
}

Common parameters

In addition to configuration blocks, most DSL functions also accept common parameters before the block for frequently used options:

// Text parameter for labels/content
button("Click me")
h1("Page Title")
paragraph("Body text")

// Label and placeholder for fields
textField("Username", placeholder = "Enter username")
passwordField("Password", placeholder = "Enter password")

// Value parameters for inputs
numberField("Quantity", value = 1.0) {
min = 0.0
max = 100.0
}
Arguments with specified names

Named arguments let you pass parameters in any order, regardless of how they appear in the function signature.

Building a complete view

With these patterns in hand, here's a complete form that brings them together:

@Route("contact")
class ContactView : Composite<Div>() {

init {
boundComponent.apply {
styles["max-width"] = "400px"
styles["padding"] = "20px"

h2("Contact Us")

val nameField = textField("Name", placeholder = "Your name") {
styles["width"] = "100%"
styles["margin-bottom"] = "16px"
}

val emailField = textField("Email", placeholder = "you@example.com") {
styles["width"] = "100%"
}

val messageField = textArea("Message", placeholder = "How can we help?") {
styles["width"] = "100%"
}

button("Send Message") {
theme = ButtonTheme.PRIMARY
styles["width"] = "100%"

onClick {
submitForm(
name = nameField.text,
email = emailField.text,
message = messageField.text
)
}
}
}
}

private fun submitForm(name: String, email: String, message: String) {
// Handle form submission
}
}

The DSL keeps the UI structure readable while giving you full access to component configuration.