kwebio / kweb-core

A Kotlin web framework
https://docs.kweb.io/book
GNU Lesser General Public License v3.0
969 stars 57 forks source link

Component Verbosity #396

Closed sigmadeltasoftware closed 1 year ago

sigmadeltasoftware commented 1 year ago

Hey everyone, first of all congratulations on the 1.0.0 release! Being an avid Kotlin user myself, I'm very excited to start using Kweb for some of my projects which I why I started to dabble into some prototyping.

Considering I'm mostly familiar with Android and React Native, I like to arrange my layout into lightweight, reusable components (or composables as they would be called in Compose) as it helps in regards of maintenance, testing, etc...

During this phase however, I noticed that the currently "recommended" component approach seems to be much verboser than it could be. Below is an excerpt from what can be currently found in your Kweb User Manual:

class SimpleComponent(
    val prompt: String = "Enter Your Name",
    val name: KVar<String>
) : Component {
    override fun render(elementCreator: ElementCreator<Element>) {
        with(elementCreator) {
            div {
                h1().text(prompt)
                input(type = text).value = name
            }
            div {
                span().text(name.map { "Hello, $it" })
            }
        }
    }
}

Considering the Component interface doesn't actually add any additional value aside from the render() method, I feel as if this could actually be reduced down to a scope/context as an extension function f.e.:

typealias EC =  ElementCreator<*>

When comparing the structure of both components, you can notice that the method approach is a lot less verbose:

class Body: Component {
    override fun render(elementCreator: ElementCreator<Element>) {
        with(elementCreator) {
            div {
                h1().text("Verbose 'Body class' component")
            }
        }
    }
}

fun EC.body() {
    div {
        h1().text("Function 'body()' component")
    }
}

As it is a lot more readable in execution as well:

        @JvmStatic
        fun main(args: Array<String>) {
            Kweb(port = 16097, plugins = plugins) {
                doc.body {
                    Body().render(this)
                    body()
                }
            }
        }

It's possible I'm overlooking something and would gladly be corrected, but wouldn't this be cleaner design choice where Component isn't an interface anymore but rather just a typealias:

typealias Component =  ElementCreator<*>

So users could just define new components by creating an extension function, and easily render these by simply calling the method?

sanity commented 1 year ago

Thank you for the kind words and the suggestion. This is an interesting idea and it's particularly valuable to get ergonomic feedback on the DSL.

The reason to use classes for components was mostly to allow parameters (normally KVars/KVals) to be passed to the component, like prompt and name in your first code example.

Can your proposal accommodate parameters, or it's just intended for cases where the component isn't parameterized?

sigmadeltasoftware commented 1 year ago

Hi @sanity , thank you as well for the effort & consideration.

Passing parameters along (properties, lambda's, ...) shouldn't be an issue:

class App {
    companion object {

        private val plugins = listOf(fomanticUIPlugin)

        @JvmStatic
        fun main(args: Array<String>) {
            Kweb(port = 16097, plugins = plugins) {
                doc.body {
                    val name = "SigmaDelta"
                    val onFinishedRendering = {
                        // action triggered when component is rendered
                    }
                    Body(name, onFinishedRendering).render(this)
                    body(name, onFinishedRendering)
                    // Or with anonymous function for specific actions:
                    body(name) {
                        // action triggered when component is rendered
                    }
                }
            }
        }
    }
}

class Body(val name: String, val onComponentRendered: () -> Unit): Component {
    override fun render(elementCreator: ElementCreator<Element>) {
        with(elementCreator) {
            div {
                h1().text("Hi $name")
            }
        }
        onComponentRendered()
    }
}

fun EC.body(name: String, onComponentRendered: () -> Unit) {
    div {
        h1().text("Hi $name")
    }
    onComponentRendered()
}

typealias EC = ElementCreator<*>

EDIT: I used default Strings in the example above, but the scenario remains the same for kVar/kVals or any type regardless.