How to construct the builder pattern around JavaFX componenets in Kotlin?

I am trying to come up with a builder pattern in Kotlin for javafx components. The pattern will look like below

fun main(args: Array<String>) {
    val vb = vbox {
        child {
            hbox {
                child {
                    label {
                        prefWidth = 20.0
                    }
                    button {
                        text = "Click"
                    }
                }
            }
            label {
                prefHeight = 80.0
            }
        }
    }
}

What I have done so far is as below, but the child is not exposing the label or the button or any of the methods declared in Child class. What am I missing here?

fun Pane.child(init: (Pane.() -> Unit)? = null): Child {
    val ch = Child()
    init?.invoke(this)
    ch.parent = this
    return ch
}

class Child {
    var parent: Pane? = null

    fun <T : Node> initChildNode(styleClass: String? = null, tag: T, init: (T.() -> Unit)? = null): T {
        init?.invoke(tag)
        tag.styleClass.add(styleClass)
        parent?.children?.add(tag)
        return tag
    }

    fun region(styleClass: String? = null, init: (Region.() -> Unit)? = null) = initChildNode(styleClass, Region(), init)
    fun vbox(styleClass: String? = null, init: (VBox.() -> Unit)? = null) = initChildNode(styleClass, VBox(), init)
    fun hbox(styleClass: String? = null, init: (HBox.() -> Unit)? = null) = initChildNode(styleClass, HBox(), init)
    fun label(styleClass: String? = null, init: (Label.() -> Unit)? = null) = initChildNode(styleClass, Label(), init)
    fun button(styleClass: String? = null, init: (Button.() -> Unit)? = null) = initChildNode(styleClass, Button(), init)

}

fun vbox(styleClass: String? = null, init: (VBox.() -> Unit)? = null) = initNode(styleClass, VBox(), init)
fun hbox(styleClass: String? = null, init: (HBox.() -> Unit)? = null) = initNode(styleClass, HBox(), init)

fun <T : Node> initNode(styleClass: String? = null, tag: T, init: (T.() -> Unit)? = null): T {
    init?.invoke(tag)
    tag.styleClass.add(styleClass)
    return tag
}

I know about the TornadoFX library and its builder interface, but I like to come up with a solution of my own mostly due to learning purpose.

You seem to be missing the fact that child has to have Child as the receiver of its init lambda (not the Pane!). I also hightly recommend to take a look at Hadi’s presentation on building DSLs with Kotlin to better understand all the concepts: Hadi Hariri @hhariri - Creating DSL's in idiomatic Kotlin - YouTube

2 Likes

Are you seen GitHub - edvin/tornadofx: Lightweight JavaFX Framework for Kotlin ?

1 Like

@elizarov thanks for pointing it out. I have finally made it to work. Here it is for future reference:

fun <T : Pane> T.children(init: (Children.() -> Unit)? = null): Children {
    val ch = Children()
    ch.parent = this
    init?.invoke(ch)
    return ch
}

class Children : Layout() {
    var parent: Pane? = null

    override fun <T : Node> initNode(styleClass: String?, tag: T, init: (T.() -> Unit)?): T {
        val node: T = super.initNode(styleClass, tag, init)
        parent?.children?.add(node)
        return node
    }
}

open class Layout {
    var pane: Pane? = null

    open fun <T : Node> initNode(styleClass: String? = null, tag: T, init: (T.() -> Unit)? = null): T {
        init?.invoke(tag)
        tag.styleClass.add(styleClass)
        return tag
    }

    fun region(styleClass: String? = null, init: (Region.() -> Unit)? = null) = initNode(styleClass, Region(), init)
    fun vbox(styleClass: String? = null, init: (VBox.() -> Unit)? = null) = initNode(styleClass, VBox(), init)
    fun hbox(styleClass: String? = null, init: (HBox.() -> Unit)? = null) = initNode(styleClass, HBox(), init)
    fun label(styleClass: String? = null, init: (Label.() -> Unit)? = null) = initNode(styleClass, Label(), init)
    fun button(styleClass: String? = null, init: (Button.() -> Unit)? = null) = initNode(styleClass, Button(), init)
    fun progressBar(styleClass: String? = null, init: (ProgressBar.() -> Unit)? = null) = initNode(styleClass, ProgressBar(), init)
}

fun <T : Pane> layout(styleSheet: String = "", init: Layout.() -> T): T {
    val layout = Layout()
    val pane = layout.init()
    layout.pane = pane
    pane.stylesheets.add(layout.javaClass.classLoader.getResource(styleSheet).toExternalForm())
    return pane
}

And the usage is like below

private var loadProgress: ProgressBar? = null
private var progressText: Label? = null

val layout = layout("xyz.css") {
            vbox("screen") {
                prefWidth = 500
                prefHeight = 450
                children {
                    region { minHeight = 250.0 }
                    hbox {
                        children {
                            region { minWidth = 30.0 }
                            label("large-label") {
                                text = "Foo"
                            }
                            label("large-label") {
                                text = "Bar"
                            }
                            region { minWidth = 20.0 }
                            label("small-label") {
                                text = "xyz"
                            }
                        }
                    }
                    region { minHeight = 10.0 }
                    loadProgress = progressBar("progress") { minWidth = this@vbox.prefWidth }
                    hbox {
                        children {
                            region { minWidth = 30.0 }
                            progressText = label("progress-label")
                        }
                    }
                }
            }
        }

Yes I have already seen that. I just wanted to come out with my own solution mainly for learning things.