Extending the DSL
The Kotlin DSL is extensible, allowing the addition of DSL functions for custom components or third-party libraries. You can build composite components that use the DSL internally.
Adding components to the DSL
To make any component available in the DSL, create an extension function on HasComponents that uses the init helper function.
Basic DSL function
Here's the pattern for a simple component. This example assumes you have a custom Badge component:
import com.webforj.concern.HasComponents
import com.webforj.kotlin.dsl.WebforjDsl
import com.webforj.kotlin.dsl.init
import com.example.component.Badge
fun @WebforjDsl HasComponents.badge(
block: @WebforjDsl Badge.() -> Unit = {}
): Badge {
return init(Badge(), block)
}
The init function does three things:
- Adds the component to the parent container
- Runs the configuration block
- Returns the configured component
Now you can use the component in DSL code:
div {
badge {
text = "New"
variant = Badge.Variant.PRIMARY
}
}
Adding parameters
Most DSL functions accept common parameters before the configuration block:
fun @WebforjDsl HasComponents.badge(
text: String? = null,
variant: Badge.Variant? = null,
block: @WebforjDsl Badge.() -> Unit = {}
): Badge {
val badge = Badge()
text?.let { badge.text = it }
variant?.let { badge.variant = it }
return init(badge, block)
}
Usage becomes more concise:
div {
badge("New", Badge.Variant.PRIMARY)
badge("Sale") {
styles["font-size"] = "12px"
}
}
Creating composite components
A Composite wraps multiple components into a single reusable unit. The DSL works well for defining composite structure.
Basic composite
class SearchBox : Composite<Div>() {
val searchField: TextField
val searchButton: Button
init {
boundComponent.apply {
styles["display"] = "flex"
styles["gap"] = "8px"
searchField = textField(placeholder = "Search...") {
styles["flex"] = "1"
}
searchButton = button("Search") {
theme = ButtonTheme.PRIMARY
}
}
}
fun onSearch(handler: (String) -> Unit) {
searchButton.onClick {
handler(searchField.text)
}
searchField.onEnter {
handler(searchField.text)
}
}
}
The composite exposes component references for external access and provides convenience methods for common operations.
Adding DSL support
Create a DSL function so the composite can be used like built-in components:
fun @WebforjDsl HasComponents.searchBox(
block: @WebforjDsl SearchBox.() -> Unit = {}
): SearchBox {
return init(SearchBox(), block)
}
Now it integrates naturally:
div {
h1("Product Catalog")
searchBox {
onSearch { query ->
filterProducts(query)
}
}
// Product list...
}
Example: Status indicator
Here's a complete example of a status indicator composite:
class StatusIndicator : Composite<Div>() {
private val dot: Div
private val label: Span
var status: Status = Status.INACTIVE
set(value) {
field = value
updateDisplay()
}
var text: String = ""
set(value) {
field = value
label.text = value
}
init {
boundComponent.apply {
styles["display"] = "flex"
styles["align-items"] = "center"
styles["gap"] = "8px"
dot = div {
styles["width"] = "10px"
styles["height"] = "10px"
styles["border-radius"] = "50%"
styles["background"] = "gray"
}
label = span()
}
updateDisplay()
}
private fun updateDisplay() {
dot.styles["background"] = when (status) {
Status.ACTIVE -> "#22c55e"
Status.WARNING -> "#f59e0b"
Status.ERROR -> "#ef4444"
Status.INACTIVE -> "#9ca3af"
}
}
enum class Status { ACTIVE, WARNING, ERROR, INACTIVE }
}
// DSL function
fun @WebforjDsl HasComponents.statusIndicator(
text: String? = null,
status: StatusIndicator.Status? = null,
block: @WebforjDsl StatusIndicator.() -> Unit = {}
): StatusIndicator {
val indicator = StatusIndicator()
text?.let { indicator.text = it }
status?.let { indicator.status = it }
return init(indicator, block)
}
Usage:
div {
statusIndicator("Database", StatusIndicator.Status.ACTIVE)
statusIndicator("Cache", StatusIndicator.Status.WARNING)
statusIndicator("External API", StatusIndicator.Status.ERROR)
}